Skip to content

numpy_backend

eigenkit.core.backends.numpy_backend

The reference NumPy statevector simulator, which is the default backend.

This backend uses only NumPy. It needs neither PyTorch nor an external quantum software development kit. It exists for two reasons:

  1. It works out of the box. The library runs (Circuit(provider="numpy")) without installing a large simulator or one that needs a licence. This backend is always available.
  2. It is the reference for correctness. Every other backend is tested against it (and against PennyLane) for equal results, so it also serves as the reference that the others are checked against.

It is written to be easy to read, not tuned for speed. For speed, install one of the optional backends. It does use two simple NumPy optimisations. A batch of inputs evolves in one vectorised pass, with a leading batch axis on the state tensor, instead of a Python loop over the samples. A controlled-NOT (CNOT) gate is applied by swapping amplitudes (np.flip on the subspace where the control is 1) instead of by contracting a 4x4 matrix.

State representation

The state is a complex128 tensor of shape (2,) * n_qubits, and qubit q is on axis q. In C order, this makes qubit 0 the most significant bit, as in PennyLane, where wire 0 is the most significant bit. As a result, state.reshape(-1)[0] is the amplitude of |0…0⟩, and the computed values agree with PennyLane to machine precision.

Gradients

The backend declares gradient_method = "adjoint" and implements NumpyCompiled.jacobian(). This method computes analytic gradients in reverse mode, with one forward pass and one backward pass, so the number of passes is O(1) in the number of parameters. The gradient code of the core package uses it in preference to the parameter-shift rule, which is the fallback, and it differentiates a 2-D inputs batch in one batched pass. The gradients agree with the parameter-shift rule and with PennyLane to machine precision.

Example
from eigenkit import Circuit, Encoding, Variational, all_z

blocks = [Encoding(in_features=4), Variational(depth=2)]

# by registered name
circuit = Circuit(blocks=blocks, observables=all_z(4), provider="numpy")

# or with the config, when a device setting matters
from eigenkit.configs import NumpyConfig

circuit = Circuit(
    blocks=blocks,
    observables=all_z(4),
    provider=NumpyConfig(),
)

This backend is always available. It is the default, and it needs no extra.

NumpyCompiled

Bases: CompiledCircuit

A circuit prepared for the reference NumPy simulator.

NumpySimulator.compile() builds it, and it is cheap to call many times. run() evaluates the observables, and jacobian() computes their analytic gradient. statevector(), probabilities() and zero_state_probability() read the state directly, which the Harrow-Hassidim-Lloyd (HHL) linear solver needs.

__init__(circuit, observables, config)

Prepare the circuit for repeated evaluation.

NumpySimulator.compile() builds this object, so there is no reason to build one directly.

Parameters:

Name Type Description Default
circuit CircuitSpec

The circuit to prepare.

required
observables Sequence[Observable]

The observables to measure.

required
config NumpyConfig

The device settings.

required

jacobian(weights, inputs, *, need_input_grad=False)

Compute the gradient with this backend's own method, the analytic adjoint.

The gradient code of the core package looks for a jacobian method. When a backend has one, it is used in preference to the parameter-shift rule, which works on any backend and is the fallback. Here it is the adjoint method in reverse mode: one forward pass and one backward pass, so the number of passes is O(1) in the number of parameters.

For a single sample (1-D inputs), the two jacobians have shapes (n_obs, n_weights) and (n_obs, n_inputs). For a 2-D inputs batch, they have a leading batch axis ((B, n_obs, n_weights) and (B, n_obs, n_inputs)), and the whole batch is differentiated in one pass. This is what makes the training of a hybrid quantum neural network with PyTorch fast on this backend. The gradients with respect to the gate angles are mapped onto the weights and inputs with the same symbolic chain-rule factors as the parameter-shift code, so angles built with Scale, Multiply, Sum or Negate are also differentiated correctly.

Parameters:

Name Type Description Default
weights ndarray

The parameter vector (1-D).

required
inputs ndarray

The input features: 1-D for one sample, or 2-D with a leading batch axis.

required
need_input_grad bool

If True, also compute the jacobian with respect to the inputs. Default: False.

False

Returns:

Name Type Description
weight_jacobian ndarray

The jacobian with respect to the weights.

input_jacobian ndarray | None

The jacobian with respect to the inputs, or None when need_input_grad is False.

probabilities(weights, inputs)

Return the measurement probabilities of all 2**n_qubits basis states.

Qubit 0 is the most significant bit, as in PennyLane's qml.probs. The probabilities are used to decode samples from the prepared state, for example the bitstrings of the Quantum Approximate Optimization Algorithm (QAOA).

Parameters:

Name Type Description Default
weights ndarray

The parameter vector (1-D).

required
inputs ndarray

The input features of one sample (1-D).

required

Returns:

Type Description
ndarray

The probabilities, of shape (2**n_qubits,).

run(weights, inputs)

Evaluate the circuit and return one expectation value per observable.

It follows the CompiledCircuit contract: the result has shape (n_obs,) when weights and inputs are both 1-D, and (B, n_obs) otherwise.

Parameters:

Name Type Description Default
weights ndarray

The parameter vector: 1-D for one sample, or 2-D with a leading batch axis.

required
inputs ndarray

The input features, with the same 1-D or 2-D shape convention as weights.

required

Returns:

Type Description
ndarray

The expectation values, one for each observable.

statevector(weights, inputs, initial_state=None)

Return the full complex statevector after the circuit has run.

The result is a flat array of 2**n_qubits amplitudes, with qubit 0 as the most significant bit (the same order as probabilities()). initial_state, a normalised flat vector in the same order, makes the evolution start from that state instead of |0…0⟩. Reading a state out and passing it back in this way is what quantum reservoir carry-over and amplitude encoding are built on. A 2-D inputs batch evolves in one pass and returns an array of shape (B, 2**n_qubits). initial_state can then be one state of shape (2**n,), shared by all samples, or a stack of shape (B, 2**n) with one state for each sample.

Parameters:

Name Type Description Default
weights ndarray

The parameter vector (1-D).

required
inputs ndarray

The input features: 1-D for one sample, or 2-D with a leading batch axis.

required
initial_state ndarray | None

The state to start from, instead of |0…0⟩. Default: None.

None

Returns:

Type Description
ndarray

The statevector, of shape (2**n_qubits,), or (B, 2**n_qubits) for a batch.

zero_state_probabilities(weights, inputs)

Return the probability of |0…0⟩ for every input of a batch, vectorised.

inputs has shape (B, n_inputs), and the result has shape (B,). The whole batch evolves in one pass. The quantum kernel uses this method to build a fidelity matrix without a Python loop over pairs.

Parameters:

Name Type Description Default
weights ndarray

The parameter vector (1-D).

required
inputs ndarray

The batch of inputs, of shape (B, n_inputs).

required

Returns:

Type Description
ndarray

The probabilities, one for each row, of shape (B,).

zero_state_probability(weights, inputs)

Return the probability of measuring the state |0…0⟩.

Fidelity kernels and swap tests are built on it. It does not go through Observable, because it is not the expectation value of a Pauli operator.

Parameters:

Name Type Description Default
weights ndarray

The parameter vector (1-D).

required
inputs ndarray

The input features of one sample (1-D).

required

Returns:

Type Description
float

The probability, a float in [0, 1].

NumpyConfig dataclass

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 property

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.