Skip to content

provider

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
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 dataclass

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

The maximum number of qubits of the hardware. None means that there is no limit, which is the case for every simulator.

supported_adjoint_gates frozenset[str] | None

The gates whose adjoint the backend can also run. None means all of supported_gates.

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.

required
inputs ndarray | Sequence[float]

The input vector, in the same kinds of container as weights.

required

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 property

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.

required
inputs ndarray | Sequence[float]

The inputs, 1-D or 2-D, in the same kinds of container as weights.

required

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.

required
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().

required
four_term_indices frozenset[int] | None

The inputs that feed a gate with several frequencies, as returned by ir.inputs_needing_four_term_shift.

None

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().

required
inputs ndarray | Sequence[float]

The input vector, in any container that compiled.run accepts: a NumPy array, a PyTorch tensor or a list.

required
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.

None

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.

required

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

A registered name, a configuration with a make_backend() method, or a backend instance.

required

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.

None

Returns:

Type Description
ndarray

The flattened statevector, after the checks.