Skip to content

Diagnostics

hqnn_forge.diagnostics

hqnn_forge.diagnostics

Tools for inspecting the quantum side of a hybrid model without reading the circuit source: what the circuit costs on hardware, and later, how it trains. The separation measures apply to any fixed feature matrix, whatever produced it.

Exported symbols

CircuitSummary Frozen record: qubits, depth, gate counts, trainable parameters. circuit_summary Build a CircuitSummary from an encoding layer or a hybrid classifier. draw_circuit Text drawing of the same circuit, for logs and notebooks. count_inert_parameters Trainable gate parameters that can never reach a measurement. count_inert_weights The same counted per weight entry, as circuit_summary reports it. LOGICAL_GATE_SET Gate names circuits are decomposed to before counting; circuit_summary also decomposes a MultiRZ on more than two wires. gradient_variance Variance of the cost gradient over random weight draws. gradient_variance_sweep The same over a grid of qubit and layer counts. GradientVarianceResult Result of gradient_variance. format_sweep Text table of a sweep. fisher_information_matrix Fisher matrix and spectrum of the quantum weights. fisher_information_spectrum Its eigenvalues only, descending. FisherSpectrum Result of fisher_information_matrix. effective_dimension Effective dimension (Abbas et al. 2021) over random draws. effective_dimension_from_spectra The formula alone, from Fisher eigenvalues. EffectiveDimensionResult Result of effective_dimension. expressibility KL divergence of the fidelity distribution from Haar (Sim et al. 2019). entangling_capability Mean Meyer–Wallach entanglement over sampled states. meyer_wallach Meyer–Wallach Q of given state vectors. ExpressibilityResult, EntanglingCapabilityResult Their results. separation_measures Every scalar separation measure of a feature matrix and its labels. SeparationMeasures Result of separation_measures. pairwise_distances Pairwise feature distances, within and between classes, and their ratio. PairwiseDistances Result of pairwise_distances. fisher_discriminant_ratio Fisher discriminant ratio along the best linear direction. effective_rank Effective rank of the feature covariance (exponential spectral entropy). linear_feature_kernel The linear kernel F Fᵀ, for kernel_target_alignment. stratified_subsample_indices Deterministic, class-stratified subsample indices.

LOGICAL_GATE_SET module-attribute

LOGICAL_GATE_SET: frozenset[str] = frozenset({'Hadamard', 'RX', 'RY', 'RZ', 'Rot', 'PhaseShift', 'CNOT', 'CZ', 'MultiRZ'})

Gate names a circuit is decomposed to before its resources are counted. Every gate the library's circuits emit is in here, so the count is of the circuit as written; a template such as AngleEmbedding is expanded. circuit_summary does not decompose to exactly this set: it keeps MultiRZ only on at most two wires and decomposes a wider one to its CNOT ladder (see _decompose_logical), so decompose(gate_set=...) with this set alone counts a wide MultiRZ once and does not reproduce it.

CircuitSummary dataclass

Resource summary of one quantum encoding layer's circuit.

Attributes:

Name Type Description
layer_type str

Class name of the summarised layer, e.g. "QuantumEncodingLayer".

n_qubits int

Wires the circuit acts on.

n_trainable_params int

Trainable entries in the layer's weight tensors (requires_grad).

depth int

Longest path of gates through the logical circuit.

n_gates int

Total gate count after decomposition to LOGICAL_GATE_SET (with MultiRZ kept only on at most two wires).

n_two_qubit_gates int

Two-qubit gates (CNOT, CZ, two-wire MultiRZ) after gates on more than two wires have been decomposed into them, i.e. the circuit's two-qubit cost. A wider gate with no decomposition counts once.

gate_counts Mapping[str, int]

Count per gate name, sorted by name. This field is a mapping, so the dataclass is frozen for immutability but is not hashable.

device_name str

PennyLane device the layer's QNode is bound to, after any fallback.

diff_method str

Differentiation method the QNode is configured with.

n_inert_params int

Trainable parameters that cannot affect any measurement for any input or weight values, found structurally by count_inert_parameters. The typical case is the ω of a Rot whose wire carries only Z-type content downstream, such as a last-layer Rot followed at most by CNOTs and a ⟨Z⟩ readout: Rot = RZ(ω)·RY(θ)·RZ(φ) and the final RZ commutes with Z. The count is of weight entries (count_inert_weights): an entry is inert when every gate parameter it feeds is inert, so it is on the same footing as n_trainable_params.

layer_type instance-attribute

layer_type: str

n_qubits instance-attribute

n_qubits: int

n_trainable_params instance-attribute

n_trainable_params: int

depth instance-attribute

depth: int

n_gates instance-attribute

n_gates: int

n_two_qubit_gates instance-attribute

n_two_qubit_gates: int

gate_counts class-attribute instance-attribute

gate_counts: Mapping[str, int] = field(default_factory=dict)

device_name class-attribute instance-attribute

device_name: str = ''

diff_method class-attribute instance-attribute

diff_method: str = ''

n_inert_params class-attribute instance-attribute

n_inert_params: int = 0

n_effective_params property

n_effective_params: int

n_trainable_params - n_inert_params: the trainable weight entries that can move the output. Both count weight entries, so the difference holds for a layer that reuses an entry in several gates or computes an angle from several entries, and it is never negative.

to_dict

to_dict() -> dict[str, Any]

Plain-dict form, for logging frameworks and JSON.

Besides the fields it holds the derived n_effective_params; drop that key to rebuild a CircuitSummary from the dict.

EntanglingCapabilityResult dataclass

Attributes:

Name Type Description
entangling_capability float

Mean Meyer–Wallach Q over the sampled states, in [0, 1].

standard_error float

Standard error of that mean.

haar_reference float

(N − 2) / (N + 1), the expected Q of a Haar-random state.

values Tensor

Q of every sampled state, float64.

layer_type instance-attribute

layer_type: str

n_qubits instance-attribute

n_qubits: int

n_samples instance-attribute

n_samples: int

entangling_capability instance-attribute

entangling_capability: float

standard_error instance-attribute

standard_error: float

haar_reference instance-attribute

haar_reference: float

values instance-attribute

values: Tensor

ExpressibilityResult dataclass

Attributes:

Name Type Description
expressibility float

D_KL(P_circuit ‖ P_Haar) in nats; lower is more expressive.

maximum float

(N − 1) · ln(n_bins), the value of a circuit whose state never changes, for reading expressibility on a scale.

fidelities Tensor

The n_pairs sampled fidelities, float64.

layer_type instance-attribute

layer_type: str

n_qubits instance-attribute

n_qubits: int

n_pairs instance-attribute

n_pairs: int

n_bins instance-attribute

n_bins: int

expressibility instance-attribute

expressibility: float

maximum instance-attribute

maximum: float

fidelities instance-attribute

fidelities: Tensor

EffectiveDimensionResult dataclass

Effective dimension of a model's quantum weights.

Attributes:

Name Type Description
layer_type, init, n_params, n_data, gamma

What was measured and with which constants.

n_theta_samples int

Parameter draws the expectation over θ was taken over.

effective_dimension float

d_{γ,n} of Abbas et al.

normalized_effective_dimension float

d_{γ,n} / n_params, the figure to compare across architectures.

mean_normalized_spectrum Tensor

Mean over draws of the trace-normalised eigenvalues, descending.

layer_type instance-attribute

layer_type: str

init instance-attribute

init: str

n_params instance-attribute

n_params: int

n_data instance-attribute

n_data: int

gamma instance-attribute

gamma: float

n_theta_samples instance-attribute

n_theta_samples: int

effective_dimension instance-attribute

effective_dimension: float

normalized_effective_dimension instance-attribute

normalized_effective_dimension: float

mean_normalized_spectrum class-attribute instance-attribute

mean_normalized_spectrum: Tensor = field(compare=False)

to_dict

to_dict() -> dict[str, Any]

Scalar fields only, for logging.

FisherSpectrum dataclass

Fisher information of a model's quantum weights at their current values.

Attributes:

Name Type Description
layer_type str

Class name of the model or layer that was measured.

likelihood str

"bernoulli" for a classifier, "gaussian" for an encoding layer.

n_params int

Number of quantum parameters measured, d.

n_data int

Rows of data_sample the expectation over x was taken over.

matrix Tensor

The (d, d) Fisher matrix, float64.

eigenvalues Tensor

Its eigenvalues in descending order, float64, length d.

parameter_slices dict[str, slice]

For each measured tensor, by TorchLayer argument name, the slice of rows and columns of matrix that belongs to it.

layer_type instance-attribute

layer_type: str

likelihood instance-attribute

likelihood: str

n_params instance-attribute

n_params: int

n_data instance-attribute

n_data: int

matrix class-attribute instance-attribute

matrix: Tensor = field(compare=False)

eigenvalues class-attribute instance-attribute

eigenvalues: Tensor = field(compare=False)

parameter_slices class-attribute instance-attribute

parameter_slices: dict[str, slice] = field(compare=False, default_factory=dict)

trace property

trace: float

rank property

rank: int

Eigenvalues above 1e-10 · max; 0 if the matrix is zero.

normalized_eigenvalues property

normalized_eigenvalues: Tensor

Eigenvalues scaled to sum to n_params (zeros if the trace is 0).

block

block(name: str) -> torch.Tensor

The (d_name, d_name) block of matrix for tensor name.

to_dict

to_dict() -> dict[str, Any]

Scalar fields plus the eigenvalues as a list, for logging.

GradientVarianceResult dataclass

Gradient-variance estimate for one layer configuration.

per_parameter and per_tensor are excluded from == and hash: comparing them would return a Tensor rather than a bool, so two results compare on their scalar fields only.

Attributes:

Name Type Description
layer_type, n_qubits, n_layers

What was measured.

init str

Name of the initialisation scheme ("custom" for a callable).

input_scale float

Inputs were drawn from [-input_scale, input_scale].

n_samples int

Number of random draws.

total_variance float

Sum of the per-entry gradient variance over every trainable tensor of the layer, i.e. over the whole gradient vector.

mean_variance float

total_variance divided by the number of trainable entries.

per_parameter Tensor

Per-entry variance. For a layer with one trainable tensor, the common case, it has that tensor's shape. With several, it is the flat concatenation of per_tensor's values, in that mapping's order.

per_tensor Mapping[str, Tensor]

Per-entry variance per trainable tensor, keyed by the TorchLayer argument name ("weights", "input_scaling"), each shaped like its tensor.

layer_type instance-attribute

layer_type: str

n_qubits instance-attribute

n_qubits: int

n_layers instance-attribute

n_layers: int

init instance-attribute

init: str

input_scale instance-attribute

input_scale: float

n_samples instance-attribute

n_samples: int

total_variance instance-attribute

total_variance: float

mean_variance instance-attribute

mean_variance: float

per_parameter class-attribute instance-attribute

per_parameter: Tensor = field(compare=False)

per_tensor class-attribute instance-attribute

per_tensor: Mapping[str, Tensor] = field(default_factory=dict, compare=False)

to_dict

to_dict() -> dict[str, Any]

Scalar fields only, for logging.

PairwiseDistances dataclass

The Euclidean distances of every pair of samples, split by class membership.

Two results are equal when both sets of distances are, element by element; a result is not hashable.

Attributes:

Name Type Description
within Tensor

D_ij of the pairs i < j with y_i = y_j, float64, length n0(n0−1)/2 + n1(n1−1)/2, in the pair order (0,1), (0,2), …, (M−2,M−1) with the other pairs left out.

between Tensor

D_ij of the pairs with y_i ≠ y_j, length n0 · n1, in the same order.

within instance-attribute

within: Tensor

between instance-attribute

between: Tensor

mean_within property

mean_within: float

Mean distance within a class, over pairs; NaN if no class has two samples.

mean_between property

mean_between: float

Mean distance between the classes, over pairs; 0 only if every row is the same.

ratio property

ratio: float

mean_between / mean_within, in (0, ∞]; about 1 for labels that are independent of the features. inf if each class is one point and the points differ; NaN if all distances are 0 or no class has two samples.

SeparationMeasures dataclass

The scalar separation measures of one feature matrix; see the module docstring.

Two records are equal when every field is, an undefined (NaN) ratio counting as equal to an undefined one; equal records have the same hash.

Attributes:

Name Type Description
n_samples, n_features

Shape (M, d) of the feature matrix.

mean_within_distance, mean_between_distance

Means of the Euclidean distances over the pairs within a class and between the classes, in the unit of the features.

distance_ratio float

mean_between_distance / mean_within_distance, in (0, ∞]; about 1 without class information.

fisher_ratio float

Fisher discriminant ratio along the best linear direction, δᵀ S_w⁻¹ δ with the pooled within-class covariance (denominator n_samples − 2), in [0, ∞].

effective_rank float

Exponential of the entropy of the normalised eigenvalues of the feature covariance, in [1, min(d, M − 1)].

kernel_alignment float

Centred alignment of the linear feature kernel F Fᵀ with the labels, in [0, 1].

The two ratios can be ``inf`` (perfect separation) or NaN (undefined, as
for identical rows), which strict JSON has no value for.

n_samples instance-attribute

n_samples: int

n_features instance-attribute

n_features: int

mean_within_distance instance-attribute

mean_within_distance: float

mean_between_distance instance-attribute

mean_between_distance: float

distance_ratio instance-attribute

distance_ratio: float

fisher_ratio instance-attribute

fisher_ratio: float

effective_rank instance-attribute

effective_rank: float

kernel_alignment instance-attribute

kernel_alignment: float

to_dict

to_dict() -> dict[str, Any]

Every field as a plain int or float, in the order above, for logging.

circuit_summary

circuit_summary(target: Module) -> CircuitSummary

Summarise the circuit of an encoding layer or of a hybrid classifier.

Parameters:

Name Type Description Default
target Module

A QuantumEncodingLayer / IQPEncodingLayer, or a classifier with a quantum_layer attribute (HybridBinaryClassifier, ParallelHybridClassifier).

required

Returns:

Type Description
CircuitSummary

Depth, gate counts, parameter count and inert-parameter count of the logical circuit. The result does not depend on the device: the same layer configuration gives the same summary on default.qubit and lightning.qubit.

Examples:

>>> from hqnn_forge.encoding import QuantumEncodingLayer
>>> from hqnn_forge.diagnostics import circuit_summary
>>> summary = circuit_summary(QuantumEncodingLayer(n_qubits=4, n_layers=2))
>>> summary.n_two_qubit_gates
8
>>> print(summary)
Circuit summary: QuantumEncodingLayer on lightning.qubit (adjoint)
  qubits           : 4
  ...

count_inert_parameters

count_inert_parameters(tape: QuantumScript) -> int

Number of trainable gate parameters that cannot affect any measurement of tape, for any input or weight values.

The measured observables are propagated backwards through the circuit in the Heisenberg picture, keeping per wire only whether the observable's content there is nothing, diagonal (Z-type) or possibly X/Y: CNOT moves Z content from target to control and X/Y content from control to target; a diagonal gate leaves Z content alone, and a diagonal multi-qubit gate (CZ, MultiRZ, IsingZZ) spreads Z content to all its wires once one of them carries X/Y; any other gate turns the content on its wires into X/Y. A trainable parameter is inert when its gate acts on wires with no content at all, or when the gate is diagonal (RZ, PhaseShift, MultiRZ, CZ, IsingZZ, ...) or the final RZ(ω) of a Rot and every wire it touches carries only diagonal content: the gate then commutes with everything measured.

The propagation over-approximates the X/Y content, so the count is a lower bound on the parameters that are dead for structural reasons: everything it counts has an exactly zero gradient for every input, and parameters that are dead only for particular inputs or weights (say, a rotation of |0⟩ about Z) are not counted.

Templates and other gates with tensor-valued parameters (such as StronglyEntanglingLayers) are first decomposed until every gate parameter is a scalar, so each counted parameter is one gate-parameter slot. A parameter-broadcast tape is rejected, since one slot there stands for a whole batch of values.

A measurement counts as diagonal when every Pauli word of its observable is a product of Z (Z(0), 2 * Z(0), Z(0) + Z(1), nested products, Z(0) @ I(1); for an observable without a Pauli representation, when its matrix is diagonal and it acts on at most six wires), and when it is a computational-basis measurement without an observable (probs, sample, counts). state, density_matrix and any other measurement mark their wires as X/Y content, and a measurement without wires (state, probs over all wires) marks every wire. A mid-circuit measurement counts as a measurement of arbitrary content on its wire, since its outcome may drive a conditional gate or be returned. Whether a gate is diagonal is decided structurally (_is_diagonal_gate: its generator, a symbolic wrapper of a diagonal gate, or the matrix of a gate without parameters on at most six wires, so a wide QFT never builds its dense matrix), so CRZ, ControlledPhaseShift, Adjoint(RZ) or a conditional RZ count as diagonal. Gates that are neither diagonal nor CNOT nor Rot are treated as fully mixing, which keeps the count a lower bound for any gate.

Raises:

Type Description
ValueError

If tape uses parameter broadcasting, or a gate with a tensor-valued parameter cannot be decomposed to scalar-parameter gates.

count_inert_weights

count_inert_weights(tape: QuantumScript, weights: Sequence[Tensor]) -> int

Number of entries of weights that cannot affect any measurement of tape: an entry is inert when every gate parameter it feeds is inert (count_inert_parameters), or when it feeds none.

This counts weight entries, the unit of n_trainable_params, rather than gate-parameter slots. The two differ for a layer that reuses one entry in several gates (an inert RZ(w) and a live RX(w): the entry is live) or computes one angle from several entries (an inert RZ(w1·w2): both entries are inert).

Which entries a slot depends on is read off autograd: the entries with a nonzero gradient of the slot's value. Build tape from generic inputs (not zeros), so that no dependence vanishes by accident, e.g. through an input-times-weight angle at input 0. The same holds for the weights themselves: a live angle at a stationary point of its function of the weights (RX(w1·w2) at w1 = w2 = 0) reads as depending on neither entry, so both are counted inert. Only weights with requires_grad are counted.

Parameters:

Name Type Description Default
tape QuantumScript

As for count_inert_parameters, with gate parameters computed from weights by torch operations.

required
weights Sequence[Tensor]

The weight tensors the gate parameters are computed from.

required

draw_circuit

draw_circuit(target: Module, inputs: Tensor | None = None, decimals: int = 2) -> str

Text drawing of the logical circuit for one sample, with the layer's current weights. Suitable for print or a log line alongside a training run.

Parameters:

Name Type Description Default
target Module

Same as for circuit_summary.

required
inputs Tensor | None

The one raw sample to draw the embedding for, shape (n_features,) -- one value per qubit, except for the amplitude encoder. It goes through the layer's prepare_inputs as in forward. Default: sample_input, zeros where the layer accepts them (every embedding rotation drawn as RX(0.00)). Pass a real sample to see the feature map it produces.

None
decimals int

Digits shown for gate parameters. Default: 2.

2
Notes

Only the printed angles depend on inputs; the gates and the wiring do not, which is why circuit_summary does not take one.

entangling_capability

entangling_capability(layer: Module, n_samples: int = 1000, *, inputs: Inputs = 'random', generator: Generator | None = None) -> EntanglingCapabilityResult

Entangling capability of layer: mean Meyer–Wallach Q over sampled states (see the module docstring). Arguments as for expressibility.

Returns:

Type Description
EntanglingCapabilityResult

meyer_wallach

meyer_wallach(states: Tensor, n_qubits: int) -> torch.Tensor

Q of each state in states (shape (batch, 2^n_qubits)), as 2 · (1 − mean_k Tr ρ_k²). States are normalised first.

effective_dimension

effective_dimension(model: Module, data_sample: Tensor, *, n_data: int | None = None, gamma: float = 1.0, n_theta_samples: int = 20, init: InitName | InitFn = 'uniform', generator: Generator | None = None) -> EffectiveDimensionResult

Effective dimension (Abbas et al. 2021) of a model's quantum weights.

The Fisher matrix is computed at n_theta_samples random draws of the quantum weights from init, each over the rows of data_sample, and the draws are combined with effective_dimension_from_spectra. The weights are restored afterwards.

Parameters:

Name Type Description Default
model Module required
data_sample Tensor

Inputs the expectation over x is taken over.

required
n_data int | None

n in the definition: the size of the data set the model is meant for, which sets the resolution κ. Default: the number of rows in data_sample. Use the same value when comparing architectures. Must give κ ≥ e (n ≥ 74 at γ = 1); this is checked before any Fisher matrix is computed.

None
gamma float

γ ∈ (0, 1]. Default: 1.

1.0
n_theta_samples int

Parameter draws. Default: 20.

20
init InitName | InitFn

"uniform" over [0, 2π) (default, as in the paper), "restricted", "block_local", or a callable that fills the weight tensor in place. As in gradient_variance, only the rotation angles are drawn; another trainable tensor (input_scaling) keeps its values, but is measured: d counts every tensor.

'uniform'
generator Generator | None

Source of randomness for the draws; the global RNG is left untouched.

None

Returns:

Type Description
EffectiveDimensionResult

effective_dimension_from_spectra

effective_dimension_from_spectra(spectra: Sequence[Tensor] | Tensor, n_data: int, gamma: float = 1.0) -> float

d_{γ,n} from Fisher eigenvalues at several parameter draws.

Parameters:

Name Type Description Default
spectra Sequence[Tensor] | Tensor

(n_theta_samples, d) tensor, or a sequence of length-d eigenvalue tensors, one per parameter draw. Eigenvalues of the raw Fisher matrices; the trace normalisation is done here.

required
n_data int

n in the definition. Must be large enough that κ = γ n / (2π log n) ≥ e (n ≥ 74 at γ = 1); see the module docstring.

required
gamma float

γ ∈ (0, 1].

1.0

Returns:

Type Description
float

The effective dimension, ≥ 0; 0 if every spectrum is zero.

Notes

sqrt(det(I + κ F̂)) is evaluated as exp(½ Σ_i log(1 + κ λ̂_i)) and the average over draws with a log-sum-exp, so large κ does not overflow.

fisher_information_matrix

fisher_information_matrix(model: Module, data_sample: Tensor, *, parameters: Sequence[str] | None = None) -> FisherSpectrum

Fisher information matrix of the quantum weights at their current values.

Parameters:

Name Type Description Default
model Module

A hybrid classifier (one logit per sample → Bernoulli likelihood) or an encoding layer (its expectation values → unit-variance Gaussian likelihood, i.e. the Gauss-Newton matrix). See the module docstring.

required
data_sample Tensor

Inputs of shape (n_samples, n_features) the expectation over x is taken over. For a classifier these go through its classical encoder; for a layer they are fed to the circuit directly.

required
parameters Sequence[str] | None

Names of the quantum layer's trainable tensors to measure, e.g. ["weights"]. Default: all of them, in the TorchLayer's order. The matrix of a subset equals the corresponding block of the full one: F = E[Jᵀ J] blockwise.

None

Returns:

Type Description
FisherSpectrum

Raises:

Type Description
ValueError

If parameters names a tensor the layer does not have, one twice, or none.

Notes

Cost is one forward pass and k backward passes per row, k being the number of outputs (1 for a classifier), plus one (d, d) eigendecomposition. The k backward passes do not each re-run the circuit's gradient under backprop or adjoint: PennyLane computes a forward pass's Jacobian once and reuses it, so a row costs one circuit execution on default.qubit and two (forward and adjoint Jacobian) on lightning.qubit, whatever k is. Only parameter-shift repeats its 2d shifted circuits for each output.

Computing the Jacobians from one batched forward instead, with vectorised backward passes, was measured slower on every path, with the same number of executions, and it cannot vectorise through parameter-shift at all (#223, #286), so the per-row loop is kept.

The model is run in eval mode (its mode is restored afterwards), so dropout is off and a model built with noise_level > 0 is measured on its noiseless circuit, as gradient_variance is. To measure it under noise, call this inside hqnn_forge.noise.apply_depolarizing_noise.

fisher_information_spectrum

fisher_information_spectrum(model: Module, data_sample: Tensor) -> torch.Tensor

Eigenvalues of fisher_information_matrix, descending, float64.

The convenience form the issue asks for; use fisher_information_matrix when the matrix or the metadata is wanted too.

format_sweep

format_sweep(results: Sequence[GradientVarianceResult]) -> str

Plain-text table of a sweep, with the ratio to the previous row of the same init and layer count so exponential decay is readable at a glance.

gradient_variance

gradient_variance(target: Module, n_samples: int = 100, *, init: InitName | InitFn = 'uniform', input_scale: float = math.pi, cost_fn: CostFn | None = None, generator: Generator | None = None) -> GradientVarianceResult

Estimate the variance of the cost gradient over random weight draws.

Parameters:

Name Type Description Default
target Module

An encoding layer, or a hybrid classifier (its quantum_layer is used and inputs are fed to it directly, bypassing the classical encoder).

required
n_samples int

Random draws. At least 2. Default: 100.

100
init InitName | InitFn

"uniform" over [0, 2π) (the barren-plateau reference), "restricted", "block_local", or a callable that fills the weight tensor in place. Only the rotation angles are drawn: the tensor named weights (or the layer's only tensor). Any other trainable tensor -- DataReuploadingLayer's input_scaling -- keeps its current values for every draw, since an angle distribution means nothing for a scale factor, but its gradient is measured and counted in total_variance like the rest of the gradient vector.

'uniform'
input_scale float

Inputs are uniform in [-input_scale, input_scale], one per input feature (n_features for the amplitude encoder, else one per qubit), and go through the layer's prepare_inputs. π matches what the classifiers feed the circuit; 0 feeds zeros, which the amplitude encoder refuses (no state has zero norm).

pi
cost_fn CostFn | None

Maps the layer output of shape (1, n_qubits) to a scalar. Default: ⟨Z_0⟩.

None
generator Generator | None

Source of randomness for weights and inputs. The global RNG state is left untouched either way.

None

Returns:

Type Description
GradientVarianceResult

gradient_variance_sweep

gradient_variance_sweep(build: Callable[[int, int], Module], qubit_counts: Iterable[int], layer_counts: Iterable[int] = (2,), **kwargs: Any) -> list[GradientVarianceResult]

gradient_variance for every (n_qubits, n_layers) combination.

Parameters:

Name Type Description Default
build Callable[[int, int], Module]

build(n_qubits, n_layers) returns a fresh layer or model, e.g. lambda q, l: QuantumEncodingLayer(q, l, device_name="default.qubit", diff_method="backprop").

required
qubit_counts Iterable[int]

Grid to sweep.

required
layer_counts Iterable[int]

Grid to sweep.

required
**kwargs Any

Passed to gradient_variance (init, n_samples, ...). A generator is shared across the sweep.

{}

Returns:

Type Description
list[GradientVarianceResult]

One per combination, qubits in the outer loop.

effective_rank

effective_rank(F: Any) -> float

Effective rank of the feature covariance: exp(−Σ_a p_a ln p_a).

p_a are the eigenvalues of the covariance of the features divided by their sum. It shows features collapsing onto fewer directions: 1 when all variance lies along one direction, k when it is spread evenly over k. See "Effective rank" in the module docstring.

Parameters:

Name Type Description Default
F Any

Features of shape (n_samples, n_features), any real-valued array-like, with at least 2 samples. No labels are used.

required

Returns:

Type Description
float

In [1, min(n_features, n_samples − 1)]. 1.0 by convention when every row is the same and the covariance is zero.

Raises:

Type Description
TypeError

If F is complex or boolean.

ValueError

If F is not a matrix with at least 2 rows and one column, or holds a NaN or an infinity.

Examples:

The rows (±√3, 0), (0, ±1) have three quarters of their variance along the first feature: exp(−¾ ln ¾ − ¼ ln ¼) = 4 / 3^(3/4).

>>> from hqnn_forge.diagnostics import effective_rank
>>> root = 3**0.5
>>> round(effective_rank([[root, 0.0], [-root, 0.0], [0.0, 1.0], [0.0, -1.0]]), 6)
1.754765

fisher_discriminant_ratio

fisher_discriminant_ratio(F: Any, y: Any) -> float

Fisher discriminant ratio along the best linear direction: δᵀ S_w⁻¹ δ.

The largest value, over directions w, of the squared difference of the projected class means over the pooled within-class variance of the projection. δ = μ1 − μ0 and S_w is the pooled within-class covariance with denominator n_samples − 2; the result is the squared Mahalanobis distance between the class means. See "Fisher discriminant ratio" in the module docstring for the derivation and the conventions.

Parameters:

Name Type Description Default
F Any

As for pairwise_distances; at least 3 samples.

required
y Any

As for pairwise_distances; at least 3 samples.

required

Returns:

Type Description
float

In [0, ∞], dimensionless. 0 when the class means coincide; inf when the means differ along a direction in which neither class has any spread (features equal to the labels, or fewer samples than n_features + 2 in general position); NaN when every row is the same. It estimates the same population value at every sample size, with an upward bias of about n_features · M/(n0 n1) (larger for a small sample and for a rare class), and with unequal class covariances it weights the larger class more; see "Sample size" and "Class imbalance" in the module docstring.

Raises:

Type Description
(TypeError, ValueError)

As pairwise_distances; also ValueError for fewer than 3 samples.

Notes

A within-class spread or a mean difference along a direction counts as zero when it is below max(n_samples, n_features) · ε · |F − F[0]|, with ε the float64 machine epsilon and the Frobenius norm of the features after the first row is subtracted from every row: the size of the rounding error of the centred features. A repeated or linearly dependent feature therefore neither changes the ratio nor makes it infinite.

Examples:

Class 0 is {-1, 1} and class 1 {3, 5}: the means differ by 4 and the pooled variance is (2 + 2)/(4 − 2) = 2.

>>> from hqnn_forge.diagnostics import fisher_discriminant_ratio
>>> round(fisher_discriminant_ratio([[-1.0], [1.0], [3.0], [5.0]], [0, 0, 1, 1]), 12)
8.0

Unequal class sizes, class 1 {2, 4, 6}: the means differ by 4 again and the pooled variance is (2 + 8)/(5 − 2).

>>> round(fisher_discriminant_ratio([[-1.0], [1.0], [2.0], [4.0], [6.0]], [0, 0, 1, 1, 1]), 12)
4.8

linear_feature_kernel

linear_feature_kernel(F: Any) -> torch.Tensor

The linear kernel of the features, K = F Fᵀ.

K[i, j] = Σ_k F[i,k] F[j,k], the Gram matrix over the samples. Pass it to kernel_target_alignment for the alignment of the features with the labels, in [0, 1] for this kernel up to the rounding described in the Notes (see "Linear feature kernel" in the module docstring), next to that of the state kernel of the same inputs.

Parameters:

Name Type Description Default
F Any

Features of shape (n_samples, n_features), any real-valued array-like.

required

Returns:

Type Description
Tensor

Shape (n_samples, n_samples), float64, exactly symmetric, positive semi-definite to rounding.

Raises:

Type Description
TypeError

If F is complex or boolean.

ValueError

If F is not a matrix with at least one row and column, or holds a NaN or an infinity.

Notes

The alignment centres the kernel, which subtracts numbers of the size of |F[i]|² from one another. For features whose rows differ only in their last digits (½ + 1e-12 · g), or not at all, the result is that of the rounding error and can be negative; separation_measures avoids this by forming the kernel of F − F[0], which has the same centred alignment.

Examples:

>>> from hqnn_forge.diagnostics import linear_feature_kernel
>>> from hqnn_forge.kernels import kernel_target_alignment
>>> linear_feature_kernel([[1.0, 2.0], [3.0, 4.0]]).tolist()
[[5.0, 11.0], [11.0, 25.0]]
>>> labels = [0, 0, 1, 1]
>>> features = [[0.0], [0.0], [1.0], [1.0]]  # equal to the labels
>>> round(float(kernel_target_alignment(linear_feature_kernel(features), labels)), 12)
1.0

pairwise_distances

pairwise_distances(F: Any, y: Any) -> PairwiseDistances

The distribution of pairwise Euclidean feature distances, within and between classes.

D_ij = |F[i] − F[j]| for every pair i < j, split into the pairs with equal and with different labels; see "Pairwise distances" in the module docstring.

Parameters:

Name Type Description Default
F Any

Features of shape (n_samples, n_features), any real-valued array-like, one row per sample. Converted to float64.

required
y Any

Binary labels, {0, 1} or {-1, +1}, one per row, with both classes present.

required

Returns:

Type Description
PairwiseDistances

The two sets of distances, with their means and the ratio of the between-class to the within-class mean as properties.

Raises:

Type Description
TypeError

If F is complex or boolean.

ValueError

If F is not a matrix with at least one row and column or holds a NaN or an infinity, or y has another length than F has rows or does not hold both classes as 0/1 or -1/+1.

Notes

The full n_samples × n_samples distance matrix is formed (8 bytes per entry, 200 MB at 5000 samples), and next to it the indices of its upper triangle and the selected values, so the peak is about three times the matrix: 650 MB at 5000 samples.

Examples:

>>> from hqnn_forge.diagnostics import pairwise_distances
>>> result = pairwise_distances([[0.0, 0.0], [3.0, 4.0], [0.0, 1.0], [6.0, 8.0]], [0, 1, 0, 1])
>>> result.within.tolist()  # the pairs (0, 2) and (1, 3)
[1.0, 5.0]
>>> result.mean_within
3.0
>>> round(result.ratio, 3)
2.372

separation_measures

separation_measures(F: Any, y: Any, *, max_rows: int | None = None, seed: int = 0) -> SeparationMeasures

Every scalar separation measure of a feature matrix, in one record.

The means and the ratio of pairwise_distances, fisher_discriminant_ratio, effective_rank and the alignment of linear_feature_kernel with the labels (kernel_target_alignment). None of them involves a trained model, and none predicts the score of one.

Parameters:

Name Type Description Default
F Any

As for pairwise_distances; at least 3 samples.

required
y Any

As for pairwise_distances; at least 3 samples.

required
max_rows int | None

Optional maximum number of samples; must be at least 3 if specified. If the input exceeds this limit, a deterministic, class-stratified subsample of size max_rows is drawn using seed before forming quadratic-size matrices. If None (default), all samples are used.

None
seed int

Random seed for the stratified subsample draw when max_rows is used. Default is 0.

0

Returns:

Type Description
SeparationMeasures

Its to_dict() gives plain numbers for a log line.

Raises:

Type Description
(TypeError, ValueError)

As fisher_discriminant_ratio; or if max_rows is not None and less than 3.

Notes

The alignment is that of the Gram matrix of F − F[0] (the first row subtracted from every row). Centring removes a common offset, so this is the centred alignment of F Fᵀ, computed without the cancellation described in linear_feature_kernel.

When max_rows is given and fewer rows are evaluated than the full input matrix, the resulting numbers are stratified Monte Carlo estimates of the population separation measures on that subsample size, and n_samples reports the number of rows actually used.

Examples:

>>> from hqnn_forge.diagnostics import separation_measures
>>> measures = separation_measures([[-1.0], [1.0], [3.0], [5.0]], [0, 0, 1, 1])
>>> measures.mean_within_distance, measures.mean_between_distance
(2.0, 4.0)
>>> round(measures.fisher_ratio, 12), measures.effective_rank
(8.0, 1.0)
>>> round(measures.kernel_alignment, 12)  # 8² / (20 · 4)
0.8

stratified_subsample_indices

stratified_subsample_indices(y: Any, max_rows: int | None = None, *, seed: int = 0) -> np.ndarray

Deterministic, class-stratified subsample indices of length at most max_rows.

Preserves class proportions as closely as possible via proportional quota allocation with largest-remainder rounding. When max_rows is at least the number of classes present, every class with at least one sample is guaranteed representation (any class with zero allocated rows after proportional distribution receives one row transferred from the class with the largest allocation). The selected indices are returned in sorted order, keeping features and labels aligned.

Parameters:

Name Type Description Default
y Any

Labels of shape (n_samples,), array-like or tensor.

required
max_rows int | None

Maximum number of samples to select. If None or greater than or equal to the total number of samples, all sample indices np.arange(n_samples) are returned. Must be >= 1 if specified.

None
seed int

Seed for the pseudorandom generator. Default is 0.

0

Returns:

Type Description
ndarray

Sorted 1D array of selected integer indices, shape (n_samples,) if max_rows is None or max_rows >= n_samples, else (max_rows,).

Raises:

Type Description
ValueError

If max_rows is not None and max_rows < 1.

Expressibility

expressibility is exported from hqnn_forge.diagnostics, but shares its name with the submodule that defines it, which hides it from the listing above.

hqnn_forge.diagnostics.expressibility.expressibility

expressibility(layer: Module, n_pairs: int = 5000, n_bins: int = 75, *, inputs: Inputs = 'random', generator: Generator | None = None) -> ExpressibilityResult

Expressibility of layer: KL divergence of its fidelity distribution from the Haar one (see the module docstring).

Parameters:

Name Type Description Default
layer Module

An encoding layer. A hybrid classifier is refused; pass its quantum_layer.

required
n_pairs int

Pairs of independently drawn states. Default: 5000 (Sim et al.).

5000
n_bins int

Histogram bins on [0, 1]. Default: 75 (Sim et al.).

75
inputs Inputs

"random" (default): each state gets its own input from (−π, π). A 1-D tensor: that input for every state.

'random'
generator Generator | None

Source of every draw. Default: a generator seeded with 0, so repeated calls agree.

None

Returns:

Type Description
ExpressibilityResult

Sample input

The sample circuit_summary counts on, and draw_circuit draws by default; not exported, import it from hqnn_forge.diagnostics.circuit.

hqnn_forge.diagnostics.circuit.sample_input

sample_input(layer: CircuitLayer) -> torch.Tensor

One raw input for drawing and counting the layer's circuit.

Zeros where the layer's prepare_inputs accepts them: the angle-type embeddings run the same gates for every input, so only the printed angles depend on it.

Amplitude embedding has no state for the zero vector, and the gates of its Möttönen state preparation do depend on the input: PennyLane leaves out a block of rotations when all its angles are zero. A uniform or all-positive vector has no phases and so shows no RZ at all, and other sign patterns drop some RZ blocks. It gets [-1, 2, 3, …, n_features] instead: distinct magnitudes and one negative entry make every block non-zero, so the counts are the most any input needs: 2**n - 1 RY and 2**n - 1 RZ rotations when n_features == 2**n. With fewer features the zero padding leaves out the RY rotations that act only on padded amplitudes (for n = 3, 6 with 4 features, 4 with 2), since no input can make those non-zero.

Separation measures: definitions, ranges and edge cases

Which statement each measure supports for the Rydberg feature map is in The Rydberg feature model.

hqnn_forge.diagnostics.separation

hqnn_forge.diagnostics.separation

How well a fixed feature matrix separates inputs, and classes, before any head is trained on it.

A classification score does not say why a fixed feature map helps or fails. These measures are properties of the features themselves: of a matrix F with one row per sample and one column per feature, and binary labels y. Nothing here is specific to one feature map. They were written for the readout of RydbergFeatureMap, which is n_atoms numbers per sample and can lose most of what the state distinguishes; which statement each measure supports there, and how they relate to the state kernel (kernel_from_density_matrices), is in docs/rydberg-model.md.

Conventions
  • Rows are samples, columns are features. F has shape (M, d); F[i] is the feature vector of sample i. The features carry whatever unit the map gives them (excitation probabilities are dimensionless); distances are in that unit, the other measures are dimensionless.
  • Labels are {0, 1} or {−1, +1} with both classes present, as for kernel_target_alignment. Class 1 is the larger value; n0 and n1 are the class sizes, μ0 and μ1 the class means (vectors of length d). No measure changes when the two classes are swapped.
  • Pairs of samples are (i, j) with i < j, in the order (0,1), (0,2), …, (0,M−1), (1,2), …, (M−2,M−1).
  • Undefined is not a number. A measure that is 0/0 on the given data is returned as NaN and one whose defining supremum is unbounded as inf; neither is replaced by a finite value. The one exception is stated under "Effective rank".
Pairwise distances

The Euclidean distance of every pair::

D_ij = |F[i] − F[j]| = √( Σ_k (F[i,k] − F[j,k])² ),        i < j

split into the n0(n0−1)/2 + n1(n1−1)/2 pairs within a class (y_i = y_j) and the n0 · n1 pairs between the classes. The two sets of distances are the distribution; their means and the ratio ::

R = mean of D over the pairs between the classes
    ───────────────────────────────────────────
    mean of D over the pairs within a class

summarise it. Each mean is over pairs, so within a class the larger class has the larger weight (it has more pairs).

  • Range. D ≥ 0 and R ∈ (0, ∞]. R = ∞ when each class is a single point and the two points differ. R is undefined (NaN) when there is no pair within a class, or when every row is the same: all distances are then 0, and a between-class mean of 0 occurs in no other case.
  • No information. If the labels are independent of the features, a pair is within a class or between the classes whatever its distance, so both means estimate the same mean distance and R ≈ 1. R below 1 is possible: members of a class can be further apart than the classes.
  • Scale. The distances scale with the features; R does not.
Fisher discriminant ratio

Project the features on a direction w (a vector of length d). The projected class means differ by wᵀ δ with δ = μ1 − μ0, and the pooled variance of the projection within the classes is wᵀ S_w w with the pooled within-class covariance ::

S_w = ( Σ_{i: y_i = 0} (F[i] − μ0)(F[i] − μ0)ᵀ + Σ_{i: y_i = 1} (F[i] − μ1)(F[i] − μ1)ᵀ ) / (M − 2)

(M − 2 because two means were estimated). Fisher's ratio for that direction is the squared difference of the means over the variance, and the measure is its value along the best direction::

J(w) = (wᵀ δ)² / (wᵀ S_w w),        J = sup_w J(w)

By the Cauchy–Schwarz inequality in the inner product ⟨a, b⟩ = aᵀ S_w b, for an invertible S_w::

(wᵀ δ)² = ⟨w, S_w⁻¹ δ⟩² ≤ ⟨w, w⟩ · ⟨S_w⁻¹ δ, S_w⁻¹ δ⟩ = (wᵀ S_w w) · (δᵀ S_w⁻¹ δ)

with equality for w ∝ S_w⁻¹ δ, so ::

J = δᵀ S_w⁻¹ δ

the squared Mahalanobis distance between the class means. With the singular value decomposition F_w = U diag(s) Vᵀ of the features centred class by class (row i of F_w is F[i] − μ_{y_i}), S_w = V diag(s²) Vᵀ / (M − 2) and J = (M − 2) Σ_a (v_aᵀ δ)² / s_a², which is how it is computed.

  • Range. J ∈ [0, ∞], dimensionless, and unchanged by any invertible affine map of the features (F → F Aᵀ + b maps δ → A δ and S_w → A S_w Aᵀ). J = 0 when the class means coincide. As computed, the invariance holds for a well-conditioned A only: a direction whose spread and mean difference are below about max(M, d) · ε times the size of all features together (ε the float64 machine epsilon; one feature in a unit 14 orders of magnitude smaller than the others at M = 60) is taken for rounding error and left out, as the Notes of fisher_discriminant_ratio state.
  • Singular S_w. A direction v_a without within-class spread (s_a = 0) has J(v_a) = (v_aᵀ δ)²/0: if the means differ along it the classes are separated perfectly and J = ∞; if they do not, the direction says nothing and is left out of the sum. J is infinite in particular for features equal to the labels, and whenever M − 2 < d and the means differ outside the span of the within-class differences, which is the rule with fewer samples than features. If every row is the same, no direction has a ratio and J is NaN.
  • Reading. For two Gaussian classes with a common covariance, the best linear rule at equal priors errs with probability Φ(−√J / 2); for other distributions J gives no error rate.
  • Sample size. δ and S_w estimate the difference of the population means and the within-class covariance whatever M is, so J estimates one number, Δ² = δᵀ Σ⁻¹ δ of the populations, and neither grows nor shrinks in proportion to M. It does overestimate Δ² on a finite sample. n0 n1 J / M is Hotelling's T², and for two Gaussian classes with a common covariance T² (M − d − 1)/((M − 2) d) has a noncentral F distribution with d and M − d − 1 degrees of freedom and noncentrality n0 n1 Δ² / M, whose mean gives, for M > d + 3::

    E[J] = (M − 2)/(M − d − 3) · ( Δ² + d · M/(n0 n1) )

For classes that do not differ at all (Δ² = 0) J is therefore about d · M/(n0 n1) on average, not 0: 4 d / M for classes of equal size. Values of J from samples of different size are comparable only where they are large against that level. * Class imbalance. Swapping the classes changes nothing, and for a common covariance the population value Δ² does not depend on the class sizes. Two things do. The level above is d · M/(n0 n1), about d over the size of the smaller class when the other is much larger, so a rare class raises J without any class difference behind it. And S_w weights the covariance of each class by its size (by n_k − 1): if the classes have different covariances Σ0 and Σ1, S_w has the mean ((n0 − 1) Σ0 + (n1 − 1) Σ1)/(M − 2), in which the larger class dominates, so J changes with the class proportions for the same two populations. * Which normalisation. S_w is the pooled covariance: the within-class sums of squares and products over their M − 2 degrees of freedom, as in the two-sample T². Two other conventions carry the same name and give other numbers. With the sums themselves in the denominator (Fisher 1936, and S_W in most textbooks) the ratio is J/(M − 2): the best direction is the same, and the value falls as 1/M for the same populations. With the sum of the two class covariances, S_0 + S_1 (each over n_k − 1), the ratio is J/2 when the two classes have the same size, and otherwise the classes are weighted equally instead of by size, which in general also changes the best direction. Class 0 {−1, 1} and class 1 {2, 4, 6} (δ = 4, sums of squares 2 and 8): J = 16/(10/3) = 4.8 here, against 16/10 = 1.6 and 16/(2 + 4) = 2.67.

Effective rank

With the eigenvalues λ_1, …, λ_d ≥ 0 of the covariance of the features (the rows centred on their mean; its normalisation cancels)::

p_a = λ_a / Σ_b λ_b,        erank = exp( − Σ_a p_a ln p_a ),        0 ln 0 = 0

the exponential of the entropy of the normalised spectrum (Roy & Vetterli 2007, applied to the covariance). The eigenvalues are the squared singular values of the centred features, which is how they are computed.

  • Range. 1 ≤ erank ≤ min(d, M − 1): 1 when all variance lies along one direction, k when it is spread evenly over k directions. It is unchanged by a translation, a rotation and a common scale of the features, but not by rescaling one feature against another.
  • Constant features. If every row is the same the covariance is zero and has no normalised spectrum. The function then returns 1.0, the lower end of the range, so that a collapse onto a point does not read as a higher rank than a collapse onto a line. This is the one value in this module that is set by convention and not by the definition.
Linear feature kernel

::

K = F Fᵀ,        K[i, j] = Σ_k F[i,k] F[j,k]

an M × M Gram matrix over the samples, symmetric and positive semi-definite. Passed to kernel_target_alignment it gives the centred alignment of the readout with the labels, to compare with that of the state kernel. With H = 1 − 11ᵀ/M, the centred features F_c = H F and the centred labels ỹ = H y (y = ±1)::

A = ⟨H K H, ỹ ỹᵀ⟩ / ( |H K H| · |ỹ ỹᵀ| ) = |F_cᵀ ỹ|² / ( |F_c F_cᵀ| · |ỹ|² )

in the Frobenius inner product and norm.

  • Range. A ∈ [0, 1] for this kernel: the numerator is a squared norm. A = 1 exactly when every feature is an affine function of the label (F_c = ỹ vᵀ), as for features equal to the labels; A = 0 when no feature is correlated with the label. If every row is the same, H K H = 0, for which kernel_target_alignment returns 0.
  • As computed, both statements hold for separation_measures, which forms the kernel of F − F[0]. The H K H of linear_feature_kernel(F) itself is zero only to rounding when the rows are equal (or differ in their last digits) and are not zero, and kernel_target_alignment then returns the alignment of that rounding error: any value in [−1, 1], negative ones included.
  • A is unchanged by a translation and a common scale of the features.
Rounding, and features that have collapsed

Every measure is unchanged when the same vector is added to every row, and separation_measures, fisher_discriminant_ratio and effective_rank use that: they subtract the first row from all rows before anything is squared or averaged. Rows that are equal then give exact zeros, so the constant case above is detected exactly, and features of the form c + ε g with a small ε (excitation probabilities near ½ under strong dephasing) keep the relative precision of ε g. The distances are computed from differences, not from squared norms, for the same reason.

The ratios, the rank and the alignment do not depend on the scale of the features, so a collapse onto a point does not by itself move them towards a no-information value: features c + ε g have the ratios, rank and alignment of g for every ε > 0, down to where ε g is rounding error. What shows the collapse is the distances; the other measures change only if g does. Whether a small difference is resolvable is a question about the measurement (the number of shots), not one these measures answer.

The scale has limits at both ends of the float64 range, where a square overflows or underflows. The independence of scale holds for entries of F − F[0] between about 1e-70 and 1e70 in size (the alignment squares the kernel, a fourth power of the features; the other measures reach about 1e±150). Outside that range the results are wrong without an error: a Fisher ratio of 0 or NaN, an alignment of 0 or NaN.

Costs

The distances and the kernel hold M² numbers; the Fisher ratio and the rank need one singular value decomposition of an M × d matrix. All results are float64 on the CPU and carry no gradient.

References
  • Fisher (1936) "The use of multiple measurements in taxonomic problems", Annals of Eugenics 7, 179–188.
  • Roy & Vetterli (2007) "The effective rank: a measure of effective dimensionality", 15th European Signal Processing Conference, 606–610.
  • Cortes, Mohri & Rostamizadeh (2012) "Algorithms for learning kernels based on centered alignment", JMLR 13, 795–828.