## `eigenkit.core.provider`

The provider protocol, its registry, and the parameter-shift fallback.

A provider compiles a circuit separately from running it, which matters for speed. Building a `qml.device` and a QNode (PennyLane), or a parametrised `QuantumCircuit` and a `SparsePauliOp` (Qiskit), takes far longer than one execution. Doing that work once per `Circuit` and reusing the compiled handle for every forward pass is what makes repeated evaluations fast.

The `run` and `run_torch` methods of each backend accept batched arrays directly. The first axis is the batch, and `weights` and `inputs` can each be 1-D (one sample) or 2-D (a batch). Backends that can run a batch at once override `run` to submit one job. The others call `batch_call` and pay for a loop in Python, but the loop is inside the backend and not in the `Circuit` that calls it. The `Capabilities.supports_batching` flag only gives information, and nothing chooses a path based on it: `Circuit` always passes the batch on unchanged.

Example

```python
from eigenkit.core.provider import get, registered_names

print(registered_names())   # ['numpy', 'pennylane'] when no extra is installed

backend = get("numpy")
print(backend.gradient_method)                 # adjoint
print(backend.capabilities.supports_batching)  # True
```

### `Capabilities`

The description that a provider gives of what it supports.

It is used to check a circuit before compilation, and to choose between code paths at run time.

Attributes:

| Name                      | Type             | Description                                                                                                                                                                                                                                                                            |
| ------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `supported_gates`         | `frozenset[str]` | The names of the gates that the backend can run. A circuit that uses any other gate is rejected before compilation, with a message that names that gate.                                                                                                                               |
| `supports_batching`       | `bool`           | Whether the backend evaluates a 2-D inputs batch in one call, instead of looping over it internally. It only gives information: Circuit passes the batch on in both cases and never reads this flag. It describes the strategy of the backend, and nothing chooses a path based on it. |
| `supports_shots`          | `bool`           | Whether the backend can sample, instead of returning exact expectation values.                                                                                                                                                                                                         |
| `max_qubits`              | \`int            | None\`                                                                                                                                                                                                                                                                                 |
| `supported_adjoint_gates` | \`frozenset[str] | None\`                                                                                                                                                                                                                                                                                 |
| `supports_oracle_matrix`  | `bool`           | Whether the dense unitary of a callable oracle can be extracted. Iterative quantum amplitude estimation (IQAE) uses this to accept oracles that are PennyLane callables.                                                                                                               |
| `supports_provider_shots` | `bool`           | Whether the backend can sample an ancilla qubit with its own shots, which is what IQAE needs for shot_mode="provider".                                                                                                                                                                 |

### `CompiledCircuit`

Bases: `Protocol`

A handle to a circuit that a backend has prepared, cheap to call many times.

`run` accepts `weights` and `inputs` that are each either 1-D (one sample) or 2-D (a batch along the first axis). The output has shape `(n_obs,)` when both are 1-D, and `(B, n_obs)` otherwise. Each backend decides whether to evaluate the batch at once or in a loop (see `batch_call`).

Implementations can also provide `run_torch(...)`, for backends that support PyTorch automatic differentiation themselves. When the `gradient_method` of the backend is `"native"`, the PyTorch interface detects this method with `hasattr` and uses it instead of the parameter-shift fallback. The same shape rule applies.

#### `run(weights, inputs)`

Evaluate the circuit and return one expectation value for each observable.

See the class description for the shapes.

Parameters:

| Name      | Type      | Description       | Default                                                                                             |
| --------- | --------- | ----------------- | --------------------------------------------------------------------------------------------------- |
| `weights` | \`ndarray | Sequence[float]\` | The weight vector, in any container that can be indexed: a NumPy array, a PyTorch tensor or a list. |
| `inputs`  | \`ndarray | Sequence[float]\` | The input vector, in the same kinds of container as weights.                                        |

### `CostWarning`

Bases: `UserWarning`

Warning issued before a submission to a paid quantum backend.

A paid backend is a quantum processing unit (QPU) or a cloud simulator that charges for use. The first compilation for such a device issues this warning, so that costs cannot build up without the user noticing.

### `JobHandle`

Bases: `Protocol`

A handle to a job running on a quantum processing unit (QPU) or in the cloud.

`compiled.submit(...)` returns it without waiting for the job. Call `.result()` to wait for the job and combine its results.

#### `result()`

Wait until the job finishes, then return its expectation values.

The shape follows the same rule as CompiledCircuit.run(): `(n_obs,)` for a single sample, and `(B, n_obs)` for a batch.

#### `status()`

Return the state of the job as the provider reports it, without waiting.

The strings are the provider's own. For example, Braket returns `"CREATED"`, `"QUEUED"`, `"RUNNING"`, `"COMPLETED"`, `"FAILED"` or `"CANCELLED"`. Compare them within one provider, not across providers.

### `Provider`

Bases: `Protocol`

The protocol that every backend follows.

A provider turns a `CircuitSpec` into something that can run. Anything with these members can be passed as `provider=`: the built-in backends, the backend built by a configuration object, or your own class. To select it by a string instead, register it under a short name with `register`.

Attributes:

| Name              | Type             | Description                                                                                                                                       |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`            | `str`            | A short name, used in error messages and in repr.                                                                                                 |
| `gradient_method` | `GradientMethod` | How the gradient code should differentiate this backend: "adjoint", "parameter_shift", "native", or "none" for hardware that only returns values. |

#### `capabilities`

Return what this backend supports, as a `Capabilities` object.

It covers the supported gates, shots, batching and the other flags.

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

Prepare `circuit` so that `observables` can be evaluated many times.

A `Circuit` calls it once, and reuses the returned handle 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* |

### `batch_call(run_one, weights, inputs)`

Call `run_one(w_i, x_i)` for each sample along the first (batch) axis.

It is meant for backends that cannot run a batch at once. It holds the loop over the samples, and if one sample fails, it raises a `RuntimeError` that gives the index of that sample, so the caller can find which sample failed.

Parameters:

| Name      | Type       | Description                           | Default                                                                                                                                                                        |
| --------- | ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `run_one` | `Callable` | A function that evaluates one sample. | *required*                                                                                                                                                                     |
| `weights` | \`ndarray  | Sequence[float]\`                     | The weights, 1-D for one sample or 2-D for a batch. They must be a NumPy array or a PyTorch tensor, because the batch is detected from ndim and shape. A list is not accepted. |
| `inputs`  | \`ndarray  | Sequence[float]\`                     | The inputs, 1-D or 2-D, in the same kinds of container as weights.                                                                                                             |

Returns:

| Type      | Description                          |
| --------- | ------------------------------------ |
| `ndarray` | The results of the samples, stacked. |

### `get(name)`

Return the provider registered under `name`.

A backend registered with `register_lazy` imports its package at the first lookup, and the backend is kept for later lookups.

Parameters:

| Name   | Type  | Description                                              | Default    |
| ------ | ----- | -------------------------------------------------------- | ---------- |
| `name` | `str` | A registered short name, such as "numpy" or "pennylane". | *required* |

Returns:

| Type       | Description            |
| ---------- | ---------------------- |
| `Provider` | The provider instance. |

Raises:

| Type          | Description                                                                                                                                                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ImportError` | If name is a registered backend whose package is not installed, or is installed but fails to import. For a missing package the message names the extra that installs it. The registration is kept, so a later call works once the package is installed. |
| `KeyError`    | If nothing is registered under name. The message lists the available names and, for a known backend, the extra that installs it.                                                                                                                        |

### `load_failure_hint(exc)`

Name the fix for a package that is installed but fails to load, if known.

Parameters:

| Name  | Type          | Description                                      | Default    |
| ----- | ------------- | ------------------------------------------------ | ---------- |
| `exc` | `ImportError` | The ImportError that loading the package raised. | *required* |

Returns:

| Type  | Description                                                |
| ----- | ---------------------------------------------------------- |
| `str` | A parenthesised hint to append to an error message, or "". |

### `parameter_shift_input_jacobian(compiled, weights, inputs, four_term_indices=None)`

Return the jacobian with respect to the inputs, by shifting each input.

Each entry is `d<O_k>/dx_j`, computed by shifting `inputs[j]` directly by `±π/2` (and also by `±3π/2` for an input listed in `four_term_indices`). The result is correct only when the angle of every gate that depends on an input is exactly `InputRef(j)`, not wrapped in `Scale`, `Multiply`, `Sum` or `Negate`, and when each `InputRef(j)` feeds a single gate. When that is not the case, use the chain-rule path instead.

This simple path does not use `shift_gate_angle` (which wraps the angle in a `Sum`), so it works on backends that do not support symbolic `Sum` expressions, such as CUDA Quantum.

Parameters:

| Name                | Type              | Description                            | Default                                                                                                                                    |
| ------------------- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `compiled`          | `CompiledCircuit` | The prepared circuit to differentiate. | *required*                                                                                                                                 |
| `weights`           | \`ndarray         | Sequence[float]\`                      | The weight vector, in any container that compiled.run accepts: a NumPy array, a PyTorch tensor or a list.                                  |
| `inputs`            | \`ndarray         | Sequence[float]\`                      | The input vector, as a NumPy array or a list. A PyTorch tensor is not accepted, because the vector is shifted on a copy made with .copy(). |
| `four_term_indices` | \`frozenset[int]  | None\`                                 | The inputs that feed a gate with several frequencies, as returned by ir.inputs_needing_four_term_shift.                                    |

Returns:

| Type      | Description                                       |
| --------- | ------------------------------------------------- |
| `ndarray` | The jacobian, of shape (n_observables, n_inputs). |

### `parameter_shift_jacobian(compiled, weights, inputs, four_term_indices=None)`

Return the jacobian with respect to the weights, by the parameter-shift rule.

Each entry is `d<O_k>/dθ_i`, computed with the parameter-shift rule. For a weight whose gates have a single frequency, the rule takes two evaluations, `(f(θ + π/2) - f(θ - π/2)) / 2`. For a weight listed in `four_term_indices`, it uses the four-term rule.

Parameters:

| Name                | Type              | Description                            | Default                                                                                                                                                                                                                                                                               |
| ------------------- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `compiled`          | `CompiledCircuit` | The prepared circuit to differentiate. | *required*                                                                                                                                                                                                                                                                            |
| `weights`           | \`ndarray         | Sequence[float]\`                      | The weight vector, as a NumPy array or a list. A PyTorch tensor is not accepted, because the vector is shifted on a copy made with .copy().                                                                                                                                           |
| `inputs`            | \`ndarray         | Sequence[float]\`                      | The input vector, in any container that compiled.run accepts: a NumPy array, a PyTorch tensor or a list.                                                                                                                                                                              |
| `four_term_indices` | \`frozenset[int]  | None\`                                 | The weights that feed a gate with several frequencies, as returned by ir.weights_needing_four_term_shift. The two-term rule is not exact for those weights (on a controlled rotation it is wrong by tens of percent), so leaving them out here returns wrong gradients, not an error. |

Returns:

| Type      | Description                                        |
| --------- | -------------------------------------------------- |
| `ndarray` | The jacobian, of shape (n_observables, n_weights). |

### `register(name, provider_or_config)`

Register a backend, or a configuration that can build one, under `name`.

Later calls to `Circuit(..., provider=name)` then use that backend. If a configuration is passed, its `make_backend()` method is called to build the backend.

Parameters:

| Name                 | Type       | Description           | Default                                                              |
| -------------------- | ---------- | --------------------- | -------------------------------------------------------------------- |
| `name`               | `str`      | The name to register. | *required*                                                           |
| `provider_or_config` | \`Provider | object\`              | A backend instance, or a configuration with a make_backend() method. |

### `register_lazy(name, loader)`

Register a backend under `name` without importing its package yet.

`loader` is called the first time the name is looked up, and its result is kept for later lookups. The optional backends are registered in this way, so that naming them imports nothing.

It replaces whatever `name` currently refers to, as `register` does. If the registration were skipped when the name was already taken, a caller could not replace a built-in backend with a lazy one, and would get no sign that the loader had been ignored.

Parameters:

| Name     | Type                     | Description                                                                                               | Default    |
| -------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | ---------- |
| `name`   | `str`                    | The name to register.                                                                                     | *required* |
| `loader` | `Callable[[], Provider]` | A function with no arguments that returns the backend. It is called the first time the name is looked up. | *required* |

### `registered_names()`

Return every provider name that `get` accepts, including the lazy ones.

### `resolve_provider(spec)`

Turn a provider (a name, a configuration or a backend) into a backend instance.

A string is looked up in the registry. Any object with a `make_backend()` method is treated as a configuration. A backend is used as it is.

Parameters:

| Name   | Type  | Description | Default    |
| ------ | ----- | ----------- | ---------- |
| `spec` | \`str | object      | Provider\` |

Returns:

| Type       | Description           |
| ---------- | --------------------- |
| `Provider` | The backend instance. |

### `validate_circuit(circuit, capabilities)`

Raise a `ValueError` if the circuit cannot run on the provider.

The message says clearly why: the circuit uses a gate, or the adjoint of a gate, that the provider does not support, or it needs more qubits than the provider has.

Parameters:

| Name           | Type           | Description                         | Default    |
| -------------- | -------------- | ----------------------------------- | ---------- |
| `circuit`      | `CircuitSpec`  | The circuit to check.               | *required* |
| `capabilities` | `Capabilities` | What the provider says it supports. | *required* |

### `validate_initial_state(initial_state, n_qubits, *, batch=None)`

Check and flatten a statevector that the user gives as a starting state.

It returns a contiguous `complex128` array in the flat order of the library, where qubit `0` is the most significant bit. The shape is `(2**n,)` when `batch` is `None`, and `(batch, 2**n)` otherwise. A 1-D state passed with a `batch` is repeated for every row of the batch. It raises a `ValueError` if the length is wrong or the state is not normalised. A unitary evolution keeps the norm, so a state that is not normalised is almost always a mistake of the caller. Each backend reshapes the returned flat array into its own state layout. Doing the checks here, in one place, keeps the error messages the same for the NumPy, PennyLane and Qiskit backends.

Parameters:

| Name            | Type      | Description                          | Default                                                                              |
| --------------- | --------- | ------------------------------------ | ------------------------------------------------------------------------------------ |
| `initial_state` | `ndarray` | The statevector to start from.       | *required*                                                                           |
| `n_qubits`      | `int`     | The number of qubits of the circuit. | *required*                                                                           |
| `batch`         | \`int     | None\`                               | The batch size that the state must match, or None for a single state. Default: None. |

Returns:

| Type      | Description                                  |
| --------- | -------------------------------------------- |
| `ndarray` | The flattened statevector, after the checks. |
