Skip to content

Noise

hqnn_forge.noise

hqnn_forge.noise

Noise channels, readout error and finite-shot evaluation for HQNN-Forge quantum layers.

The layers and hybrid classifiers can train under depolarizing, amplitude-damping, phase-damping, bit-flip or phase-flip noise, and with readout_error=(p01, p10) (see Training-time noise). Post hoc, apply_depolarizing_noise re-runs a trained model's quantum layer with any of the five channels for the duration of a with block, always as an exact density matrix on default.mixed whatever the layer's noise_method, swapping only the QNode and restoring it when the block exits. Gradients still flow, so optimizer steps inside the block can update model weights. apply_readout_error applies readout error, and noise_sweep repeats either over a range of strengths (channel="readout" for readout error). apply_shots and shot_sweep evaluate with finite shots (see Shot noise).

Channel

qml.DepolarizingChannel(p) maps ρ → (1−p)ρ + p/3 (XρX + YρY + ZρZ), so on its own it scales every Pauli expectation by 1 − 4p/3; p = 3/4 is fully depolarizing, and p is restricted to [0, 3/4].

position chooses where channels go, as in qml.noise.insert:

  • "all" (default) -- after every gate, on every wire the gate acts on: a simple gate-noise model whose effect grows with circuit size.
  • "end" -- once on every wire before measurement: readout-style noise that damps each ⟨Z_i⟩ by exactly 1 − 4p/3.
Other channels

channel (noise_channel on the layers) selects the channel p is the strength of. What each does to a ⟨Z⟩ readout with position="end":

======================== ============================= =================== channel effect on ⟨Z⟩ at "end" p range ======================== ============================= =================== "depolarizing" (1 − 4p/3) ⟨Z⟩ [0, 3/4] "amplitude_damping" (1 − p) ⟨Z⟩ + p (T1) [0, 1] "phase_damping" unchanged (T2 dephasing) [0, 1] "bit_flip" (1 − 2p) ⟨Z⟩ [0, 1] "phase_flip" unchanged [0, 1] readout error (1 − p01 − p10) ⟨Z⟩ each in [0, 1] (p01, p10) + (p10 − p01) ======================== ============================= ===================

Dephasing and phase flips commute with a Z measurement, so they act only through gates that follow them (position="all"). A readout error flips each measured bit, 0 → 1 with probability p01 and 1 → 0 with p10; on hardware p10 is usually the larger, as the qubit relaxes during measurement. It is classical, so it acts on the expectation values directly (readout_error_map), with no mixed-state simulation: apply_readout_error applies it post hoc, noise_sweep(..., channel="readout") sweeps it, and the layers' and classifiers' readout_error=(p01, p10) trains with it (#358). The symmetric case p01 = p10 = p is exactly "bit_flip" at "end". On a sampled layer each shot is flipped instead, from the device's generator (#486; see apply_readout_error). Because the map is applied to the layer's output rather than in its QNode, it exists only where the layer runs its circuit through TrainingNoiseMixin._run_circuit: a direct layer.qlayer(...) call is unaffected, as are the kernels, which rebuild the circuit from qlayer.qnode, and apply_readout_error refuses a layer without the mixin. Training noise by trajectories covers every channel; see Damping channels as trajectories below for how the two damping channels are sampled and what that rules out.

p = 0 replaces nothing: the original QNode stays in place, so the output is bit-identical to the noiseless model rather than merely close to it. It still counts as an active wrapper, so a layer's training-time noise (below), its readout_error included, is suppressed inside it just as for p > 0, but it does not count as a replaced QNode: a p > 0 block may be opened inside it.

Training-time noise

The encoding layers and hybrid classifiers also take noise_level, noise_position and noise_channel at construction. With noise_level > 0 the layer runs the same noisy QNode (built by training_noise_qnode) whenever it is in train mode, so gradients are computed through the noisy circuit, and the noiseless QNode in eval mode, like dropout. readout_error is the same kind of option, train mode only, with or without a noise_level. Unlike a channel that only shrinks ⟨Z⟩, it also shifts every output by p10 − p01, which the head learns: evaluate such a model inside apply_readout_error with the same pair, or it is evaluated on outputs without the shift it was trained on (#488). Evaluation under noise is then done with apply_depolarizing_noise / noise_sweep, which take precedence over the training-time channel if both are active at once. noise_level=0 (the default) leaves the layer exactly as before.

Two methods, chosen with noise_method:

"density" (default) The exact channel: mixed-state simulation on default.mixed, differentiated with backprop. It costs O(4^n) memory, and training keeps a batch × 4^n complex density matrix for every gate and every inserted channel for the backward pass. At 8 qubits, batch 64 and position="all", one step measured +2.7 GB peak and 50 s, so this is practical up to about 6 qubits. "trajectories" Pauli-trajectory (Monte Carlo) sampling on the layer's own device and differentiation method. At every channel site each sample independently gets one Pauli, drawn with the channel's weights: for depolarizing I with probability 1 − p and X, Y or Z with p/3 each; for bit flip I with 1 − p and X with p; for phase flip I with 1 − p and Z with p (the paulis weights in CHANNELS). Each mixture is its channel, so the output averaged over draws equals the "density" output, and so does the gradient: each step's loss gradient is an unbiased estimate of the one "density" computes, at pure-state cost. The same step measured +34 MB and 0.8 s on lightning.qubit with adjoint (+18 MB and 0.3 s noiseless). The price is gradient variance, as with dropout, which a fresh draw every forward pass resembles. noise_trajectories = k averages k draws per sample, at k times the cost, to reduce it.

Measured on one small proxy dataset (#311, breast cancer, 4 and 6
qubits, 5 seeds; ``docs/results/trajectory-noise-study.md``; the
benchmark datasets are #414): at
``p = 0.01``, and with noise only before measurement, trajectory
training matched the density channel; at ``p = 0.05`` after every gate
it was slower to leave the initial loss plateau, so that 3 of 20 runs
scored a test MCC below 0.5 within 30 epochs and a patience of 10,
density none.  Re-run for 60 epochs without early stopping, two of the
three trained (one of them at ``k = 4``) and one, at ``k = 1``, still
had not (#480).  A follow-up on the same data and budget over 10 seeds
(#347, ``docs/results/trajectory-collapse-study.md``) counted such runs
across five training variants: 10 of 100 at ``k = 1``, 6 of 100 at
``k = 4`` and none of 100 at ``noise_trajectories = 8``, which also
matched density.  A lower learning rate, gradient clipping and a noise
warm-up did not reliably help within that budget; the counts do not
separate a late start from a run that never trains (#480).  On
``default.qubit`` with backprop the draws run as one batch, so ``k = 8``
cost 1.1 to 1.4 times ``k = 1`` at 4 and 6 qubits; on the adjoint path
each sample runs separately, so it is expected to cost about 8 times
(not measured), which is why the default stays 1.  So keep ``"density"``
where it fits (up to about 6 qubits), and beyond that use
``"trajectories"`` with ``noise_trajectories ≥ 8``, or with fewer draws
a longer training budget than density needs, and check the runs.

**Damping channels as trajectories** (#357).  Phase damping with
strength ``γ`` keeps the diagonal of ρ and scales its off-diagonal by
``sqrt(1 − γ)``, which is exactly what a phase flip with probability
``p = (1 − sqrt(1 − γ))/2`` does, so it is sampled as that Pauli channel,
with no restriction.  Amplitude damping is not a mixture of unitaries:
its jump ``sqrt(γ)|0⟩⟨1|`` depends on the state, and a true quantum-jump
unravelling would need a state-dependent choice inside the circuit, which
a PennyLane tape cannot express.  Instead each site draws a Kraus branch
with a *fixed* probability -- the jump with ``q = γ/2`` -- and applies
``K_i / sqrt(q_i)`` as a non-unitary matrix.  The state is left
unnormalised, and the expectation value of the weighted state, averaged
over draws, is the channel's output exactly: enumerating every branch of
a two-qubit circuit reproduced the density result and its gradient to
1e-14.  The price is twofold.  First, a draw's output is no longer
bounded by ±1 and spreads more than a Pauli draw's: the RMS error of one
draw on a 4-qubit circuit with noise after every gate was 0.16, 0.32 and
0.88 at ``γ`` = 0.01, 0.05 and 0.2, against 0.13, 0.22 and 0.26 for
depolarizing trajectories at the same strength, so use more draws
(``noise_trajectories``) at larger ``γ``.  Second, it needs
``backprop``, ``parameter-shift`` or ``finite-diff``: ``adjoint`` inverts
every operation by its adjoint, which is wrong for a non-unitary one (its
gradient was off by 0.17 on the test circuit), and shot sampling draws
from normalised probabilities.  Third, the device must apply the given
matrix as it is: one that decomposes ``QubitUnitary`` into rotations
would run a unitary in its place and drop the weight.  That was checked
on ``default.qubit`` and ``lightning.qubit`` only (#477).  Any other
method or device, and shots, are refused when the layer is built.

The Pauli at a site is applied as ``RZ(π·z)`` then ``RX(π·x)`` with bits
``(x, z)``: ``(0, 0)`` is ``I``, ``(1, 0)`` is ``X``, ``(0, 1)`` is ``Z``
and ``(1, 1)`` is ``Y`` up to a global phase.  Every sample therefore
runs the same gate sequence with different angles, so parameter
broadcasting and the per-sample batch split of the adjoint path apply
unchanged.  The sites are placed by ``qml.noise.insert`` itself, so they
are exactly the sites the ``"density"`` path puts channels on.  The
draws use torch's global RNG, like dropout, so ``torch.manual_seed``
makes them reproducible -- on an exact layer.  With ``shots`` the
readouts are also sampled by the device's own generator, which torch does
not seed, so such a layer repeats only when built with ``seed``.
Shot noise

The other error a device adds is sampling: every expectation value is estimated from a finite number of measurements. A layer or classifier built with shots=N is sampled that way throughout (training with parameter-shift). apply_shots evaluates a model trained on exact values with N shots for the duration of a with block, and shot_sweep repeats its predictions over a range of shot counts, like noise_sweep over noise levels.

Position module-attribute

Position = Literal['all', 'end']

NoiseMethod module-attribute

NoiseMethod = Literal['density', 'trajectories']

Channel module-attribute

Channel = Literal['depolarizing', 'amplitude_damping', 'phase_damping', 'bit_flip', 'phase_flip']

MAX_P module-attribute

MAX_P = 0.75

CHANNELS module-attribute

CHANNELS: dict[str, _ChannelSpec] = {'depolarizing': _ChannelSpec(qml.DepolarizingChannel, MAX_P, lambda p: (1 - p, p / 3, p / 3, p / 3)), 'amplitude_damping': _ChannelSpec(qml.AmplitudeDamping, 1.0, None, _amplitude_damping_branches), 'phase_damping': _ChannelSpec(qml.PhaseDamping, 1.0, _phase_damping_as_phase_flip), 'bit_flip': _ChannelSpec(qml.BitFlip, 1.0, lambda p: (1 - p, p, 0.0, 0.0)), 'phase_flip': _ChannelSpec(qml.PhaseFlip, 1.0, lambda p: (1 - p, 0.0, 0.0, p))}

KRAUS_DIFF_METHODS module-attribute

KRAUS_DIFF_METHODS = ('backprop', 'parameter-shift', 'finite-diff')

Differentiation methods whose gradient through the weighted Kraus branches equals the density channel's (enumerated in the tests). A list of the methods allowed, not of those refused: adjoint reverses the circuit with each operation's adjoint as its inverse and is wrong, and a name such as "best" resolves to it on lightning without saying so.

KRAUS_DEVICES module-attribute

KRAUS_DEVICES = ('default.qubit', 'lightning.qubit')

Devices checked to apply a non-unitary QubitUnitary matrix as given. One that decomposes the operation runs a unitary in its place (#477).

MAX_TRAINING_NOISE_QUBITS module-attribute

MAX_TRAINING_NOISE_QUBITS = 6

Above this many qubits, a layer built with noise_method="density" warns.

TrainingNoiseMixin

Training-time noise for an encoding layer: construction, dispatch and repr.

The one implementation behind every layer's noise_level, noise_position, noise_method, noise_trajectories and readout_error. A layer calls _init_training_noise once its QNode exists, runs its circuit through _run_circuit (train mode with noise: the noisy QNode, else qlayer itself; eval passes under torch.no_grad() dispatch to an undifferentiated QNode when diff_method="adjoint") and appends _noise_repr to extra_repr. Everything else -- validation at construction, the memory warning, the QNode swap and its restore, the precedence of apply_depolarizing_noise -- happens here.

A readout error is classical and is applied to the circuit's output, not inside it: the layer's own readout_error in train mode, or apply_readout_error's in either mode. It therefore exists only on the _run_circuit path; qlayer(prepare_inputs(x)) called directly never has it.

qlayer instance-attribute

qlayer: TorchLayer

training instance-attribute

training: bool

noise_level instance-attribute

noise_level: float

noise_channel instance-attribute

noise_channel: Channel

noise_position instance-attribute

noise_position: Position

noise_method instance-attribute

noise_method: NoiseMethod

noise_trajectories instance-attribute

noise_trajectories: int

readout_error instance-attribute

readout_error: tuple[float, float] | None

shots property

shots: int | None

The shot count the layer samples with now; None for exact values.

Read from the QNode the layer runs, so it follows apply_shots (and is None inside a p > 0 apply_depolarizing_noise block, which simulates the exact channel) instead of repeating the construction argument.

NoiseSweepPoint

Bases: NamedTuple

One noise level of a sweep.

p instance-attribute

p: float | tuple[float, float]

The channel strength, or (p01, p10) for channel="readout".

probabilities instance-attribute

probabilities: Tensor

score instance-attribute

score: float | None

ShotSweepPoint

Bases: NamedTuple

One shot count of a sweep.

shots instance-attribute

shots: int | None

probabilities instance-attribute

probabilities: Tensor

(n_repeats, *predict_proba(X).shape): (n_repeats, n_samples) for a binary classifier, (n_repeats, n_samples, n_classes) for MulticlassHybridClassifier.

scores instance-attribute

scores: list[float] | None

One score per row of probabilities, or None without score_fn.

qnode_shots

qnode_shots(qnode: object) -> int | None

The shot count qnode samples with, or None for exact values.

validate_noise

validate_noise(p: float, position: str, *, p_name: str = 'p', position_name: str = 'position', channel: str = 'depolarizing', channel_name: str = 'channel') -> None

Raise ValueError unless channel is known, 0 <= p is at most the channel's maximum, and position is known.

The *_name arguments are the names the messages use, so a caller that exposes these under other names (noise_level) is quoted.

training_noise_qnode

training_noise_qnode(qnode: QNode, n_qubits: int, p: float, position: Position = 'all', *, channel: Channel) -> qml.QNode

The noisy counterpart of qnode an encoding layer runs in train mode.

Same construction as apply_depolarizing_noise uses: the layer's circuit function on default.mixed with channel of strength p inserted at position, differentiated with backprop. The layer's own device_name and diff_method apply to its noiseless path only; mixed-state simulation costs O(4^n) memory per sample, and training through it keeps one such state per operation for backprop (see the module docstring).

Raises:

Type Description
ValueError

If channel is unknown, p is 0 or above the channel's maximum, or position is unknown.

check_kraus_trajectories

check_kraus_trajectories(channel: str, qnode: QNode) -> None

Raise ValueError if channel's trajectories cannot run on qnode.

Only channels that are not drawn as Pauli errors are affected: amplitude damping, sampled by weighted Kraus branches. Their states are unnormalised, so the gradient is right only for the methods in KRAUS_DIFF_METHODS (adjoint was off by 0.17 on a two-qubit test circuit, #357; diff_method=None computes none and is allowed), the matrices reach the simulator unchanged only on the devices in KRAUS_DEVICES, and shot sampling, drawing from normalised probabilities, cannot represent them. A channel with neither sampler is refused as well.

trajectory_noise_qnode

trajectory_noise_qnode(qnode: QNode, p: float, position: Position = 'all', *, channel: Channel) -> qml.QNode

qnode with channel sampled as trajectories.

Runs on qnode's own device and differentiation method; each call draws a fresh error pattern per sample (see the module docstring). The average over draws equals training_noise_qnode's output. The Pauli channels -- depolarizing, bit flip, phase flip, and phase damping, which is a phase flip -- are drawn as Pauli errors; amplitude damping as weighted Kraus branches, which rules out adjoint, shots and devices other than default.qubit and lightning.qubit (see check_kraus_trajectories).

Raises:

Type Description
ValueError

If p is 0 or out of range, position is unknown, or amplitude damping is asked for on a qnode whose differentiation method, device or shots check_kraus_trajectories refuses.

run_with_training_noise

run_with_training_noise(qlayer: TorchLayer, noisy_qnode: QNode, x: Tensor, n_trajectories: int = 1) -> torch.Tensor

Evaluate qlayer on x with noisy_qnode in place of its QNode.

n_trajectories > 1 (the "trajectories" method only) runs each sample that many times and returns the mean, each run with its own draw.

If apply_depolarizing_noise is active on the layer, its channel (none at p = 0) is kept and the training-time one is not applied: the post-hoc wrapper is the evaluation instrument and wins. The original QNode is restored afterwards, including when the forward pass raises.

validate_noise_method

validate_noise_method(method: str, n_trajectories: object) -> None

Raise ValueError unless method is known and n_trajectories fits it.

n_trajectories must be a positive int (not bool), and 1 for "density", which is exact and has nothing to average.

validate_readout_error

validate_readout_error(readout_error: object, name: str = 'readout_error') -> tuple[float, float] | None

readout_error as a pair of plain floats (p01, p10), or None.

Accepts any ordered pair of real numbers in [0, 1] -- a tuple, a list, an array or a tensor of two, NumPy scalars included -- and returns built-in floats, which is the form to store: it survives a checkpoint and does not alias the caller's object.

Raises:

Type Description
ValueError

Unless readout_error is None or such a pair. A string, a mapping and a set are refused: they have no (p01, p10) order. So is a boolean of any kind (bool, np.bool_, a boolean tensor): a comparison result is not a probability. So is an element that is not a scalar, such as the rows of a 2 × 1 array or tensor.

readout_error_map

readout_error_map(expectations: Tensor, p01: float, p10: float) -> torch.Tensor

⟨Z⟩ of each measured qubit after an asymmetric readout error.

Each measured bit reads 1 instead of 0 with probability p01 and 0 instead of 1 with probability p10: a classical confusion matrix on the qubit's outcome distribution. With P(0) = (1 + ⟨Z⟩)/2 that gives

⟨Z⟩ → (1 − p01 − p10) ⟨Z⟩ + (p10 − p01),

exactly, for every single-qubit ⟨Z⟩ readout, which is all the layers measure. p01 = p10 = p is "bit_flip" at position="end".

This is the effect on exact expectation values. A sampled layer (shots, or inside apply_shots) flips its shots instead (#486), so that the spread is that of a device with this readout error; the map is then the mean of that output and gives its gradient (see apply_readout_error).

apply_readout_error

apply_readout_error(model: Module, p01: float, p10: float) -> Iterator[nn.Module]

Evaluate model with an asymmetric readout error for the with block.

Applies readout_error_map to the quantum layer's output, in train and eval mode alike, without touching the weights, and on top of an open apply_depolarizing_noise or apply_shots block. A layer's own training-time readout_error is replaced, not added to, while the block is open.

Sampled layers (#486). With shots (the layer's own, or an apply_shots block's) the error flips every shot rather than rescaling the estimate: each output has the distribution of the mean of shots flipped shots, mean ⟨Z'⟩ and variance (1 − ⟨Z'⟩²)/shots, which is what a device with this readout error gives. An estimate ẑ from N shots is k = N (1 + ẑ)/2 zeros (outcome 0 is Z = +1); the zeros read out are Binomial(k, 1 − p01) + Binomial(N − k, p10), exactly in distribution, and the output is (2k' − N)/N. The same applies to the layer's own readout_error in train mode.

  • Generator. The flips are drawn from the device's own NumPy generator, the one the shots come from, so a layer built with seed repeats them whatever torch's seed, and SPSA's common random numbers cover them; a device without one gets a generator seeded from torch's (one draw from it per forward pass).
  • Trajectories. With noise_method="trajectories" in train mode an output is the mean of noise_trajectories draws of shots shots each; the flips are drawn on the pooled count of all of them. The shots are flipped independently, and a sum of independent binomials with the same probability is the binomial of the summed count, so this is the same in distribution as flipping each draw's shots.
  • Gradient. A flipped count is piecewise constant in the weights, so its own derivative is zero almost everywhere. The output therefore carries the gradient of readout_error_map of the sampled estimate, (1 − p01 − p10) times the sampled gradient, the same estimator as before #486 (a seeded layer's values differ, as the flips now draw from its generator before the backward pass's shots): the conditional mean of the flipped output given the shots is exactly that map, so with parameter-shift's unbiased sampled gradient this is an unbiased estimate of ∂⟨Z'⟩/∂θ. SPSA uses the flipped values themselves.
  • (0, 0) draws nothing and returns the sampled estimate unchanged.

On exact values (shots=None) the map is the whole effect, unchanged.

The map is applied where the layer runs its circuit (TrainingNoiseMixin._run_circuit), not inside the QNode, so the layer must be a TrainingNoiseMixin whose forward goes through it, as every built-in encoding layer's does. Code that runs the circuit itself inside the block -- a direct layer.qlayer(...) call, or hqnn_forge.kernels, which rebuild it from qlayer.qnode -- gets its output without the error.

Raises:

Type Description
ValueError

If p01 or p10 is not in [0, 1].

TypeError

If model has no quantum layer, or the layer is not a TrainingNoiseMixin: the block would do nothing on it, and the model would read as untouched by readout error.

RuntimeError

If a block is already open on the same layer.

apply_depolarizing_noise

apply_depolarizing_noise(model: Module, p: float, *, position: Position = 'all', channel: Channel = 'depolarizing') -> Iterator[nn.Module]

Run model's quantum layer with noise inside the block.

Named for its default channel; channel selects any of CHANNELS.

Parameters:

Name Type Description Default
model Module

A hybrid classifier or an encoding layer.

required
p float

Strength of each channel: the depolarizing probability in [0, 0.75], or the damping / flip probability in [0, 1].

required
position Position

"all" or "end"; see the module docstring.

'all'
channel Channel

"depolarizing" (default), "amplitude_damping", "phase_damping", "bit_flip" or "phase_flip"; see the module docstring.

'depolarizing'

Yields:

Type Description
Module

model itself, for convenience.

Raises:

Type Description
TypeError

If model has no quantum layer.

ValueError

If p or position is out of range.

RuntimeError

If the layer is already running a replaced QNode (nested use).

Notes

The original QNode is restored on exit, including when the block raises. Gradients flow through the noisy circuit, so the block can also be used to fine-tune under noise.

noise_sweep

noise_sweep(model: Module, X: Tensor, ps: Iterable[float] | Iterable[float | tuple[float, float]], *, position: Position = 'all', channel: Channel | Literal['readout'] = 'depolarizing', y: Tensor | None = None, score_fn: Callable[[Tensor, Tensor], float] | None = None) -> list[NoiseSweepPoint]

Predict X at each noise level in ps, without retraining.

Parameters:

Name Type Description Default
model Module

A classifier with predict_proba (the hybrid classifiers).

required
X Tensor

Inputs to evaluate.

required
ps Iterable[float] | Iterable[float | tuple[float, float]]

Channel strengths, each in the channel's range; consumed once. With channel="readout", readout errors instead: a pair (p01, p10), or one number p for the symmetric (p, p). Numbers may be NumPy scalars or 0-d tensors, so a torch.linspace works for either kind of channel. ps is always the list of levels: a single asymmetric error is [(p01, p10)]. A bare (p01, p10) is two symmetric levels, (p01, p01) and (p10, p10), unlike the two arguments of apply_readout_error.

required
position Position

Passed to apply_depolarizing_noise. channel="readout" sweeps apply_readout_error instead; position is then validated but has no effect.

'all'
channel Position

Passed to apply_depolarizing_noise. channel="readout" sweeps apply_readout_error instead; position is then validated but has no effect.

'all'
y Tensor | None

If both are given, score_fn(y, probabilities) is recorded per level, e.g. lambda y, p: find_optimal_threshold(y, p).score.

None
score_fn Tensor | None

If both are given, score_fn(y, probabilities) is recorded per level, e.g. lambda y, p: find_optimal_threshold(y, p).score.

None

Returns:

Type Description
list[NoiseSweepPoint]

In the order of ps.

Raises:

Type Description
ValueError

If only one of y and score_fn is given, if channel is neither a noise channel nor "readout", or if any level is out of range -- checked before the first evaluation, not as the sweep reaches it.

TypeError

If model has no predict_proba, or with channel="readout" if its quantum layer is not a TrainingNoiseMixin (see apply_readout_error).

apply_shots

apply_shots(model: Module, shots: int | None) -> Iterator[nn.Module]

Run model's quantum layer with shots samples per circuit inside the block.

Every expectation value is then estimated from shots measurements, as on hardware: a readout with exact value ⟨Z⟩ comes back with standard deviation sqrt((1 − ⟨Z⟩²) / shots). This evaluates a model trained on exact values under sampling, without rebuilding it or touching its weights; shots=None gives exact values again, for a reference point.

The layer's circuit runs on its own device through a parameter-shift QNode, the one differentiation method that samples unbiased gradients, so gradients inside the block are sampled too. A layer with noise_method="trajectories" training noise samples its train-mode trajectories with the block's shots as well; one with "density" training noise, which simulates the exact channel, raises in train mode, and so does one with amplitude-damping trajectories, whose unnormalised states cannot be sampled (eval mode, where training noise is off, is unaffected in both cases). The original QNodes are restored on exit, including when the block raises.

Raises:

Type Description
TypeError

If model has no quantum layer.

ValueError

If shots is not None or a positive int.

RuntimeError

If the layer is already inside apply_shots or a p > 0 apply_depolarizing_noise block, whose exact channel shots would not apply to.

shot_sweep

shot_sweep(model: Module, X: Tensor, shots: Iterable[int | None], *, n_repeats: int = 5, y: Tensor | None = None, score_fn: Callable[[Tensor, Tensor], float] | None = None) -> list[ShotSweepPoint]

Predict X n_repeats times at each finite shot count, without retraining.

The repeats show the spread shot noise alone puts on the predictions and on the score; None in shots gives the exact reference, evaluated and scored once with its probabilities and score repeated n_repeats times. That assumes predict_proba is deterministic at shots=None, as it is for the hqnn_forge models, which predict in eval mode; a custom one that leaves dropout on would have its spread hidden. Arguments as for noise_sweep.

Raises:

Type Description
ValueError

If n_repeats < 1, only one of y and score_fn is given, or a shot count is invalid -- checked before the first evaluation.

TypeError

If model has no predict_proba.