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. |
n_qubits |
int
|
Wires the circuit acts on. |
n_trainable_params |
int
|
Trainable entries in the layer's weight tensors ( |
depth |
int
|
Longest path of gates through the logical circuit. |
n_gates |
int
|
Total gate count after decomposition to |
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
|
gate_counts
class-attribute
instance-attribute
¶
gate_counts: Mapping[str, int] = field(default_factory=dict)
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 |
standard_error |
float
|
Standard error of that mean. |
haar_reference |
float
|
|
values |
Tensor
|
|
ExpressibilityResult
dataclass
¶
Attributes:
| Name | Type | Description |
|---|---|---|
expressibility |
float
|
|
maximum |
float
|
|
fidelities |
Tensor
|
The |
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 |
effective_dimension |
float
|
|
normalized_effective_dimension |
float
|
|
mean_normalized_spectrum |
Tensor
|
Mean over draws of the trace-normalised eigenvalues, descending. |
mean_normalized_spectrum
class-attribute
instance-attribute
¶
mean_normalized_spectrum: Tensor = field(compare=False)
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
|
|
n_params |
int
|
Number of quantum parameters measured, |
n_data |
int
|
Rows of |
matrix |
Tensor
|
The |
eigenvalues |
Tensor
|
Its eigenvalues in descending order, |
parameter_slices |
dict[str, slice]
|
For each measured tensor, by TorchLayer argument name, the slice of
rows and columns of |
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 ( |
input_scale |
float
|
Inputs were drawn from |
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
|
|
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 |
Mapping[str, Tensor]
|
Per-entry variance per trainable tensor, keyed by the TorchLayer
argument name ( |
per_tensor
class-attribute
instance-attribute
¶
per_tensor: Mapping[str, Tensor] = field(default_factory=dict, compare=False)
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
|
|
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 |
|
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
|
|
fisher_ratio |
float
|
Fisher discriminant ratio along the best linear direction,
|
effective_rank |
float
|
Exponential of the entropy of the normalised eigenvalues of the
feature covariance, in |
kernel_alignment |
float
|
Centred alignment of the linear feature kernel |
The two ratios can be ``inf`` (perfect separation) or NaN (undefined, as |
|
|
for identical rows), which strict JSON has no value for. |
|
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 |
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 |
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 |
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 |
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 |
required |
inputs
|
Tensor | None
|
The one raw sample to draw the embedding for, shape
|
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
|
As for |
required |
data_sample
|
Tensor
|
Inputs the expectation over |
required |
n_data
|
int | None
|
|
None
|
gamma
|
float
|
|
1.0
|
n_theta_samples
|
int
|
Parameter draws. Default: 20. |
20
|
init
|
InitName | InitFn
|
|
'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
|
|
required |
n_data
|
int
|
|
required |
gamma
|
float
|
|
1.0
|
Returns:
| Type | Description |
|---|---|
float
|
The effective dimension, |
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 |
required |
parameters
|
Sequence[str] | None
|
Names of the quantum layer's trainable tensors to measure, e.g.
|
None
|
Returns:
| Type | Description |
|---|---|
FisherSpectrum
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
n_samples
|
int
|
Random draws. At least 2. Default: 100. |
100
|
init
|
InitName | InitFn
|
|
'uniform'
|
input_scale
|
float
|
Inputs are uniform in |
pi
|
cost_fn
|
CostFn | None
|
Maps the layer output of shape |
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]
|
|
required |
qubit_counts
|
Iterable[int]
|
Grid to sweep. |
required |
layer_counts
|
Iterable[int]
|
Grid to sweep. |
required |
**kwargs
|
Any
|
Passed to |
{}
|
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 |
required |
Returns:
| Type | Description |
|---|---|
float
|
In |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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 |
required |
y
|
Any
|
As for |
required |
Returns:
| Type | Description |
|---|---|
float
|
In |
Raises:
| Type | Description |
|---|---|
(TypeError, ValueError)
|
As |
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 |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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 |
required |
y
|
Any
|
Binary labels, |
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 |
ValueError
|
If |
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 |
required |
y
|
Any
|
As for |
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 |
None
|
seed
|
int
|
Random seed for the stratified subsample draw when |
0
|
Returns:
| Type | Description |
|---|---|
SeparationMeasures
|
Its |
Raises:
| Type | Description |
|---|---|
(TypeError, ValueError)
|
As |
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 |
required |
max_rows
|
int | None
|
Maximum number of samples to select. If |
None
|
seed
|
int
|
Seed for the pseudorandom generator. Default is 0. |
0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Sorted 1D array of selected integer indices, shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
required |
n_pairs
|
int
|
Pairs of independently drawn states. Default: 5000 (Sim et al.). |
5000
|
n_bins
|
int
|
Histogram bins on |
75
|
inputs
|
Inputs
|
|
'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.
Fhas shape(M, d);F[i]is the feature vector of samplei. 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 forkernel_target_alignment. Class 1 is the larger value;n0andn1are the class sizes,μ0andμ1the class means (vectors of lengthd). No measure changes when the two classes are swapped. - Pairs of samples are
(i, j)withi < 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/0on the given data is returned as NaN and one whose defining supremum is unbounded asinf; 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 ≥ 0andR ∈ (0, ∞].R = ∞when each class is a single point and the two points differ.Ris 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.Rbelow 1 is possible: members of a class can be further apart than the classes. - Scale. The distances scale with the features;
Rdoes 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ᵀ + bmapsδ → A δandS_w → A S_w Aᵀ).J = 0when the class means coincide. As computed, the invariance holds for a well-conditionedAonly: a direction whose spread and mean difference are below aboutmax(M, d) · εtimes the size of all features together (ε thefloat64machine epsilon; one feature in a unit 14 orders of magnitude smaller than the others atM = 60) is taken for rounding error and left out, as the Notes offisher_discriminant_ratiostate. - Singular
S_w. A directionv_awithout within-class spread (s_a = 0) hasJ(v_a) = (v_aᵀ δ)²/0: if the means differ along it the classes are separated perfectly andJ = ∞; if they do not, the direction says nothing and is left out of the sum.Jis infinite in particular for features equal to the labels, and wheneverM − 2 < dand 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 andJis NaN. - Reading. For two Gaussian classes with a common covariance, the best
linear rule at equal priors errs with probability
Φ(−√J / 2); for other distributionsJgives no error rate. -
Sample size.
δandS_westimate the difference of the population means and the within-class covariance whateverMis, soJestimates one number,Δ² = δᵀ Σ⁻¹ δof the populations, and neither grows nor shrinks in proportion toM. It does overestimateΔ²on a finite sample.n0 n1 J / Mis Hotelling'sT², and for two Gaussian classes with a common covarianceT² (M − d − 1)/((M − 2) d)has a noncentral F distribution withdandM − d − 1degrees of freedom and noncentralityn0 n1 Δ² / M, whose mean gives, forM > 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,kwhen it is spread evenly overkdirections. 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 = 1exactly when every feature is an affine function of the label (F_c = ỹ vᵀ), as for features equal to the labels;A = 0when no feature is correlated with the label. If every row is the same,H K H = 0, for whichkernel_target_alignmentreturns 0. - As computed, both statements hold for
separation_measures, which forms the kernel ofF − F[0]. TheH K Hoflinear_feature_kernel(F)itself is zero only to rounding when the rows are equal (or differ in their last digits) and are not zero, andkernel_target_alignmentthen returns the alignment of that rounding error: any value in[−1, 1], negative ones included. Ais 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.