Skip to content

Quantum Reservoir Computing

Quantum reservoir computing (QRC) uses a quantum circuit as a fixed dynamical system, called the reservoir, that is never trained. Only a classical model on top of the features it produces, called the readout, is trained, and it is inexpensive to train. The weights of the reservoir are sampled once and then frozen, so all of the learning happens in the readout.

This page covers eigenkit.reservoir, the part of the library that uses only NumPy and needs no other framework. It contains the driver, the state policies, the feature extractors and a ridge-regression readout. Use these building blocks when you want full control, for example to process the features in your own way or to use your own readout from scikit-learn or PyTorch. The scikit-learn-style estimators built on them, QuantumReservoirClassifier and QuantumReservoirRegressor, are described on the quantum machine learning (QML) page.

Everything relies on Circuit.statevector(weights, inputs, initial_state=). The state is carried from one timestep to the next by reading the statevector out and injecting it back as the initial state of the following step.

The driver

run_reservoir(circuit, policy, sequences, weights) evolves the circuit over a batch of sequences under a policy, and returns the trajectory of raw statevectors. The driver does not compute features or fit a readout, so the three layers can be combined freely: the driver first, then a feature extractor, then a readout.

import numpy as np
from eigenkit import Circuit, Encoding, Variational, all_z
from eigenkit.reservoir import (
    run_reservoir,
    LeakyCarry,
    expectation_features,
    LinearReadout,
)

circuit = Circuit(
    blocks=[Encoding(in_features=3, rotation="RY"), Variational(depth=3, rotation="RY")],
    observables=all_z(3),
    provider="numpy",
)
weights = np.random.default_rng(0).standard_normal(circuit.spec.n_weights)
sequences = np.random.default_rng(1).uniform(-1, 1, size=(8, 40, 3))

states = run_reservoir(
    circuit, LeakyCarry(alpha=0.5), sequences, weights
)  # (8, 40, 8) complex
feats = expectation_features(states, "Z0 + Z1 + 0.5 X0 X1")  # (8, 40, 1)
readout = LinearReadout(ridge=1e-4)

State policies

A policy decides, at each timestep, which state the circuit starts from and which inputs drive it. Two policies carry the quantum state from one timestep to the next, so the timesteps run one after another. The third policy carries no state, so all timesteps can run in parallel.

Policy Constructor What it does Memory Execution
Stateless Stateless(window=1) Starts each step from |0…0⟩ and encodes the last window inputs as rotation angles. classical (the window) parallel: all timesteps in one batched run
Carry Carry() Applies the circuit to the previous state, ψ_t = U(x_t) ψ_{t-1}. quantum state sequential
LeakyCarry LeakyCarry(alpha, reset=None) Mixes the carried state with a reset state and renormalises the result. quantum, fading sequential

With the carry policies, the circuit takes n_inputs = F inputs per step, where F is the number of features per step. With Stateless, it takes the whole window, so the circuit must encode n_inputs = window * F. The driver checks this and raises a clear error otherwise.

Fading memory matters

A Carry policy preserves the norm of the state, but it tends to mix the history into a form that a linear readout cannot recover, so it often has little usable memory. A memory that fades over time is what makes a reservoir work in practice, so LeakyCarry with alpha between 0.3 and 0.7 is usually the right starting point for temporal tasks. The memory-capacity example shows this.

Feature extractors

The readout trains on real-valued features extracted from the trajectory of statevectors. Each extractor takes the trajectory returned by the driver and returns real-valued features for each timestep.

Extractor Features per step Notes
pauli_z_features(states) n_qubits The expectation value of Z on each qubit. This is the usual default in QRC; it is inexpensive and can be measured on hardware.
probability_features(states) 2**n_qubits The measurement distribution over all basis states. It carries more information, and it is quadratic in the amplitudes.
amplitude_features(states) 2 * 2**n_qubits The real and imaginary parts of the state. Only a simulator can provide them; they cannot be measured on hardware.
expectation_features(states, observables) one per observable The expectation values of any Pauli observables. Pass the same Observable or PauliExpr objects, or Hamiltonian strings, that the rest of the library uses. A single Hamiltonian string is one observable.

Readout

LinearReadout(ridge=1e-06) performs ridge regression in closed form, with a bias term and no iterative optimiser. It is only a convenience: the features are a plain NumPy array, so any scikit-learn or PyTorch model can replace it.

Backends and limitations

  • Quantum reservoir computing runs on any backend that provides a statevector and accepts an injected initial state: NumPy, PennyLane, the Qiskit statevector simulator and the CUDA-Q simulators. Backends without access to the statevector, such as the Braket cloud services and Qiskit Runtime, raise a clear NotImplementedError. The default provider="numpy" needs no extra dependencies.
  • The carry policies use state injection, which cannot be differentiated, so the reservoir is frozen and only the classical readout is trained. This is the standard setup in QRC, and it avoids computing gradients through the carried state.
  • LeakyCarry approximates a dissipative reservoir with pure states. True dissipation needs density matrices, which the pure-state backends do not provide. The renormalised mixing used here approximates that behaviour, but it is not a physical quantum channel.

See the short-term memory example for a worked benchmark that compares the policies.

API reference

eigenkit.reservoir

Quantum reservoir computing (QRC).

A circuit with fixed weights, usually random, is driven over input sequences. The quantum state is carried from one timestep to the next, and classical features are read out from it.

The reservoir circuit is never trained; only a classical readout is. The driver, the policies, the feature extractors and the ridge-regression readout use only NumPy. Like optimization and chemistry, this package belongs to the core of the library, so it needs neither PyTorch nor a backend package.

Three policies cover the common variants of QRC:

  • Stateless starts every step from |0…0⟩ and encodes a sliding window of inputs. All timesteps run in parallel, in one batched evaluation.
  • Carry carries the state from one step to the next with the circuit's unitary: ψ_t = U(x_t) ψ_{t-1}.
  • LeakyCarry mixes the carried state with a reset state, and renormalises the result.
Example
from eigenkit import Circuit
from eigenkit.reservoir import (
    run_reservoir, Carry, expectation_features, LinearReadout,
)

states = run_reservoir(circuit, Carry(), sequences, weights)  # (B, T, 2**n)
feats = expectation_features(states, "Z0 + Z1 + 0.5 X0 X1")   # (B, T, 1)
readout = LinearReadout(ridge=1e-4).fit(feats.reshape(B * T, -1), targets)

Carry dataclass

Policy that applies the circuit to the carried state: ψ_t = U(x_t) ψ_{t-1}.

The quantum state is the memory of the reservoir, and the first step starts from |0…0⟩. This policy is equivalent to LeakyCarry(alpha=1.0).

advance(x_t, evolved, state)

Build the state for the next timestep from the result of this step.

Parameters:

Name Type Description Default
x_t ndarray

The input of this timestep, of shape (B, F).

required
evolved ndarray

The statevector that the circuit produced.

required
state ReservoirState

The state that went into this step.

required

Returns:

Type Description
ReservoirState

The state for the next timestep.

initial(batch)

Return the state carried into the first timestep.

Parameters:

Name Type Description Default
batch int

The number of sequences that run in parallel.

required

Returns:

Type Description
ReservoirState

The initial ReservoirState.

inject(x_t, state)

Turn the input of this timestep into the arguments of the circuit call.

Parameters:

Name Type Description Default
x_t ndarray

The input of this timestep, of shape (B, F).

required
state ReservoirState

The state carried over from the previous step.

required

Returns:

Name Type Description
initial_state ndarray | None

The state to evaluate the circuit from, or None to start from |0…0⟩.

gate_inputs ndarray

The values to pass to the gates of the circuit.

LeakyCarry dataclass

Policy that mixes the carried state with a reset state and renormalises it.

It approximates a leaky, dissipative reservoir with pure states: ψ_t = normalise(α U(x_t) ψ_{t-1} + (1 − α) reset).

reset defaults to |0…0⟩ and should be a normalised statevector; this is not checked. With alpha=1, the policy is exactly Carry. This is not a true dissipative channel, which would need density matrices that the pure-state backends do not provide. It is an approximation that uses pure states only.

Attributes:

Name Type Description
alpha float

The mixing weight of the carried state, in [0, 1]. Lower values forget faster, and 1 keeps everything, which is exactly Carry.

reset ndarray | None

The state that is mixed in, a normalised statevector. None means |0…0⟩. The default is None.

advance(x_t, evolved, state)

Build the state for the next timestep from the result of this step.

Parameters:

Name Type Description Default
x_t ndarray

The input of this timestep, of shape (B, F).

required
evolved ndarray

The statevector that the circuit produced.

required
state ReservoirState

The state that went into this step.

required

Returns:

Type Description
ReservoirState

The state for the next timestep.

initial(batch)

Return the state carried into the first timestep.

Parameters:

Name Type Description Default
batch int

The number of sequences that run in parallel.

required

Returns:

Type Description
ReservoirState

The initial ReservoirState.

inject(x_t, state)

Turn the input of this timestep into the arguments of the circuit call.

Parameters:

Name Type Description Default
x_t ndarray

The input of this timestep, of shape (B, F).

required
state ReservoirState

The state carried over from the previous step.

required

Returns:

Name Type Description
initial_state ndarray | None

The state to evaluate the circuit from, or None to start from |0…0⟩.

gate_inputs ndarray

The values to pass to the gates of the circuit.

LinearReadout

Ridge regression in closed form, with a bias term.

It solves W = (XᵀX + λI)⁻¹ XᵀY, and the bias column is not regularised. Y can have one output or several. ridge is the strength λ of the L2 penalty, which adds λ‖W‖² to the squared error of the fit. A small positive value also keeps the solution numerically stable when the reservoir features are collinear.

Parameters:

Name Type Description Default
ridge float

The strength λ of the L2 penalty. Raise it when the readout overfits or the feature matrix is nearly singular, and lower it when the readout underfits. The default is 1e-06.

1e-06
fit(X, y)

Fit the readout to X and y.

Parameters:

Name Type Description Default
X ndarray

The samples, of shape (n_samples, n_features).

required
y ndarray

The targets, of shape (n_samples,) for one output, or (n_samples, n_outputs) for several.

required

Returns:

Type Description
'LinearReadout'

This readout, so that calls can be chained.

predict(X)

Predict the target for every sample in X.

Parameters:

Name Type Description Default
X ndarray

The samples, of shape (n_samples, n_features).

required

Returns:

Type Description
ndarray

One prediction per sample, with one column per output when the readout was fitted on several outputs.

ReservoirPolicy

Bases: Protocol

A reservoir policy that carries state from one timestep to the next.

Parallel policies, which carry no state, do not implement this protocol. They provide expand_inputs(sequences), which returns the inputs of every timestep with shape (B, T, n_inputs), and the driver evaluates all timesteps at once. See Stateless.

advance(x_t, evolved, state)

Build the state for the next timestep from the result of this step.

Parameters:

Name Type Description Default
x_t ndarray

The input of this timestep, of shape (B, F).

required
evolved ndarray

The statevector that the circuit produced.

required
state ReservoirState

The state that went into this step.

required

Returns:

Type Description
ReservoirState

The state for the next timestep.

initial(batch)

Return the state carried into the first timestep.

Parameters:

Name Type Description Default
batch int

The number of sequences that run in parallel.

required

Returns:

Type Description
ReservoirState

The initial ReservoirState.

inject(x_t, state)

Turn the input of this timestep into the arguments of the circuit call.

Parameters:

Name Type Description Default
x_t ndarray

The input of this timestep, of shape (B, F).

required
state ReservoirState

The state carried over from the previous step.

required

Returns:

Name Type Description
initial_state ndarray | None

The state to evaluate the circuit from, or None to start from |0…0⟩.

gate_inputs ndarray

The values to pass to the gates of the circuit.

ReservoirState dataclass

The state that sequential policies carry from one timestep to the next.

vector is the (B, 2**n) statevector to inject at the next step, and None means that the step starts from |0…0⟩. history holds the recent raw inputs, of shape (B, k, F), for a sequential policy that encodes a window of inputs.

Attributes:

Name Type Description
vector ndarray | None

The statevector to inject at the next step, of shape (B, 2**n). None means that the step starts from |0…0⟩.

history ndarray | None

The last k raw inputs, of shape (B, k, F), for a policy that encodes a window of them. It is None when the policy keeps no history.

Stateless dataclass

Parallel policy that carries no state.

Every step starts from |0…0⟩, and the last window inputs are encoded as rotation angles of the circuit's gates. The memory is purely classical (the window), and the quantum state carries nothing from one step to the next. Every timestep is therefore independent, and the driver evaluates all of them in one batched run. With window=1, the policy has no memory: it encodes only the current input.

The circuit's n_inputs must equal window * F, where F is the number of features per step, because each step passes the whole flattened window to the encoding gates.

Attributes:

Name Type Description
window int

The number of consecutive inputs to encode at each step. 1 means no memory: only the current input is encoded. Whatever the value, the circuit's n_inputs must equal window * F. The default is 1.

expand_inputs(sequences)

Return the gate inputs of every timestep, with shape (B, T, window * F).

Step t encodes the inputs t - window + 1 to t. The first window - 1 steps are padded with zeros at the front. No quantum state is involved, so this is only a reshaping of the input sequence.

Parameters:

Name Type Description Default
sequences ndarray

The whole input batch, of shape (B, T, F).

required

Returns:

Type Description
ndarray

The gate inputs for every timestep, of shape (B, T, window * F).

amplitude_features(states)

The raw statevector as real features: its real and imaginary parts, concatenated.

states is a complex array of shape (B, T, 2**n), and the result is a real array of shape (B, T, 2 * 2**n). It exposes the full state, which only a simulator can provide; it cannot be measured on hardware.

Parameters:

Name Type Description Default
states ndarray

The trajectory of statevectors, of shape (B, T, 2**n).

required

Returns:

Type Description
ndarray

The real and imaginary parts, concatenated, for each timestep.

expectation_features(states, observables)

Expectation values ⟨ψ|H_k|ψ⟩ of each observable, over the whole trajectory.

states is a complex array of shape (B, T, 2**n), and the result is a real array of shape (B, T, n_obs). observables can be an Observable, a PauliExpr or a Hamiltonian string, or a sequence of these. These are the same types that the rest of the library uses, so measuring an arbitrary Hamiltonian, as QRC experiments often require, needs no manual handling of Pauli terms. A single Hamiltonian string is one observable, and gives one feature.

Parameters:

Name Type Description Default
states ndarray

The trajectory of statevectors, of shape (B, T, 2**n).

required
observables Sequence[Observable]

The observables to measure.

required

Returns:

Type Description
ndarray

One feature per observable and per timestep.

pauli_z_features(states)

The expectation value ⟨Z_q⟩ on each qubit, the usual default readout in QRC.

states has shape (B, T, 2**n), and the result has shape (B, T, n). The values are computed with expectation_features(), so both functions share the same tested measurement code.

Parameters:

Name Type Description Default
states ndarray

The trajectory of statevectors, of shape (B, T, 2**n).

required

Returns:

Type Description
ndarray

One feature per qubit and per timestep.

probability_features(states)

The measurement distribution over all 2**n basis states, |ψ|².

states is a complex array of shape (B, T, 2**n), and the result is a real array of the same shape.

Parameters:

Name Type Description Default
states ndarray

The trajectory of statevectors, of shape (B, T, 2**n).

required

Returns:

Type Description
ndarray

The distribution over the basis states, for each timestep.

run_reservoir(circuit, policy, sequences, weights)

Evolve circuit over sequences under policy, with frozen weights.

The result is the trajectory of statevectors, a complex array of shape (B, T, 2**n).

sequences has shape (B, T, F): B independent series, T timesteps and F features per step. weights is the fixed 1-D parameter vector of the reservoir, shared by the whole batch. Features are extracted separately from the returned trajectory (see the feature extractors), so the same run can produce expectation, probability or amplitude features without evolving the circuit again.

Parameters:

Name Type Description Default
circuit Circuit

The reservoir circuit, run with frozen weights.

required
policy Policy

How the state passes from one timestep to the next.

required
sequences ndarray

The inputs, of shape (B, T, F).

required
weights ndarray

The frozen weights of the reservoir.

required

Returns:

Type Description
ndarray

The trajectory of statevectors, of shape (B, T, 2**n).