Skip to content

API Reference

Complete programmatic interface for the Quantum Encoding Atlas. All public classes and functions are documented below with their signatures, parameters, and return types.


Encodings

All encodings inherit from BaseEncoding and share a unified interface.

Base Class

BaseEncoding

BaseEncoding(n_features: int, **kwargs: Any)

Bases: ABC

Abstract base class for all quantum data encodings.

All encoding implementations must inherit from this class and implement the required abstract methods.

Parameters

n_features : int Number of classical features to encode. **kwargs : Any Additional encoding-specific parameters.

Examples

from encoding_atlas import AngleEncoding encoding = AngleEncoding(n_features=4, rotation='Y') encoding.n_qubits 4

Notes

This class uses slots for memory efficiency. Subclasses should also define slots listing their own instance attributes to maintain this optimization. If a subclass does not define slots, it will have a dict and the memory benefit is partially lost.

Source code in src/encoding_atlas/core/base.py
def __init__(self, n_features: int, **kwargs: Any) -> None:
    if not isinstance(n_features, int) or n_features < 1:
        raise ValueError(f"n_features must be a positive integer, got {n_features}")

    self._n_features = n_features
    self._config = kwargs
    self._properties: EncodingProperties | None = None
    # Thread lock for safe lazy initialization of properties
    # Uses double-checked locking pattern for efficiency
    self._properties_lock: threading.Lock = threading.Lock()

    # Optional LRU cache for circuit construction (disabled by default).
    # Enabled via :meth:`enable_cache`. ``None`` means caching is off.
    self._circuit_cache: OrderedDict[tuple[bytes, str], CircuitType] | None = None
    self._circuit_cache_maxsize: int = 0
    self._circuit_cache_lock: threading.Lock = threading.Lock()

n_qubits abstractmethod property

n_qubits: int

Number of qubits required for this encoding.

depth abstractmethod property

depth: int

Circuit depth of the encoding.

properties property

properties: EncodingProperties

Compute and return encoding properties.

This property uses thread-safe lazy initialization with double-checked locking to ensure safe access in multi-threaded environments while minimizing lock contention for subsequent accesses.

Returns

EncodingProperties Computed properties of this encoding.

Notes

Thread Safety: The first access in a multi-threaded context will acquire a lock to ensure only one thread computes the properties. Subsequent accesses bypass the lock entirely for optimal performance.

The double-checked locking pattern
  1. First check without lock (fast path for initialized case)
  2. Acquire lock
  3. Second check inside lock (handles race condition)
  4. Compute if still None

config property

config: dict[str, Any]

Encoding configuration parameters.

get_circuit

get_circuit(x: ArrayLike, backend: BackendType = 'pennylane') -> CircuitType

Generate quantum circuit for the given data.

This is a template method that validates input and delegates to the encoding-specific :meth:_get_circuit_from_validated. Subclasses should normally override :meth:_get_circuit_from_validated rather than this method.

If circuit caching has been enabled via :meth:enable_cache, the result for repeated calls with byte-identical inputs is returned from cache.

Parameters

x : ArrayLike Input data of shape (n_features,) or (1, n_features) for a single sample. If 2D with a single row, the row is unwrapped before dispatch. backend : {'pennylane', 'qiskit', 'cirq'}, default='pennylane' Target quantum computing framework.

Returns

CircuitType Circuit in the specified backend's format.

Raises

ValueError If input shape doesn't match n_features, contains NaN/Inf values, or backend is not recognized.

Source code in src/encoding_atlas/core/base.py
def get_circuit(
    self,
    x: ArrayLike,
    backend: BackendType = "pennylane",
) -> CircuitType:
    """Generate quantum circuit for the given data.

    This is a template method that validates input and delegates to the
    encoding-specific :meth:`_get_circuit_from_validated`. Subclasses
    should normally override :meth:`_get_circuit_from_validated` rather
    than this method.

    If circuit caching has been enabled via :meth:`enable_cache`, the
    result for repeated calls with byte-identical inputs is returned
    from cache.

    Parameters
    ----------
    x : ArrayLike
        Input data of shape (n_features,) or (1, n_features) for a single
        sample. If 2D with a single row, the row is unwrapped before
        dispatch.
    backend : {'pennylane', 'qiskit', 'cirq'}, default='pennylane'
        Target quantum computing framework.

    Returns
    -------
    CircuitType
        Circuit in the specified backend's format.

    Raises
    ------
    ValueError
        If input shape doesn't match n_features, contains NaN/Inf values,
        or backend is not recognized.
    """
    x_validated = self._validate_input(x)
    if x_validated.ndim == 2:
        x_validated = x_validated[0]
    return self._cached_dispatch(x_validated, backend)

Properties

EncodingProperties dataclass

EncodingProperties(n_qubits: int, depth: int, gate_count: int, single_qubit_gates: int, two_qubit_gates: int, parameter_count: int, is_entangling: bool, simulability: Literal['simulable', 'conditionally_simulable', 'not_simulable'], expressibility: Optional[float] = None, entanglement_capability: Optional[float] = None, trainability_estimate: Optional[float] = None, noise_resilience_estimate: Optional[float] = None, notes: str = '')

Properties computed for a quantum encoding.

Attributes

n_qubits : int Number of qubits required. depth : int Circuit depth (number of time steps). gate_count : int Total number of gates. single_qubit_gates : int Number of single-qubit gates. two_qubit_gates : int Number of two-qubit gates. parameter_count : int Number of data-dependent parameters. is_entangling : bool Whether the encoding creates entanglement. simulability : str Classical simulability status.

__post_init__

__post_init__() -> None

Validate properties after initialization.

Source code in src/encoding_atlas/core/properties.py
def __post_init__(self) -> None:
    """Validate properties after initialization."""
    if self.n_qubits < 1:
        raise ValueError("n_qubits must be at least 1")
    if self.depth < 0:
        raise ValueError("depth cannot be negative")
    if self.gate_count < 0:
        raise ValueError("gate_count cannot be negative")
    if self.single_qubit_gates + self.two_qubit_gates != self.gate_count:
        raise ValueError(
            "single_qubit_gates + two_qubit_gates must equal gate_count"
        )

to_dict

to_dict() -> dict

Convert properties to dictionary.

Source code in src/encoding_atlas/core/properties.py
def to_dict(self) -> dict:
    """Convert properties to dictionary."""
    return {
        "n_qubits": self.n_qubits,
        "depth": self.depth,
        "gate_count": self.gate_count,
        "single_qubit_gates": self.single_qubit_gates,
        "two_qubit_gates": self.two_qubit_gates,
        "parameter_count": self.parameter_count,
        "is_entangling": self.is_entangling,
        "simulability": self.simulability,
        "expressibility": self.expressibility,
        "entanglement_capability": self.entanglement_capability,
        "trainability_estimate": self.trainability_estimate,
        "noise_resilience_estimate": self.noise_resilience_estimate,
        "notes": self.notes,
    }

Encoding Classes

Angle Encoding

AngleEncoding

AngleEncoding(n_features: int, rotation: Literal['X', 'Y', 'Z'] = 'Y', reps: int = 1)

Bases: BaseEncoding

Angle encoding using single-qubit rotation gates.

AngleEncoding maps classical features directly to rotation angles of single-qubit gates. Each feature is encoded on a dedicated qubit using a rotation gate around a specified axis (X, Y, or Z).

This encoding creates product states (no entanglement), making it: - Classically simulable (efficient classical simulation possible) - Free from barren plateaus (good trainability) - Hardware-efficient (only single-qubit gates)

The circuit structure for each repetition is:

|0⟩ ─ Rₐ(x₀) ─
|0⟩ ─ Rₐ(x₁) ─
|0⟩ ─ Rₐ(x₂) ─
...

where Rₐ is the rotation gate for axis a ∈ {X, Y, Z}.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. Each feature requires one qubit, so this also determines the number of qubits in the circuit. rotation : {"X", "Y", "Z"}, default="Y" The axis of rotation to use for encoding:

- "X": Uses RX gates, encoding features in the YZ plane
- "Y": Uses RY gates (default), encoding features in the XZ plane
- "Z": Uses RZ gates, encoding features as phases

The Y rotation is commonly used as it creates real-valued amplitudes
when starting from |0⟩, and can represent any point on the Bloch
sphere's XZ great circle.

reps : int, default=1 Number of times to repeat the encoding layer. Higher values increase the effective rotation angle by a factor of reps. Must be at least 1.

With reps > 1, the effective rotation for feature xᵢ becomes:
Rₐ(xᵢ)^reps = Rₐ(reps · xᵢ)

Attributes

rotation : str The rotation axis used ("X", "Y", or "Z"). reps : int Number of encoding layer repetitions. n_features : int Number of classical features (inherited from BaseEncoding). n_qubits : int Number of qubits, equal to n_features.

Examples

Create a basic angle encoding with default Y rotation:

from encoding_atlas import AngleEncoding import numpy as np enc = AngleEncoding(n_features=4) enc.n_qubits 4 enc.rotation 'Y'

Generate a PennyLane circuit:

x = np.array([0.1, 0.2, 0.3, 0.4]) circuit = enc.get_circuit(x, backend='pennylane') callable(circuit) True

Use X rotation for different encoding behavior:

enc_x = AngleEncoding(n_features=4, rotation='X') qiskit_circuit = enc_x.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 4

Use multiple repetitions to increase rotation range:

enc_reps = AngleEncoding(n_features=2, reps=3) enc_reps.depth 3 enc_reps.properties.gate_count 6

Access encoding properties:

enc = AngleEncoding(n_features=4) props = enc.properties props.is_entangling False props.simulability 'simulable'

References

.. [1] Schuld, M., & Petruccione, F. (2018). "Supervised Learning with Quantum Computers." Springer. .. [2] LaRose, R., & Coyle, B. (2020). "Robust data encodings for quantum classifiers." Physical Review A, 102(3), 032420.

See Also

AmplitudeEncoding : Encodes features in state amplitudes (logarithmic qubits). IQPEncoding : Adds entanglement via ZZ interactions. DataReuploading : Re-uploads data multiple times with trainable layers. PauliFeatureMap : Configurable Pauli rotations with entanglement.

Notes

Expressivity: Angle encoding creates product states, which limits its expressivity compared to entangling encodings. However, this simplicity is advantageous for trainability and classical simulation.

Rotation Axis Choice:

  • RY is most common as it creates real amplitudes from |0⟩
  • RX creates complex amplitudes with imaginary components
  • RZ only adds phases to |0⟩ (no population change from |0⟩)

Scaling Considerations: The rotation gates have period 2π (for RX, RY) or 4π (for full Bloch sphere coverage). Input features should be scaled appropriately to utilize the full encoding range.

Comparison with Other Encodings:

+-------------------+----------+--------+------------+---------------+ | Encoding | Qubits | Depth | Entangling | Simulability | +===================+==========+========+============+===============+ | AngleEncoding | n | reps | No | Simulable | +-------------------+----------+--------+------------+---------------+ | BasisEncoding | n | 1 | No | Simulable | +-------------------+----------+--------+------------+---------------+ | AmplitudeEncoding | log₂(n) | O(2^n) | Yes | Not simulable | +-------------------+----------+--------+------------+---------------+ | IQPEncoding | n | O(n²) | Yes | Not simulable | +-------------------+----------+--------+------------+---------------+ | ZZFeatureMap | n | O(n²) | Yes | Not simulable | +-------------------+----------+--------+------------+---------------+

Initialize the angle encoding.

Parameters

n_features : int Number of classical features to encode. rotation : {"X", "Y", "Z"}, default="Y" Rotation axis for the encoding gates. reps : int, default=1 Number of encoding layer repetitions.

Raises

ValueError If rotation is not one of "X", "Y", "Z". ValueError If reps is less than 1. ValueError If n_features is less than 1 (raised by parent class).

Amplitude Encoding

AmplitudeEncoding

AmplitudeEncoding(n_features: int, normalize: bool = True)

Bases: BaseEncoding

Amplitude encoding maps classical features to quantum state amplitudes.

AmplitudeEncoding provides exponential data compression by encoding n classical features into the amplitudes of a log₂(n)-qubit quantum state. This is one of the most powerful quantum data encodings in terms of compression, but comes with significant circuit depth requirements.

The encoding prepares a quantum state where each computational basis state |i⟩ has amplitude proportional to the i-th feature value:

|0⟩ → (1/‖x‖) Σᵢ xᵢ|i⟩

The circuit structure requires state preparation gates:

|0⟩⊗ⁿ ─── [State Preparation Unitary U] ─── |ψ(x)⟩

where U|0⟩⊗ⁿ = |ψ(x)⟩ and the state preparation uses O(2^n) gates for n qubits in the general case.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. The number of qubits is ⌈log₂(n_features)⌉, with zero-padding if n_features is not a power of 2. normalize : bool, default=True Whether to normalize input data to create a valid quantum state. If True (default), input vectors are divided by their L2 norm. If False, the user must ensure inputs are already normalized.

.. warning::
    Setting normalize=False with unnormalized data may cause
    undefined behavior or errors in quantum backends.

Attributes

normalize : bool Whether automatic normalization is enabled. n_features : int Number of classical features (inherited from BaseEncoding). n_qubits : int Number of qubits: max(1, ⌈log₂(n_features)⌉). depth : int Circuit depth: O(2^n_qubits) for general state preparation.

Examples

Create a basic amplitude encoding for 4 features (requires 2 qubits):

from encoding_atlas import AmplitudeEncoding import numpy as np enc = AmplitudeEncoding(n_features=4) enc.n_qubits 2 enc.n_features 4

Encode a feature vector (automatically normalized):

x = np.array([1.0, 2.0, 3.0, 4.0]) circuit = enc.get_circuit(x, backend='pennylane') callable(circuit) True

Single feature requires 1 qubit (padded to 2 amplitudes):

enc_single = AmplitudeEncoding(n_features=1) enc_single.n_qubits 1

Generate circuits for different backends:

qiskit_circuit = enc.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 2

cirq_circuit = enc.get_circuit(x, backend='cirq') len(cirq_circuit.all_qubits()) 2

Non-power-of-two features are zero-padded:

enc_5 = AmplitudeEncoding(n_features=5) enc_5.n_qubits # ceil(log2(5)) = 3, so 8 amplitudes 3

Batch processing multiple samples:

X = np.random.randn(10, 4) circuits = enc.get_circuits(X, backend='pennylane') len(circuits) 10

Access encoding properties:

props = enc.properties props.is_entangling True props.simulability 'not_simulable'

References

.. [1] Schuld, M., & Petruccione, F. (2018). "Supervised Learning with Quantum Computers." Springer. .. [2] Mottonen, M., et al. (2004). "Transformation of quantum states using uniformly controlled rotations."

See Also

AngleEncoding : Linear scaling (n features = n qubits), no entanglement. BasisEncoding : Encodes discrete data in computational basis states. IQPEncoding : Adds entanglement with quadratic feature interactions. DataReuploading : Re-encodes data multiple times with trainable layers.

Notes

Compression vs. Circuit Depth Tradeoff: While amplitude encoding provides exponential compression (n features in log₂(n) qubits), it requires O(2^n) gates for state preparation. This means the encoding circuit depth scales linearly with the number of features, not logarithmically.

Qubit Calculation Edge Case: For n_features=1, the formula log₂(1) = 0 would yield 0 qubits. The implementation enforces a minimum of 1 qubit, padding the single feature to a 2-element state vector [x, 0] → |0⟩ after normalization.

Normalization Behavior: - If normalize=True: Input vector x is divided by ‖x‖ before encoding - If normalize=False: Input is used as-is (must be pre-normalized) - Zero-norm vectors will cause errors (division by zero)

Backend-Specific Implementation: - PennyLane: Uses qml.AmplitudeEmbedding for optimal performance - Qiskit: Uses QuantumCircuit.initialize() with automatic decomposition - Cirq: Uses a custom unitary gate constructed via Gram-Schmidt

Qubit Ordering Conventions: Quantum computing frameworks differ in how they map statevector indices to qubit labels. This library adopts the MSB (most significant bit) convention used by PennyLane and Cirq, where qubit 0 is the leftmost (most significant) bit.

  • PennyLane — MSB natively. qml.state() returns a statevector where index i encodes the computational basis state with qubit 0 as the most significant bit. No conversion needed.

  • Cirq — MSB natively. Simulator.simulate() returns a statevector with LineQubit(0) as the most significant bit. No conversion needed.

  • Qiskit — LSB natively. Statevector(circuit).data returns amplitudes where qubit 0 is the least significant bit. _to_qiskit() compensates for this by bit-reversing the amplitude indices before calling initialize(), and the analysis module's _reverse_qubit_order() reverses the statevector when reading results back. The net effect is transparent MSB semantics for the caller.

Example: For 4 features (2 qubits), amplitude vector [a₀, a₁, a₂, a₃] maps to quantum state: a₀|00⟩ + a₁|01⟩ + a₂|10⟩ + a₃|11⟩

In the MSB convention used by all backends after conversion:

  • Index 0 (binary 00) → qubit 0 = 0, qubit 1 = 0
  • Index 1 (binary 01) → qubit 0 = 0, qubit 1 = 1
  • Index 2 (binary 10) → qubit 0 = 1, qubit 1 = 0
  • Index 3 (binary 11) → qubit 0 = 1, qubit 1 = 1

This consistent ordering ensures that circuits generated by different backends produce equivalent quantum states, enabling reliable cross- backend testing and backend-agnostic algorithm development.

Hardware Considerations: Due to the deep circuit depth, amplitude encoding may accumulate significant errors on NISQ hardware. Consider using shallower encodings (AngleEncoding, IQPEncoding) for near-term applications.

Initialize the amplitude encoding.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. normalize : bool, default=True Whether to automatically normalize input vectors.

Raises

ValueError If n_features is not a positive integer (raised by parent class). TypeError If normalize is not a boolean.

Examples

enc = AmplitudeEncoding(n_features=8) enc.n_qubits 3

enc = AmplitudeEncoding(n_features=4, normalize=False) enc.normalize False

Basis Encoding

BasisEncoding

BasisEncoding(n_features: int, threshold: float = 0.5)

Bases: BaseEncoding

Basis encoding for binary/discrete data into computational basis states.

BasisEncoding provides the simplest quantum data encoding by mapping binary classical data directly to computational basis states. Each qubit represents one classical bit, with X gates applied to encode 1s.

This encoding creates pure computational basis states (no superposition), making it:

  • Trivially simulable: Classical computers can efficiently track basis states
  • Hardware efficient: Only single-qubit X gates, no entanglement
  • Deterministic: Measurement always yields the encoded bit string
  • Minimal depth: Circuit depth of 1 regardless of feature count

The circuit structure is:

|0⟩ ─── X^{x₀} ─── |x₀⟩
|0⟩ ─── X^{x₁} ─── |x₁⟩
|0⟩ ─── X^{x₂} ─── |x₂⟩
...

where X^{xᵢ} means "apply X gate if xᵢ = 1, otherwise identity".

Parameters

n_features : int Number of binary features to encode. Must be a positive integer. Each feature requires one qubit, so this also determines the number of qubits in the circuit. threshold : float, default=0.5 Binarization threshold for continuous inputs. Values strictly greater than this threshold are mapped to 1 (X gate applied), while values less than or equal to the threshold are mapped to 0 (no gate).

Common threshold choices:
- 0.5: Standard for data normalized to [0, 1]
- 0.0: Treat negative values as 0, positive as 1
- Custom: Match your data's natural decision boundary

Attributes

n_features : int Number of binary features (inherited from BaseEncoding). n_qubits : int Number of qubits, equal to n_features. depth : int Circuit depth, always 1 for basis encoding. threshold : float Binarization threshold. Values > threshold become 1.

Examples

Create a basic basis encoding for 4 binary features:

from encoding_atlas import BasisEncoding import numpy as np enc = BasisEncoding(n_features=4) enc.n_qubits 4 enc.depth 1 enc.threshold 0.5

Encode binary data directly:

x_binary = np.array([1, 0, 1, 1]) circuit = enc.get_circuit(x_binary, backend='pennylane') callable(circuit) True

Continuous data is automatically binarized at threshold 0.5:

x_continuous = np.array([0.8, 0.2, 0.6, 0.9]) # -> [1, 0, 1, 1] circuit = enc.get_circuit(x_continuous, backend='pennylane')

Custom threshold for data with different scales:

enc_custom = BasisEncoding(n_features=4, threshold=0.0) enc_custom.threshold 0.0

Now: positive values -> 1, non-positive values -> 0

x_signed = np.array([-0.5, 0.5, -0.1, 0.1]) # -> [0, 1, 0, 1] circuit = enc_custom.get_circuit(x_signed, backend='pennylane')

Generate circuits for different backends:

x = np.array([1, 0, 1, 0]) qiskit_circuit = enc.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 4

cirq_circuit = enc.get_circuit(x, backend='cirq') len(cirq_circuit.all_qubits()) 4

Batch processing multiple samples:

X = np.array([[1, 0, 0, 1], [0, 1, 1, 0], [1, 1, 1, 1]]) circuits = enc.get_circuits(X, backend='pennylane') len(circuits) 3

Access encoding properties:

props = enc.properties props.is_entangling False props.simulability 'simulable' props.depth 1

Analyze actual resource usage for specific data:

enc = BasisEncoding(n_features=4) enc.properties.gate_count # Theoretical worst-case 4 enc.actual_gate_count([1, 0, 0, 0]) # Actual for sparse data 1 enc.binarize([0.8, 0.2, 0.6, 0.4]) # Inspect binarization array([1, 0, 1, 0])

References

.. [1] Nielsen, M. A., & Chuang, I. L. (2010). "Quantum Computation and Quantum Information." Cambridge University Press. .. [2] Schuld, M., & Petruccione, F. (2018). "Supervised Learning with Quantum Computers." Springer.

See Also

AngleEncoding : Encodes continuous features as rotation angles. AmplitudeEncoding : Encodes features in state amplitudes (logarithmic qubits). IQPEncoding : Adds entanglement via ZZ interactions for richer expressivity.

Notes

Binarization Behavior: This implementation automatically binarizes continuous inputs using the configured threshold (default 0.5):

  • x > threshold → 1 (X gate applied)
  • x ≤ threshold → 0 (no gate applied)

For pre-binarized data (0s and 1s), this threshold has no effect. The threshold can be customized at construction time to match your data's natural decision boundary.

Comparison with Other Encodings:

+------------------+----------+-------+------------+---------------+ | Encoding | Qubits | Depth | Entangling | Data Type | +==================+==========+=======+============+===============+ | BasisEncoding | n | 1 | No | Binary | +------------------+----------+-------+------------+---------------+ | AngleEncoding | n | 1 | No | Continuous | +------------------+----------+-------+------------+---------------+ | AmplitudeEncoding| log₂(n) | O(2ⁿ) | Yes | Continuous | +------------------+----------+-------+------------+---------------+ | IQPEncoding | n | O(n) | Yes | Continuous | +------------------+----------+-------+------------+---------------+

When to Use Basis Encoding:

  • Your data is naturally binary or categorical
  • You need a simple baseline encoding for comparison
  • Circuit depth must be minimal (NISQ hardware)
  • You're implementing algorithms that work with marked basis states (Grover's search, QAOA for combinatorial problems)

When NOT to Use Basis Encoding:

  • Your data is continuous and pattern-rich (use AngleEncoding or IQP)
  • You need amplitude-based information processing
  • Your algorithm requires superposition or entanglement from the encoding

Cirq Backend Note:

Cirq's circuit.all_qubits() only returns qubits that have operations. For sparse inputs (many zeros), this may return fewer qubits than expected. Always use encoding.n_qubits to determine the actual qubit count required for your circuit, not circuit.all_qubits().

Understanding Gate Counts (Important):

BasisEncoding provides two ways to analyze gate counts:

  1. Theoretical (worst-case): properties.gate_count returns n_features, assuming all features binarize to 1. This is computed once at construction time and cached. Use this for:

  2. Capacity planning and resource allocation

  3. Worst-case guarantees
  4. Comparing encoding algorithms theoretically
  5. Documentation and specifications

  6. Actual (data-dependent): actual_gate_count(x) returns the exact count of X gates for specific input data. This equals the number of 1s after binarization. Use this for:

  7. Accurate hardware resource estimation

  8. Benchmarking with real datasets
  9. Cost analysis for sparse data
  10. Debugging unexpected behavior

Example showing the difference:

enc = BasisEncoding(n_features=100) enc.properties.gate_count # Theoretical: always 100 100 sparse_data = np.zeros(100) sparse_data[0] = 1 # Only one feature is "on" enc.actual_gate_count(sparse_data) # Actual: only 1 gate 1

For sparse binary data, the actual gate count can be significantly lower than the theoretical maximum, which matters for:

  • Hardware execution time (fewer gates = faster circuits)
  • Error rates (fewer gates = less decoherence)
  • Classical simulation cost (sparser circuits = easier to simulate)

Initialize the basis encoding.

Parameters

n_features : int Number of binary features to encode. Each feature requires one qubit, so this determines both the input dimension and the number of qubits in the circuit. threshold : float, default=0.5 Binarization threshold for continuous inputs. Values strictly greater than this threshold are mapped to 1 (X gate applied), while values less than or equal are mapped to 0 (no gate).

Must be a finite real number. Common choices:
- 0.5 for data normalized to [0, 1]
- 0.0 for signed data (negative -> 0, positive -> 1)

Raises

ValueError If n_features is not a positive integer (raised by parent class). TypeError If threshold is not a numeric type (int or float). ValueError If threshold is NaN or infinite.

Examples

enc = BasisEncoding(n_features=4) enc.n_qubits 4 enc.depth 1 enc.threshold 0.5

enc = BasisEncoding(n_features=8) enc.properties.gate_count # Worst case: all X gates 8

enc = BasisEncoding(n_features=4, threshold=0.0) enc.threshold 0.0

IQP Encoding

IQPEncoding

IQPEncoding(n_features: int, reps: int = _DEFAULT_REPS, entanglement: Literal['full', 'linear', 'circular'] = _DEFAULT_ENTANGLEMENT)

Bases: BaseEncoding

IQP encoding using Hadamard gates and ZZ interactions.

IQPEncoding implements Instantaneous Quantum Polynomial circuits for quantum machine learning. It creates highly entangled quantum states by combining Hadamard gates, single-qubit Z rotations, and two-qubit ZZ interactions parameterized by classical input features.

This encoding is notable for its provable classical hardness: simulating IQP circuits is believed to be intractable for classical computers under standard complexity-theoretic assumptions, making it a candidate for demonstrating quantum advantage in machine learning.

The circuit structure for each repetition is:

|0⟩ ─ H ─ RZ(2x₀) ─╭─────╮─╭─────╮─
|0⟩ ─ H ─ RZ(2x₁) ─│ ZZ  │─│     │─
|0⟩ ─ H ─ RZ(2x₂) ─╰─────╯─│ ZZ  │─
...                        ╰─────╯

where ZZ(xᵢxⱼ) interactions are applied according to the entanglement topology (full, linear, or circular connectivity).

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. Each feature requires one qubit, so this also determines the number of qubits in the circuit. reps : int, default=2 Number of times to repeat the encoding layers. Higher values create deeper circuits with stronger entanglement but may face trainability issues. Must be at least 1. entanglement : {"full", "linear", "circular"}, default="full" Topology of ZZ interactions between qubits:

- "full": All-to-all connectivity. Every pair (i, j) with i < j has a
  ZZ interaction. Creates n(n-1)/2 entangling gates per layer.
  Maximum expressivity but highest gate count.
- "linear": Nearest-neighbor connectivity. Only pairs (i, i+1) have
  ZZ interactions. Creates n-1 entangling gates per layer.
  Hardware-friendly for linear qubit topologies.
- "circular": Nearest-neighbor with periodic boundary. Like linear,
  but adds a ZZ interaction between qubits n-1 and 0.
  Creates n entangling gates per layer.

Attributes

reps : int Number of encoding layer repetitions. entanglement : str The entanglement topology ("full", "linear", or "circular"). n_features : int Number of classical features (inherited from BaseEncoding). n_qubits : int Number of qubits, equal to n_features.

Examples

Create a basic IQP encoding with default settings:

from encoding_atlas import IQPEncoding import numpy as np enc = IQPEncoding(n_features=4) enc.n_qubits 4 enc.entanglement 'full'

Generate a PennyLane circuit:

x = np.array([0.1, 0.2, 0.3, 0.4]) circuit = enc.get_circuit(x, backend='pennylane') callable(circuit) True

Use linear entanglement for hardware-friendly circuits:

enc_linear = IQPEncoding(n_features=4, entanglement='linear') props = enc_linear.properties props.two_qubit_gates < enc.properties.two_qubit_gates True

Generate Qiskit and Cirq circuits:

qiskit_circuit = enc.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 4 cirq_circuit = enc.get_circuit(x, backend='cirq') len(cirq_circuit.all_qubits()) 4

Access encoding properties:

props = enc.properties props.is_entangling True props.simulability 'not_simulable'

References

.. [1] Havlíček, V., et al. (2019). "Supervised learning with quantum-enhanced feature spaces." Nature, 567(7747), 209-212. .. [2] Bremner, M. J., et al. (2016). "Average-case complexity versus approximate simulation of commuting quantum computations." Physical Review Letters, 117(8), 080501. .. [3] McClean, J. R., et al. (2018). "Barren plateaus in quantum neural network training landscapes." Nature Communications, 9, 4812.

See Also

ZZFeatureMap : Similar encoding with different phase conventions. AngleEncoding : Simpler encoding without entanglement. PauliFeatureMap : Generalized Pauli-based feature map. DataReuploading : Re-uploads data with trainable intermediate layers.

Warnings

Full Entanglement with Many Features: When using entanglement='full' with more than 12 features, a warning is issued because the circuit complexity may exceed practical limits for NISQ devices. The number of two-qubit gates scales as O(n²), which can lead to:

  • Excessive circuit depth and gate errors
  • Hardware connectivity constraints (all-to-all required)
  • Potential trainability issues (barren plateaus)

Consider using entanglement='linear' or entanglement='circular' for large feature counts.

Notes

Classical Hardness: The output distribution of IQP circuits is provably hard to sample from classically under the assumption that the polynomial hierarchy does not collapse [2]_. This provides theoretical motivation for potential quantum advantage in machine learning.

Feature Interactions: The ZZ(xᵢxⱼ) gates encode products of features, allowing the quantum kernel to capture pairwise correlations. This is analogous to polynomial feature expansion in classical machine learning, but executed in superposition.

Entanglement Topology Trade-offs:

+------------+------------------+---------------+------------------------+ | Topology | Gate Scaling | Connectivity | Best For | +============+==================+===============+========================+ | full | O(n²) | All-to-all | Max expressivity | +------------+------------------+---------------+------------------------+ | linear | O(n) | Nearest-only | NISQ devices | +------------+------------------+---------------+------------------------+ | circular | O(n) | Ring | Periodic problems | +------------+------------------+---------------+------------------------+

Trainability: Deeper circuits (higher reps) with full entanglement may exhibit barren plateaus, where gradients become exponentially small [3]_. Mitigation strategies include:

  • Using fewer repetitions (reps=1 or 2)
  • Using sparser entanglement (linear or circular)
  • Layer-wise training approaches
  • Careful parameter initialization

Gate Decomposition: The ZZ interaction is implemented using the standard decomposition: ZZ(θ) = CNOT · RZ(2θ) · CNOT. This requires 2 CNOT gates per interaction, which dominates the circuit's two-qubit gate count.

Initialize the IQP encoding.

Parameters

n_features : int Number of classical features to encode. reps : int, default=2 Number of encoding layer repetitions. entanglement : {"full", "linear", "circular"}, default="full" Topology of ZZ interactions between qubits.

Raises

ValueError If reps is less than 1. ValueError If entanglement is not one of "full", "linear", "circular". ValueError If n_features is less than 1 (raised by parent class).

Warns

UserWarning If using full entanglement with more than 12 features, as the circuit complexity may exceed practical NISQ device limits.

ZZ Feature Map

ZZFeatureMap

ZZFeatureMap(n_features: int, reps: int = _DEFAULT_REPS, entanglement: Literal['full', 'linear', 'circular'] = _DEFAULT_ENTANGLEMENT)

Bases: BaseEncoding

ZZ Feature Map encoding following Qiskit conventions.

ZZFeatureMap implements the second-order Pauli-Z expansion feature map commonly used in quantum kernel methods and QSVM. It creates entangled quantum states by applying Hadamard gates, phase rotations on single qubits, and ZZ entangling interactions between pairs of qubits.

This encoding follows the Qiskit convention for ZZFeatureMap, using the phase formula 2(π - xᵢ)(π - xⱼ) for two-qubit interactions, which creates a different kernel geometry compared to the standard IQP encoding.

The circuit structure for each repetition is:

|0⟩ ─ H ─ P(2x₀) ─╭──────────────╮─╭──────────────╮─
|0⟩ ─ H ─ P(2x₁) ─│ ZZ(φ₀₁)     │─│              │─
|0⟩ ─ H ─ P(2x₂) ─╰──────────────╯─│ ZZ(φ₁₂)     │─
...                                ╰──────────────╯

where φᵢⱼ = 2(π - xᵢ)(π - xⱼ) and ZZ interactions are applied according to the specified entanglement topology.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. Each feature requires one qubit, so this also determines the number of qubits in the circuit. reps : int, default=2 Number of times to repeat the encoding layers. Higher values create deeper circuits that may capture more complex feature relationships. Must be at least 1. entanglement : {"full", "linear", "circular"}, default="full" Topology of ZZ interactions between qubits:

- "full": All-to-all connectivity. Every pair (i, j) with i < j has a
  ZZ interaction. Creates n(n-1)/2 entangling gates per layer.
- "linear": Nearest-neighbor connectivity. Only pairs (i, i+1) have
  ZZ interactions. Creates n-1 entangling gates per layer.
- "circular": Nearest-neighbor with periodic boundary. Like linear,
  but adds a ZZ interaction between qubits n-1 and 0 when n > 2.

Attributes

reps : int Number of encoding layer repetitions. entanglement : str The entanglement topology ("full", "linear", or "circular"). n_features : int Number of classical features (inherited from BaseEncoding). n_qubits : int Number of qubits, equal to n_features.

Use Cases

Quantum Kernel Methods (QSVM): ZZFeatureMap is the canonical choice for quantum kernel estimation in supervised learning. The (π-x) phase convention creates kernels that can separate data classes that are not linearly separable in the original feature space.

Quantum Neural Networks: Use as a feature embedding layer before variational ansatz circuits. The entanglement creates correlations that parameterized layers can learn to exploit.

Benchmarking: Standard circuit for comparing quantum machine learning approaches across different platforms and simulators.

Research: Studying the effect of different phase conventions on kernel geometry and quantum advantage in machine learning tasks.

Limitations

Scalability: Full entanglement creates O(n²) ZZ pairs, leading to deep circuits for large feature counts. For n > 10 features, consider using entanglement='linear' or entanglement='circular'.

Hardware Connectivity: Full entanglement requires all-to-all qubit connectivity, which is not available on most near-term quantum hardware. SWAP gates will be inserted during transpilation, increasing circuit depth.

Noise Sensitivity: Deep circuits with many two-qubit gates are more susceptible to decoherence and gate errors on NISQ devices.

Classical Simulation: Unlike product-state encodings (e.g., AngleEncoding), ZZFeatureMap creates entangled states that cannot be efficiently simulated classically for large qubit counts.

Resource Analysis

The circuit resources scale as follows per repetition:

Full entanglement (entanglement='full'): - ZZ pairs: n(n-1)/2 - CNOT gates: n(n-1) per rep - Example: n=10 → 45 pairs, 90 CNOTs per rep

Linear entanglement (entanglement='linear'): - ZZ pairs: n-1 - CNOT gates: 2(n-1) per rep - Example: n=10 → 9 pairs, 18 CNOTs per rep

Circular entanglement (entanglement='circular'): - ZZ pairs: n (for n > 2) - CNOT gates: 2n per rep - Example: n=10 → 10 pairs, 20 CNOTs per rep

Use gate_count_breakdown() for detailed gate counts, or get_entanglement_pairs() to inspect the connectivity.

Entanglement Topology Trade-offs

+------------+------------------+----------------+------------------+ | Topology | Connectivity | ZZ Pairs | Hardware | +============+==================+================+==================+ | full | All-to-all | n(n-1)/2 | Requires SWAP | | | (maximum) | (quadratic) | gates on most HW | +------------+------------------+----------------+------------------+ | linear | Nearest-neighbor | n-1 | Native on most | | | (chain) | (linear) | superconducting | +------------+------------------+----------------+------------------+ | circular | Nearest-neighbor | n (for n > 2) | Native on ring | | | + wrap-around | (linear) | topologies | +------------+------------------+----------------+------------------+

Recommendation: Use entanglement='linear' for NISQ hardware experiments to minimize SWAP overhead. Use entanglement='full' for simulation studies or when maximum expressivity is needed.

Examples

Create a basic ZZ Feature Map with default settings:

from encoding_atlas import ZZFeatureMap import numpy as np enc = ZZFeatureMap(n_features=4) enc.n_qubits 4 enc.entanglement 'full'

Generate a PennyLane circuit:

x = np.array([0.5, 1.0, 1.5, 2.0]) circuit = enc.get_circuit(x, backend='pennylane') callable(circuit) True

Use linear entanglement for hardware-friendly circuits:

enc_linear = ZZFeatureMap(n_features=4, entanglement='linear') enc_linear.properties.two_qubit_gates 6

Generate Qiskit circuit (compatible with Qiskit's ZZFeatureMap):

qiskit_circuit = enc.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 4

Batch processing with parallel execution:

X = np.random.randn(100, 4) circuits = enc.get_circuits(X, backend='pennylane', parallel=True) len(circuits) 100

Inspect entanglement connectivity:

enc.get_entanglement_pairs() [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]

Get detailed gate count breakdown:

breakdown = enc.gate_count_breakdown() breakdown['cnot'] 24 breakdown['total'] 52

Access encoding properties:

props = enc.properties props.is_entangling True props.simulability 'not_simulable'

References

.. [1] Havlíček, V., et al. (2019). "Supervised learning with quantum-enhanced feature spaces." Nature, 567(7747), 209-212. .. [2] Qiskit Development Team. "ZZFeatureMap documentation."

See Also

IQPEncoding : Similar IQP-style encoding with direct xᵢxⱼ interactions. PauliFeatureMap : Generalized Pauli-based feature map. AngleEncoding : Simpler encoding without entanglement. DataReuploading : Re-uploads data with trainable intermediate layers.

Notes

Phase Convention: The (π - x) convention in ZZ interactions means:

  • Features at x = π have zero interaction strength
  • Features at x = 0 or x = 2π have maximum interaction
  • This creates a kernel with different geometry than standard IQP

Comparison with IQPEncoding:

  • ZZFeatureMap uses P(2x) phase gates; IQP uses RZ(2x)
  • ZZFeatureMap uses (π-xᵢ)(π-xⱼ) for ZZ; IQP uses xᵢxⱼ
  • Both create classically hard-to-simulate circuits
  • Choice depends on data distribution and kernel requirements

Qiskit Compatibility: This implementation follows Qiskit conventions and produces equivalent circuits to qiskit.circuit.library.ZZFeatureMap.

Thread Safety: This class is thread-safe for circuit generation. Multiple threads can safely call get_circuit() or get_circuits() concurrently on the same encoding instance. The encoding object is not modified during circuit generation, and input validation creates defensive copies to prevent data races.

Circular Entanglement Edge Case: For n_features=2, circular entanglement produces the same connectivity as linear (only one pair). The wrap-around edge (n-1, 0) is only added when n > 2 to avoid duplicate pairs.

Initialize the ZZ Feature Map encoding.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. Each feature requires one qubit, so this also determines the number of qubits in the circuit. reps : int, default=2 Number of encoding layer repetitions. Must be at least 1. entanglement : {"full", "linear", "circular"}, default="full" Topology of ZZ interactions between qubits.

Raises

ValueError If reps is not a positive integer. ValueError If entanglement is not one of "full", "linear", "circular". ValueError If n_features is less than 1 (raised by parent class).

Examples

enc = ZZFeatureMap(n_features=4) enc.n_qubits 4 enc.reps 2 enc.entanglement 'full'

enc = ZZFeatureMap(n_features=8, reps=3, entanglement='linear') len(enc.get_entanglement_pairs()) 7

Pauli Feature Map

PauliFeatureMap

PauliFeatureMap(n_features: int, reps: int = _DEFAULT_REPS, paulis: list[str] | None = None, entanglement: Literal['full', 'linear', 'circular'] = _DEFAULT_ENTANGLEMENT)

Bases: BaseEncoding

Configurable Pauli feature map encoding for quantum machine learning.

PauliFeatureMap encodes classical data into quantum states using a configurable set of Pauli rotation gates. This provides flexibility to tailor the encoding to specific problem structures.

The circuit structure for each repetition consists of: 1. A Hadamard layer creating uniform superposition 2. Pauli rotation layers as specified by the paulis parameter

For single-qubit Paulis (e.g., "Z", "Y", "X"), rotations R_P(2φ(x_i)) are applied where φ is a feature mapping function.

For two-qubit Paulis (e.g., "ZZ", "YY", "XX"), entangling rotations exp(-i φ(x_i, x_j) P⊗P) are applied using the specified entanglement pattern.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. reps : int, default=2 Number of repetitions (layers) of the feature map circuit. Higher values increase expressibility but also circuit depth. Must be at least 1. paulis : list[str] or None, default=None List of Pauli strings specifying which rotations to apply. Valid single-qubit terms: "X", "Y", "Z" Valid two-qubit terms: "XX", "YY", "ZZ", "XY", "XZ", "YZ" If None, defaults to ["Z", "ZZ"] (equivalent to ZZFeatureMap). entanglement : {"full", "linear", "circular"}, default="full" Pattern for two-qubit interactions: - "full": All pairs (i, j) where i < j - "linear": Consecutive pairs (i, i+1) - "circular": Linear plus (n-1, 0)

Attributes

reps : int Number of circuit repetitions. paulis : list[str] List of Pauli terms used in the encoding. entanglement : str Entanglement pattern for two-qubit gates.

Examples

Create a basic Pauli feature map with default settings:

from encoding_atlas import PauliFeatureMap import numpy as np enc = PauliFeatureMap(n_features=4) enc.n_qubits 4 enc.paulis ['Z', 'ZZ']

Create a custom feature map with Y and YY rotations:

enc = PauliFeatureMap(n_features=4, paulis=["Y", "YY"], reps=3) x = np.array([0.1, 0.2, 0.3, 0.4]) circuit = enc.get_circuit(x, backend='pennylane')

Use linear entanglement for hardware-friendly circuits:

enc_full = PauliFeatureMap(n_features=8, entanglement="full") enc_linear = PauliFeatureMap(n_features=8, entanglement="linear") enc_linear.properties.two_qubit_gates < enc_full.properties.two_qubit_gates True

Batch processing with parallel execution:

X = np.random.randn(100, 4) circuits = enc.get_circuits(X, backend='pennylane', parallel=True) len(circuits) 100

Inspect entanglement connectivity:

enc = PauliFeatureMap(n_features=4) enc.get_entanglement_pairs() [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]

Get detailed gate count breakdown:

breakdown = enc.gate_count_breakdown() breakdown['cnot'] 24 breakdown['total'] 52

References

.. [1] Havlíček, V., et al. (2019). "Supervised learning with quantum- enhanced feature spaces." Nature, 567(7747), 209-212.

See Also

ZZFeatureMap : Specialized version with fixed Z and ZZ paulis. IQPEncoding : Similar encoding with IQP-style structure. AngleEncoding : Simpler encoding without entanglement.

Warnings

Full Entanglement with Many Features: When using entanglement='full' with more than 12 features, a warning is issued because the circuit complexity may exceed practical limits for NISQ devices. The number of two-qubit gates scales as O(n²), which can lead to:

  • Excessive circuit depth and gate errors
  • Hardware connectivity constraints (all-to-all required)
  • Potential trainability issues (barren plateaus)

Consider using entanglement='linear' or entanglement='circular' for large feature counts.

Notes

Feature Interactions: The two-qubit Pauli gates encode products of features using the (π - xᵢ)(π - xⱼ) convention, allowing the quantum kernel to capture pairwise correlations.

Entanglement Topology Trade-offs:

+------------+------------------+---------------+------------------------+ | Topology | Gate Scaling | Connectivity | Best For | +============+==================+===============+========================+ | full | O(n²) | All-to-all | Max expressivity | +------------+------------------+---------------+------------------------+ | linear | O(n) | Nearest-only | NISQ devices | +------------+------------------+---------------+------------------------+ | circular | O(n) | Ring | Periodic problems | +------------+------------------+---------------+------------------------+

Pauli Term Trade-offs:

+-------+-------------------+------------------------------------------+ | Pauli | Gate Overhead | Notes | +=======+===================+==========================================+ | Z | Minimal | Native on most hardware | +-------+-------------------+------------------------------------------+ | X | Low (1 H gate) | Requires basis change | +-------+-------------------+------------------------------------------+ | Y | Medium (H + S) | Requires two basis change gates | +-------+-------------------+------------------------------------------+ | ZZ | 2 CNOTs | Standard for IQP/ZZFeatureMap | +-------+-------------------+------------------------------------------+ | XX | 2 CNOTs + 4 H | Basis change adds 4 single-qubit gates | +-------+-------------------+------------------------------------------+ | YY | 2 CNOTs + 8 gates | Most overhead due to Y-basis changes | +-------+-------------------+------------------------------------------+

Trainability: Deeper circuits (higher reps) with full entanglement and many Pauli terms may exhibit barren plateaus, where gradients become exponentially small. Mitigation strategies include:

  • Using fewer repetitions (reps=1 or 2)
  • Using sparser entanglement (linear or circular)
  • Selecting fewer Pauli terms
  • Layer-wise training approaches

Thread Safety: This class is thread-safe for circuit generation. Multiple threads can safely call get_circuit() or get_circuits() concurrently on the same encoding instance.

Initialize the Pauli feature map encoding.

Parameters

n_features : int Number of classical features to encode. reps : int, default=2 Number of repetitions of the feature map circuit. paulis : list[str] or None, default=None List of Pauli strings. Defaults to ["Z", "ZZ"]. entanglement : {"full", "linear", "circular"}, default="full" Entanglement pattern for two-qubit gates.

Raises

ValueError If any parameter is invalid. TypeError If paulis is not a list or None.

Warns

UserWarning If using full entanglement with more than 12 features, as the circuit complexity may exceed practical NISQ device limits.

Data Re-uploading

DataReuploading

DataReuploading(n_features: int, n_layers: int = _DEFAULT_N_LAYERS, n_qubits: int | None = None)

Bases: BaseEncoding

Data re-uploading quantum feature map with high expressivity.

DataReuploading implements a quantum encoding strategy where classical data is encoded multiple times throughout the circuit, interleaved with entangling layers. This multi-layer approach creates quantum states with rich Fourier spectra, enabling expressive quantum kernels and serving as a foundation for variational quantum classifiers.

The key insight is that re-uploading data at multiple circuit depths increases the number of accessible Fourier frequencies, similar to how depth increases expressivity in classical neural networks.

The circuit structure repeats L times:

|0⟩ ─ RY(x₀) ─╭────╮─ RY(x₀) ─╭────╮─ ... ─
|0⟩ ─ RY(x₁) ─│CNOT│─ RY(x₁) ─│CNOT│─ ... ─
|0⟩ ─ RY(x₂) ─│    │─ RY(x₂) ─│    │─ ... ─
...           ╰────╯          ╰────╯

Each layer consists of data encoding (RY rotations) followed by entanglement (CNOT ladder), repeated n_layers times.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. If n_features > n_qubits, features are cyclically mapped to qubits. n_layers : int, default=3 Number of re-uploading layers. Higher values increase the number of accessible Fourier frequencies but also circuit depth. Must be at least 1. More layers provide richer feature representations. n_qubits : int, optional Number of qubits in the circuit. If not specified, defaults to n_features. Can be set lower than n_features to use fewer qubits with cyclic feature mapping.

Attributes

n_layers : int Number of re-uploading layers. n_features : int Number of classical features (inherited from BaseEncoding). n_qubits : int Number of qubits in the circuit.

Examples

Create a basic data re-uploading encoding:

from encoding_atlas import DataReuploading import numpy as np enc = DataReuploading(n_features=4) enc.n_qubits 4 enc.n_layers 3

Generate a PennyLane circuit:

x = np.array([0.1, 0.2, 0.3, 0.4]) circuit = enc.get_circuit(x, backend='pennylane') callable(circuit) True

Use fewer qubits than features (cyclic mapping):

enc_compact = DataReuploading(n_features=8, n_qubits=4) enc_compact.n_qubits 4

Increase layers for higher expressivity:

enc_deep = DataReuploading(n_features=4, n_layers=10) enc_deep.depth 30

Generate circuits for different backends:

qiskit_circuit = enc.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 4

Access encoding properties:

props = enc.properties props.is_entangling True

References

.. [1] Pérez-Salinas, A., et al. (2020). "Data re-uploading for a universal quantum classifier." Quantum, 4, 226. .. [2] Schuld, M., et al. (2021). "Effect of data encoding on the expressive power of variational quantum machine learning models."

See Also

AngleEncoding : Single-layer encoding without re-uploading. IQPEncoding : Entangling encoding without data re-uploading. HardwareEfficientEncoding : Similar structure optimized for hardware. PauliFeatureMap : Alternative multi-layer encoding approach.

Notes

Feature Map vs. Universal Approximation: This class implements a fixed feature map that provides high expressivity through repeated data encoding. For full universal function approximation as described in [1]_, trainable parameters must be added between data uploads. Use this encoding as the feature map layer in a variational circuit, then add trainable rotations to achieve universal approximation capability.

Fourier Expressivity: With L encoding layers, the quantum state can represent functions with Fourier frequencies up to ±L. More layers enable the representation of higher-frequency components. The specific Fourier coefficients are determined by the fixed circuit structure.

Trainability Considerations: While data re-uploading helps with expressivity, very deep circuits may still face trainability challenges. The interleaved structure generally provides better gradient flow than purely entangling circuits.

Feature Mapping: When n_features > n_qubits, feature xᵢ is encoded on qubit (i mod n_qubits). This allows compact representations but may create interference between features mapped to the same qubit.

Gate Set Choice: This implementation uses RY rotations for data encoding and a CNOT ladder for entanglement. The original single-qubit formulation in [1]_ uses general SU(2) (U3) rotations with three parameters per gate, and the multi-qubit extension uses CZ gates with varying entanglement topologies. The RY + CNOT variant used here is a common simplification adopted by many QML frameworks; it is less expressive per gate but sufficient for most feature map applications. The Fourier frequency analysis from [2]_ applies regardless of the specific rotation axis.

Comparison with Classical Deep Learning: Data re-uploading is analogous to residual connections in deep neural networks, where information about the input is preserved and refined through layers.

Initialize the data re-uploading encoding.

Parameters

n_features : int Number of classical features to encode. n_layers : int, default=3 Number of re-uploading layers for expressivity. n_qubits : int, optional Number of qubits. Defaults to n_features if not specified.

Raises

ValueError If n_layers is less than 1. ValueError If n_qubits is less than 1 (when specified). ValueError If n_features is less than 1 (raised by parent class).

Warns

UserWarning If n_layers exceeds the deep circuit threshold (default 10), as very deep circuits may face trainability challenges due to barren plateaus.

Examples

enc = DataReuploading(n_features=4) enc.n_qubits 4 enc.n_layers 3

enc = DataReuploading(n_features=4, n_layers=5, n_qubits=2) enc.n_qubits 2 enc.n_layers 5

Hardware Efficient Encoding

HardwareEfficientEncoding

HardwareEfficientEncoding(n_features: int, reps: int = 2, rotation: Literal['X', 'Y', 'Z'] = 'Y', entanglement: Literal['linear', 'circular', 'full'] = 'linear')

Bases: BaseEncoding

Hardware-efficient encoding optimized for NISQ devices.

HardwareEfficientEncoding implements a quantum data encoding that uses only native gates and respects physical qubit connectivity constraints. This makes it particularly suitable for near-term quantum devices where gate errors and limited connectivity are significant concerns.

The encoding uses simple alternating layers of single-qubit rotations (parameterized by input features) and entangling CNOT gates following a linear or circular topology.

The circuit structure for each repetition is:

|0⟩ ─ Rₐ(x₀) ─╭────╮─ Rₐ(x₀) ─╭────╮─
|0⟩ ─ Rₐ(x₁) ─│CNOT│─ Rₐ(x₁) ─│CNOT│─
|0⟩ ─ Rₐ(x₂) ─│    │─ Rₐ(x₂) ─│    │─
...           ╰────╯          ╰────╯

where Rₐ is the chosen rotation gate (RX, RY, or RZ) and CNOT gates connect neighboring qubits according to the entanglement topology.

Parameters

n_features : int Number of classical features to encode. Must be a positive integer. Each feature requires one qubit, so this also determines the number of qubits in the circuit. reps : int, default=2 Number of times to repeat the encoding layers. Higher values increase circuit expressivity but also depth and accumulated errors. Must be at least 1. rotation : {"X", "Y", "Z"}, default="Y" Axis of rotation for single-qubit gates:

- "X": Uses RX gates, commonly native on superconducting qubits
- "Y": Uses RY gates (default), creates real-valued amplitudes
- "Z": Uses RZ gates, often "virtual" gates with zero error

The optimal choice depends on the target hardware's native gate set.

entanglement : {"linear", "circular", "full"}, default="linear" Topology of CNOT entangling gates:

- "linear": Nearest-neighbor connectivity. CNOT gates connect pairs
  (0,1), (1,2), ..., (n-2, n-1). Creates n-1 entangling gates per layer.
  Matches linear qubit arrays common in superconducting devices.
- "circular": Linear connectivity plus periodic boundary. Adds a CNOT
  between qubits n-1 and 0. Creates n entangling gates per layer.
  Matches circular/ring qubit topologies.
- "full": All-to-all connectivity. CNOT gates connect every pair of
  qubits. Creates n(n-1)/2 entangling gates per layer (O(n²) scaling).
  Matches ion trap devices (IonQ, Quantinuum) with global connectivity.

Attributes

reps : int Number of encoding layer repetitions. rotation : str The rotation axis ("X", "Y", or "Z"). entanglement : str The entanglement topology ("linear", "circular", or "full"). n_features : int Number of classical features (inherited from BaseEncoding). n_qubits : int Number of qubits, equal to n_features.

Examples

Create a basic hardware-efficient encoding:

from encoding_atlas import HardwareEfficientEncoding import numpy as np enc = HardwareEfficientEncoding(n_features=4) enc.n_qubits 4 enc.rotation 'Y'

Generate a PennyLane circuit:

x = np.array([0.1, 0.2, 0.3, 0.4]) circuit = enc.get_circuit(x, backend='pennylane') callable(circuit) True

Use RZ rotations (often "virtual" gates with zero error):

enc_rz = HardwareEfficientEncoding(n_features=4, rotation='Z') enc_rz.rotation 'Z'

Use circular entanglement for ring topologies:

enc_circ = HardwareEfficientEncoding(n_features=4, entanglement='circular') enc_circ.properties.two_qubit_gates 8

Generate circuits for different backends:

qiskit_circuit = enc.get_circuit(x, backend='qiskit') qiskit_circuit.num_qubits 4 cirq_circuit = enc.get_circuit(x, backend='cirq') len(cirq_circuit.all_qubits()) 4

Access encoding properties:

props = enc.properties props.is_entangling True props.trainability_estimate 0.8

References

.. [1] Kandala, A., et al. (2017). "Hardware-efficient variational quantum eigensolver for small molecules and quantum magnets." Nature. .. [2] Cerezo, M., et al. (2021). "Variational quantum algorithms." Nature Reviews Physics.

See Also

AngleEncoding : Simpler encoding without entanglement. DataReuploading : Re-uploads data for universal approximation. IQPEncoding : Higher expressivity with ZZ interactions. ZZFeatureMap : Qiskit-style feature map encoding.

Notes

Hardware Native Gates: Most superconducting quantum computers support RX, RY, RZ rotations and CNOT (or CZ) as native gates. Ion trap devices typically support different gate sets. Choose the rotation type that matches your target hardware.

Virtual RZ Gates: On many superconducting devices, RZ gates are implemented virtually (by updating the phase tracking) with essentially zero error. Using rotation='Z' can significantly reduce circuit errors.

Depth Considerations: Each repetition adds one rotation layer and one entanglement layer. For n qubits with linear entanglement: - Total single-qubit gates: reps × n - Total two-qubit gates: reps × (n-1) - Circuit depth: approximately 2 × reps

Expressivity vs. Trainability: Hardware-efficient encodings typically have good trainability (gradients don't vanish as quickly as random circuits) but may have lower expressivity than IQP-style encodings. This trade-off often favors hardware-efficient designs on NISQ devices.

Initialize the hardware-efficient encoding.

Parameters

n_features : int Number of classical features to encode. reps : int, default=2 Number of encoding layer repetitions. rotation : {"X", "Y", "Z"}, default="Y" Rotation axis for single-qubit gates. entanglement : {"linear", "circular", "full"}, default="linear" Topology for entangling CNOT gates.

Raises

ValueError If reps is less than 1. ValueError If rotation is not one of "X", "Y", "Z". ValueError If entanglement is not one of "linear", "circular", "full". ValueError If n_features is less than 1 (raised by parent class).


Analysis Module

analysis

Analysis tools for quantum encoding properties.

This module provides quantitative analysis capabilities for quantum encodings, including resource counting, simulability analysis, expressibility computation, entanglement capability measurement, and trainability estimation.

Analysis Functions

The main analysis functions are:

  • :func:count_resources: Count computational resources (gates, depth, qubits)
  • :func:get_resource_summary: Quick resource summary from encoding properties
  • :func:get_gate_breakdown: Detailed gate-by-gate breakdown
  • :func:compare_resources: Compare resources across multiple encodings
  • :func:estimate_execution_time: Estimate circuit execution time
  • :func:check_simulability: Determine if classical simulation is efficient
  • :func:get_simulability_reason: Get a concise simulability explanation
  • :func:is_clifford_circuit: Check if encoding uses only Clifford gates
  • :func:is_matchgate_circuit: Check if encoding uses only matchgate operations
  • :func:estimate_entanglement_bound: Estimate upper bound on entanglement entropy
  • :func:compute_expressibility: Measure Hilbert space coverage
  • :func:compute_entanglement_capability: Measure entanglement generation
  • :func:estimate_trainability: Detect barren plateau risk
  • :func:compute_kernel_concentration: Measure fidelity-kernel concentration
  • :func:estimate_concentration_scaling: Track concentration across widths
  • :func:summarize_kernel_concentration: Concentration from a given kernel
  • :func:scan_feature_ranges: Measure sensitivity to feature scaling
  • :func:recommend_feature_range: Pick the range that maximises alignment

Utility Functions

Lower-level utilities for custom analysis:

  • :func:simulate_encoding_statevector: Simulate circuit to get statevector
  • :func:compute_fidelity: Compute fidelity between pure states
  • :func:compute_purity: Compute purity of a density matrix
  • :func:partial_trace_single_qubit: Compute reduced density matrix

Examples

Basic usage:

from encoding_atlas import AngleEncoding, IQPEncoding from encoding_atlas.analysis import count_resources, check_simulability

Compare resources between encodings

enc_simple = AngleEncoding(n_features=4) enc_complex = IQPEncoding(n_features=4, reps=2)

res_simple = count_resources(enc_simple) res_complex = count_resources(enc_complex)

print(f"Simple encoding: {res_simple['gate_count']} gates") print(f"Complex encoding: {res_complex['gate_count']} gates")

Advanced analysis:

from encoding_atlas.analysis import simulate_encoding_statevector import numpy as np

enc = AngleEncoding(n_features=2) x = np.array([0.5, 1.0]) state = simulate_encoding_statevector(enc, x) print(f"State dimension: {len(state)}")

See Also

encoding_atlas.core.properties : Encoding property dataclasses. encoding_atlas.core.exceptions : Analysis-related exceptions.

ConcentrationResult dataclass

ConcentrationResult(n_qubits: int, n_samples: int, offdiagonal_mean: float, offdiagonal_variance: float, offdiagonal_std: float, offdiagonal_min: float, offdiagonal_max: float, haar_mean: float, haar_variance: float, concentration_ratio: float, mean_ratio: float, identity_distance: float, is_concentrated: bool, threshold: float, shots_per_entry: float)

Fidelity-kernel concentration measured at one circuit width.

Attributes

n_qubits : int Circuit width the kernel was measured at. n_samples : int Number of inputs used; statistics are over the n_samples (n_samples - 1) / 2 off-diagonal pairs. offdiagonal_mean : float Mean off-diagonal kernel entry, in [0, 1]. offdiagonal_variance, offdiagonal_std : float Spread of the off-diagonal entries — the signal a kernel method actually uses. Its vanishing is concentration. offdiagonal_min, offdiagonal_max : float Extremes of the off-diagonal entries. haar_mean, haar_variance : float Haar-random reference moments at this width, from :func:haar_kernel_moments. concentration_ratio : float offdiagonal_variance / haar_variancethe order parameter. Near 1 means the kernel's spread has reached the Haar floor and carries no usable geometry; larger means structure remains. Directly comparable across qubit counts. mean_ratio : float offdiagonal_mean / haar_mean. Reported for completeness only: it is 1 for any ensemble of independent random single-qubit states as well as for Haar, so it cannot distinguish the two regimes. Do not use it as a concentration measure. identity_distance : float Root-mean-square off-diagonal entry, i.e. ||K - I||_F / sqrt(n (n - 1)). Zero means K is exactly I. is_concentrated : bool Whether concentration_ratio is below :attr:threshold. threshold : float The concentration_ratio cut used for :attr:is_concentrated. shots_per_entry : float Shots per kernel entry for the compute-uncompute estimator's standard error to fall below half the observed spread — 4 K_bar (1 - K_bar) / Var. inf if the entries have no spread. A noiseless lower bound on hardware cost.

shots_for_dataset

shots_for_dataset(n_samples: int) -> float

Total shots to estimate a full n_samples x n_samples kernel.

The kernel is symmetric with a deterministic unit diagonal, so only n (n - 1) / 2 entries need measuring.

Parameters

n_samples : int Dataset size. Must be a positive integer.

Returns

float shots_per_entry * n (n - 1) / 2; inf if the per-entry cost is unbounded.

Raises

ValueError If n_samples is not a positive integer.

EncodingCharacterization dataclass

EncodingCharacterization(encoding_name: str, n_qubits: int, n_features: int, is_simulable: bool | None, metrics: Mapping[str, Any], notes: Mapping[str, str])

A computed, multi-axis profile of a single encoding.

Attributes

encoding_name : str The encoding's class name. n_qubits, n_features : int Circuit width and input dimensionality. is_simulable : bool or None Whether the encoding is classically efficiently simulable. metrics : Mapping[str, float | None] Measured metrics. Always includes depth, expressibility, entanglement_capability, trainability_estimate, noise_retained_fidelity and the concentration axis (kernel_concentration_ratio, kernel_offdiagonal_mean, kernel_shots_per_entry, kernel_is_concentrated); includes kernel_target_alignment, geometric_difference and effective_dimension when a dataset was supplied. A value is None if that axis could not be computed (see notes). depth/expressibility/entanglement_capability/ trainability_estimate share the atlas's definitions; noise_retained_fidelity is the fidelity-based noise metric (distinct from the atlas's noise_resilience trade-off column). kernel_is_concentrated is a bool, not a float. notes : Mapping[str, str] Per-axis failure reasons for any metric that is None.

metric

metric(key: str, default: float | None = None) -> float | None

Return a metric value, or default if absent or uncomputable.

EntanglementResult

Bases: TypedDict

Result dictionary from entanglement capability computation.

Attributes

entanglement_capability : float Average entanglement measure in [0, 1]. Higher values indicate the encoding creates more entangled states on average. entanglement_samples : NDArray[np.floating] Array of entanglement measure values for each sampled input. Shape: (n_samples,). The measure type depends on the measure parameter used (Meyer-Wallach or Scott). std_error : float Standard error of the mean, computed as std / sqrt(n_samples). Useful for assessing the reliability of the estimate. n_samples : int Number of random samples used in the computation. per_qubit_entanglement : NDArray[np.floating] Average linear entropy contribution from each qubit. Shape: (n_qubits,). Useful for identifying which qubits are most entangled. Note: Only meaningful for Meyer-Wallach measure; returns zeros for Scott measure. measure : str The entanglement measure used: "meyer_wallach" or "scott". scott_k : int | None The k value used for Scott measure, or None if Meyer-Wallach was used. entanglement_ci_lower : float Lower bound of the percentile bootstrap confidence interval on entanglement_capability at confidence_level. Computed by resampling entanglement_samples with replacement. entanglement_ci_upper : float Upper bound of the bootstrap CI on entanglement_capability. confidence_level : float Two-sided confidence level used for the CI bounds above (default 0.95 → 95% CIs). sampling : {'uniform', 'sobol'} Sampling strategy that produced the per-sample inputs. See :func:compute_entanglement_capability for details.

ExpressibilityResult

Bases: TypedDict

Result dictionary from expressibility computation.

This TypedDict defines the structure of the result returned by :func:compute_expressibility when return_distributions=True.

Attributes

expressibility : float Normalized expressibility score in [0, 1]. Higher values indicate the encoding's fidelity distribution is closer to Haar-random, meaning it can explore more of the Hilbert space. kl_divergence : float Raw Kullback-Leibler divergence from the encoding's fidelity distribution to the Haar distribution. Lower values indicate higher expressibility. fidelity_distribution : NDArray[np.floating] Histogram of sampled fidelities (probability for each bin). Shape: (n_bins,). Values sum to approximately 1. haar_distribution : NDArray[np.floating] Theoretical Haar-random fidelity distribution evaluated at the same bin centers. Shape: (n_bins,). bin_edges : NDArray[np.floating] Edges of the histogram bins. Shape: (n_bins + 1,). Range is [0, 1] for fidelity values. n_samples : int Number of fidelity samples used in the computation. n_bins : int Number of histogram bins used. convergence_estimate : float Bootstrap estimate of the standard error of the KL divergence. Smaller values indicate more reliable results. mean_fidelity : float Mean of the sampled fidelities. Useful for understanding the typical overlap between encoded states. std_fidelity : float Standard deviation of the sampled fidelities. expressibility_ci_lower : float Lower bound of the percentile bootstrap confidence interval on expressibility at confidence_level. Computed by resampling the per-sample fidelity array and re-running the full histogram → KL → score pipeline on each resample. expressibility_ci_upper : float Upper bound of the percentile bootstrap CI on expressibility. mean_fidelity_ci_lower : float Lower bound of the bootstrap CI on mean_fidelity. mean_fidelity_ci_upper : float Upper bound of the bootstrap CI on mean_fidelity. confidence_level : float Two-sided confidence level used for the CI bounds above (default 0.95 → 95% CIs). sampling : {'uniform', 'sobol'} Sampling strategy that produced the fidelity pairs. See :func:compute_expressibility for details.

Examples

result = compute_expressibility(enc, return_distributions=True, seed=42) print(f"Expressibility: {result['expressibility']:.4f}") print(f"KL Divergence: {result['kl_divergence']:.4f}") print(f"Mean Fidelity: {result['mean_fidelity']:.4f}") lo, hi = result['expressibility_ci_lower'], result['expressibility_ci_upper'] print(f"95% CI: [{lo:.4f}, {hi:.4f}]")

FeatureRangeResult dataclass

FeatureRangeResult(low: float, high: float, width: float, alignment: float, concentration_ratio: float, offdiagonal_mean: float, offdiagonal_variance: float)

An encoding measured at one feature-scaling range.

Attributes

low, high : float The range the features were scaled into. width : float high - low, as a convenience for plotting against period coverage. alignment : float Kernel-target alignment at this range, in [-1, 1]. Higher predicts better downstream accuracy. concentration_ratio : float Off-diagonal kernel variance over the Haar variance. Near 1 and with offdiagonal_mean near 1/2**n_qubits means the kernel has collapsed onto the Haar floor. Not monotone in the range width on its own — see the module docstring. offdiagonal_mean, offdiagonal_variance : float Raw moments of the kernel's off-diagonal entries. The mean is the cleaner read on how close the kernel is to the floor: it decreases towards 1/2**n_qubits as the range widens.

FeatureRangeScan dataclass

FeatureRangeScan(encoding_name: str, n_qubits: int, n_features: int, n_samples_used: int, n_samples_supplied: int, centered: bool, results: tuple[FeatureRangeResult, ...])

How an encoding's kernel geometry responds to feature scaling.

Attributes

encoding_name : str Class name of the scanned encoding. n_qubits, n_features : int Circuit width and input dimensionality. n_samples_used, n_samples_supplied : int Rows used after sub-sampling, and rows supplied. centered : bool Whether the centred alignment variant was used. results : tuple[FeatureRangeResult, ...] One measurement per range, in the order scanned.

alignment_spread property

alignment_spread: float

Highest minus lowest alignment across the scanned ranges.

A large spread means the scaling choice matters as much as, or more than, the choice of encoding — so it should be reported, not assumed.

best property

The range with the highest alignment.

Ties break towards the narrower range, which is the more conservative choice: it keeps the kernel further from the concentration floor.

Raises

RuntimeError If the scan produced no measurements.

best_range property

best_range: tuple[float, float]

(low, high) of the highest-aligned range.

at

at(low: float, high: float) -> FeatureRangeResult | None

Return the measurement for a given range, or None.

NoiseResilienceResult dataclass

NoiseResilienceResult(retained_fidelity: float, fidelity_decay: float, noise_level: str, single_qubit_error: float, two_qubit_error: float, n_samples: int, std_fidelity: float, min_fidelity: float, max_fidelity: float)

Result of a noise-resilience analysis.

Attributes

retained_fidelity : float Mean fidelity between the ideal and noisy states over the sampled inputs, in [0, 1] (1 = perfectly resilient). fidelity_decay : float 1 - retained_fidelity: the fraction of state fidelity lost to noise. noise_level : str The preset level used, or "custom" for explicit noise_params. single_qubit_error, two_qubit_error : float The depolarizing probabilities applied after one- and two-qubit gates. n_samples : int Number of random inputs averaged over. std_fidelity : float Standard deviation of the per-input fidelities. min_fidelity, max_fidelity : float Extremes of the per-input fidelities.

ScalingResult dataclass

ScalingResult(encoding_name: str, n_qubits: tuple[int, ...], feature_counts: tuple[int, ...], results: tuple[ConcentrationResult, ...], decay_rate: float, mean_decay_rate: float, haar_normalized_slope: float, r_squared: float, skipped: dict[int, str])

How an encoding's kernel concentration scales with circuit width.

Attributes

encoding_name : str Class name of the encodings the factory produced. n_qubits : tuple[int, ...] Circuit widths successfully measured, ascending. feature_counts : tuple[int, ...] Feature counts that produced those widths, in the same order. results : tuple[ConcentrationResult, ...] Per-width measurements, in the same order. decay_rate : float Per-qubit factor by which the off-diagonal variance shrinks, from a least-squares fit of log(variance) against n_qubits. 1.0 means no decay. The Haar floor itself decays at 4.0, so a rate near 4 means the encoding is scrambling as fast as it possibly can. nan if fewer than two widths were measurable. mean_decay_rate : float The same quantity for the off-diagonal mean. The Haar floor decays at 2.0. nan if unfittable. haar_normalized_slope : float Slope of log(concentration_ratio) against n_qubits. Negative means the kernel is collapsing towards the Haar floor as the circuit widens; non-negative means it holds structure or pulls away. This is the honest discriminator — a large decay_rate alone proves nothing, because the floor is falling too. r_squared : float Coefficient of determination of the log(variance) fit, in [0, 1]. Well below ~0.9 means the log-linear model — and any extrapolation from it — should not be trusted. nan for fewer than two points. skipped : dict[int, str] Feature counts that could not be measured, mapped to the reason.

concentration_ratios property

concentration_ratios: tuple[float, ...]

The measured concentration_ratio at each width.

offdiagonal_means property

offdiagonal_means: tuple[float, ...]

The measured mean off-diagonal entry at each width.

offdiagonal_variances property

offdiagonal_variances: tuple[float, ...]

The measured off-diagonal variance at each width.

concentration_horizon

concentration_horizon(threshold: float = CONCENTRATION_THRESHOLD) -> int | None

Smallest qubit count at which the kernel reaches the Haar floor.

Resolution order:

  1. If the measured widths show a sustained crossing — some width from which every wider measured point is also below threshold — the smallest such width is returned. No extrapolation involved. The crossing must be sustained: a single dip below the threshold at the narrowest width, followed by growth away from the floor, is not a horizon.
  2. Otherwise, if :attr:haar_normalized_slope is negative, the fit is extrapolated from the widest measured point to the first integer width whose predicted ratio falls below threshold.
  3. Otherwise None: the encoding shows no sign of approaching the floor over the measured range.
Parameters

threshold : float, default=:data:CONCENTRATION_THRESHOLD concentration_ratio defining the floor. Must be positive.

Returns

int or None The horizon, or None if the measurement does not support one.

Raises

ValueError If threshold is not positive.

Warnings

Case 2 is an extrapolation from a fitted log-linear model. Check :attr:r_squared before relying on it.

shots_per_entry_at

shots_per_entry_at(n_qubits: int) -> float

Extrapolate the per-entry shot requirement to a wider circuit.

Propagates the mean and the variance forward with their own fitted decay rates and re-evaluates 4 K_bar (1 - K_bar) / Var. Using both fits matters: the relative spread of the entries is not preserved as the circuit widens, so scaling the standard deviation with the mean would be wrong by an exponential factor.

Parameters

n_qubits : int Target circuit width. Must be a positive integer. Widths at or below the measured range return the nearest measurement instead of an extrapolation.

Returns

float Extrapolated shots per kernel entry, or nan if no fit exists.

Raises

ValueError If n_qubits is not a positive integer.

Warnings

Beyond the measured range this is an extrapolation, not a measurement. Check :attr:r_squared.

SimulabilityResult

Bases: TypedDict

Result of simulability analysis.

Attributes

is_simulable : bool True if the encoding circuit can be efficiently simulated classically. simulability_class : str One of "simulable", "conditionally_simulable", "not_simulable". reason : str Human-readable explanation of the classification. details : dict[str, Any] Additional analysis details including:

- is_entangling: Whether circuit creates entanglement
- is_clifford: Whether circuit uses only Clifford gates
- is_matchgate: Whether circuit uses only matchgate operations
- entanglement_pattern: Description of entanglement structure
- two_qubit_gate_count: Number of two-qubit gates
- n_qubits: Number of qubits
- declared_simulability: Simulability from encoding properties
list[str]

Suggestions for simulation approaches.

TrainabilityResult

Bases: TypedDict

Result of trainability estimation.

This TypedDict defines the structure of the detailed result returned by :func:estimate_trainability when return_details=True.

Attributes

trainability_estimate : float Overall trainability score in the range [0, 1], where higher values indicate better trainability. This score is derived from the gradient variance using a logistic transformation. gradient_variance : float Average variance of parameter gradients computed over successful samples only. This is the primary metric for barren plateau detection. Values near zero indicate a barren plateau. barren_plateau_risk : {"low", "medium", "high"} Categorical assessment of barren plateau risk based on gradient variance and circuit size:

- "low": Variance is healthy, training should proceed normally
- "medium": Variance is borderline, training may be slow
- "high": Variance is very low, likely barren plateau
float

Estimated effective parameter dimension (HEURISTIC METRIC), computed as the count of parameters with variance exceeding 1% of the mean variance. This indicates how many parameters contribute meaningfully to the optimization landscape. Note: This is not a standard metric in barren plateau literature; it is provided for diagnostic purposes. For rigorous analysis, examine per_parameter_variance directly.

n_samples : int Total number of random parameter samples requested for the estimate. n_successful_samples : int Number of samples where gradient computation succeeded. This is the actual sample size used for variance computation. Equal to n_samples - n_failed_samples. per_parameter_variance : NDArray[np.floating] Array of variance values for each individual parameter, computed over successful samples only. This can reveal if specific parameters are more trainable than others. n_failed_samples : int Number of samples where gradient computation failed. High values may indicate issues with the encoding or simulation backend. Failed samples are excluded from variance computation to ensure unbiased statistical estimates. trainability_ci_lower : float Lower bound of the percentile bootstrap CI on trainability_estimate at confidence_level. Computed by resampling the per-sample gradient matrix and re-running the variance → trainability mapping on each resample. trainability_ci_upper : float Upper bound of the bootstrap CI on trainability_estimate. gradient_variance_ci_lower : float Lower bound of the bootstrap CI on gradient_variance. gradient_variance_ci_upper : float Upper bound of the bootstrap CI on gradient_variance. confidence_level : float Two-sided confidence level used for the CI bounds above (default 0.95 → 95% CIs). sampling : {'uniform', 'sobol'} Sampling strategy that produced the per-sample inputs. See :func:estimate_trainability for details.

atlas_comparable_metrics

atlas_comparable_metrics() -> list[str]

Return the metric keys that can be compared against the atlas.

centered_kernel_target_alignment

centered_kernel_target_alignment(K: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]]) -> float

Centred kernel-target alignment in [-1, 1] (Cortes et al., 2012).

Centring removes the bias the uncentred score suffers for fidelity kernels (whose diagonal is always 1), giving a more faithful measure of task alignment. Returns 0.0 for degenerate inputs or n < 2.

This is the variant the benchmark uses and the one to screen encodings with. It is already invariant to affine relabelling of y; labels are normalised through :func:_signed_labels so both variants agree on what a two-class split means.

check_simulability

check_simulability(encoding: BaseEncoding, detailed: bool = True) -> SimulabilityResult

Check whether an encoding circuit is classically simulable.

Analyzes the encoding's circuit structure to determine if it can be efficiently simulated on a classical computer. This analysis considers entanglement structure, gate set, and circuit topology.

Parameters

encoding : BaseEncoding The encoding instance to analyze. detailed : bool, default=True If True, include detailed analysis in the result. If False, return minimal result with just classification.

Returns

SimulabilityResult Dictionary containing:

- ``is_simulable`` : bool
    True if efficiently classically simulable.
- ``simulability_class`` : str
    One of "simulable", "conditionally_simulable", "not_simulable".
- ``reason`` : str
    Human-readable explanation of the classification.
- ``details`` : dict
    Additional analysis details (if detailed=True):

    - is_entangling: Whether circuit creates entanglement
    - is_clifford: Whether circuit uses only Clifford gates
    - entanglement_pattern: Description of entanglement structure
    - two_qubit_gate_count: Number of two-qubit gates
    - n_qubits: Number of qubits
    - declared_simulability: Simulability from encoding properties

- ``recommendations`` : list[str]
    Suggestions for simulation approaches.
Raises

AnalysisError If encoding is not a valid BaseEncoding instance or analysis fails.

Examples

Check a non-entangling encoding:

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) result = check_simulability(enc) print(result['simulability_class']) simulable print(result['reason']) Encoding produces only product states (no entanglement)

Check an entangling encoding:

from encoding_atlas import IQPEncoding enc = IQPEncoding(n_features=4, reps=2) result = check_simulability(enc) print(result['simulability_class']) not_simulable print(result['recommendations']) ['Use statevector simulation for small instances (< 20 qubits)', ...]

Quick check without details:

result = check_simulability(enc, detailed=False) if result['is_simulable']: ... print("Can use classical simulation")

Notes

Simulability Classes:

  • simulable: Circuit can always be efficiently simulated. Examples: Clifford circuits, product state circuits.

  • conditionally_simulable: Circuit may be simulable depending on input data, circuit parameters, or specific structure. Examples: Circuits with linear/circular entanglement topology.

  • not_simulable: Circuit is believed to be hard to simulate classically. Examples: IQP circuits, random circuits with high entanglement.

Limitations:

This analysis is based on known theoretical results and heuristics. It may be conservative (classifying some simulable circuits as not_simulable) but aims not to falsely claim simulability.

See Also

is_clifford_circuit : Check if circuit uses only Clifford gates. estimate_entanglement_bound : Estimate entanglement entropy bound. count_resources : Get detailed gate counts.

compare_resources

compare_resources(encodings: list[BaseEncoding], metrics: list[str] | None = None, *, include_names: bool = True) -> dict[str, list[Any]]

Compare resources across multiple encodings.

Provides side-by-side comparison of resource metrics for a list of encodings. Useful for encoding selection and benchmarking.

Parameters

encodings : list[BaseEncoding] List of encodings to compare. Must all be valid BaseEncoding instances. metrics : list[str], optional Specific metrics to compare. If None, compares all standard metrics. Available metrics:

- ``"n_qubits"``: Number of qubits
- ``"depth"``: Circuit depth
- ``"gate_count"``: Total gates
- ``"single_qubit_gates"``: Single-qubit gate count
- ``"two_qubit_gates"``: Two-qubit gate count
- ``"parameter_count"``: Parameterized gate count
- ``"two_qubit_ratio"``: Ratio of two-qubit gates
- ``"gates_per_qubit"``: Average gates per qubit
bool, default=True

If True, include encoding names in the result.

Returns

dict[str, list[Any]] Dictionary mapping metric names to lists of values (one per encoding). If include_names=True, includes an "encoding_name" key with encoding class names.

Raises

AnalysisError If any encoding is invalid. ValueError If encodings list is empty.

Examples

from encoding_atlas import AngleEncoding, IQPEncoding from encoding_atlas.analysis import compare_resources

encodings = [ ... AngleEncoding(n_features=4), ... IQPEncoding(n_features=4, reps=1), ... IQPEncoding(n_features=4, reps=2), ... ] comparison = compare_resources(encodings) print(comparison['encoding_name']) ['AngleEncoding', 'IQPEncoding', 'IQPEncoding'] print(comparison['gate_count']) [4, 26, 52]

Compare specific metrics:

comparison = compare_resources( ... encodings, ... metrics=['gate_count', 'two_qubit_gates'], ... ) print(comparison.keys()) dict_keys(['encoding_name', 'gate_count', 'two_qubit_gates'])

Notes

For data-dependent encodings, this function returns worst-case (maximum) values from encoding properties. Use :func:count_resources with specific input data for actual counts.

See Also

count_resources : Get resources for a single encoding. get_resource_summary : Quick summary for a single encoding.

compare_to_atlas

compare_to_atlas(characterization: EncodingCharacterization, metric: str) -> dict[str, Any]

Rank a profiled encoding against the 16 built-in encodings on metric.

Only axes that share the atlas's definition are supported (see :func:atlas_comparable_metrics): depth (lower is better), expressibility, entanglement_capability and trainability_estimate (higher is better).

Parameters

characterization : EncodingCharacterization A profile produced by :func:profile_encoding. metric : str The axis to compare on.

Returns

dict {"metric", "value", "rank", "n_atlas", "percentile", "beats", "atlas_min", "atlas_median", "atlas_max", "higher_is_better"} where rank is 1-based (1 = better than every atlas encoding) and percentile is the fraction of atlas encodings the value beats.

Raises

ValueError If metric is not atlas-comparable or was not computed.

compute_all_parameter_gradients

compute_all_parameter_gradients(encoding: BaseEncoding, x: NDArray[floating[Any]], observable: Literal['computational', 'global_z', 'local_z', 'pauli_z'] = 'computational', backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> FloatArray

Compute gradients for all parameters using the parameter-shift rule.

Builds the full (2 * n_params, n_features) matrix of shifted parameter vectors, dispatches a single :func:simulate_encoding_statevectors_batch call, and evaluates all expectation values with one vectorised observable computation.

Compared to the previous implementation — a Python for loop over parameters, each calling :func:compute_parameter_gradient and therefore each invoking the backend simulator twice independently — this version still performs the same 2 * n_params statevector simulations (the parameter-shift rule fundamentally requires them) but amortises the Python and observable-evaluation overhead into a single pass. The numerical result is identical to the per-call version (verified by test_batched_matches_per_call_gradient).

Parameters

encoding : BaseEncoding The encoding instance. x : NDArray[np.floating] Input data vector of shape (n_features,). observable : {"computational", "global_z", "local_z", "pauli_z"}, default="computational" Observable to measure. See :func:compute_parameter_gradient for the mathematical definitions. "pauli_z" is a deprecated alias for "global_z" and emits a DeprecationWarning. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Simulation backend.

Returns

FloatArray Array of gradients of shape (n_features,). The element at index i is ∂⟨O⟩/∂xᵢ evaluated at the input point x.

Raises

ValidationError If x is not a 1-D array. ValueError If observable is not one of the recognised options. SimulationError If the underlying batched simulation fails.

Examples

from encoding_atlas import AngleEncoding import numpy as np

enc = AngleEncoding(n_features=3) x = np.array([0.5, 1.0, 1.5]) gradients = compute_all_parameter_gradients(enc, x) print(f"Gradients shape: {gradients.shape}") Gradients shape: (3,)

compute_effective_dimension

compute_effective_dimension(encoding: BaseEncoding, X: NDArray[floating[Any]], *, regularization: float = 1.0, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', shots: int | None = None, seed: int | None = None) -> float

Feature-space effective dimension of an encoding's fidelity kernel.

Larger values indicate the encoding spreads data across more effective feature-space directions (higher capacity). See :func:kernel_effective_dimension for the definition.

Parameters

encoding : BaseEncoding Encoding under test. X : ndarray, shape (n_samples, n_features) Input data. regularization : float, default=1.0 Ridge parameter of the effective-dimension sum. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector backend. shots : int or None, default=None Measure on a finite-shot kernel estimate instead of the exact one. Shot noise raises the apparent dimension by spreading the eigenvalue spectrum, so finite-shot values overstate capacity. seed : int or None, default=None Seed for the shot sampling. Ignored when shots is None.

compute_entanglement_capability

compute_entanglement_capability(encoding: BaseEncoding, n_samples: int = ..., input_range: tuple[float, float] = ..., seed: int | None = ..., backend: Literal['pennylane', 'qiskit', 'cirq'] = ..., measure: Literal['meyer_wallach', 'scott'] = ..., scott_k: int | None = ..., return_details: Literal[False] = ..., verbose: bool = ..., parallel: ParallelArg = ..., max_workers: int | None = ..., sampling: SamplingMethod = ..., confidence_level: float = ..., n_bootstrap_ci: int = ...) -> float
compute_entanglement_capability(encoding: BaseEncoding, n_samples: int = ..., input_range: tuple[float, float] = ..., seed: int | None = ..., backend: Literal['pennylane', 'qiskit', 'cirq'] = ..., measure: Literal['meyer_wallach', 'scott'] = ..., scott_k: int | None = ..., return_details: Literal[True] = ..., verbose: bool = ..., parallel: ParallelArg = ..., max_workers: int | None = ..., sampling: SamplingMethod = ..., confidence_level: float = ..., n_bootstrap_ci: int = ...) -> EntanglementResult
compute_entanglement_capability(encoding: BaseEncoding, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', measure: Literal['meyer_wallach', 'scott'] = 'meyer_wallach', scott_k: int | None = None, return_details: bool = False, verbose: bool = False, parallel: ParallelArg = False, max_workers: int | None = None, sampling: SamplingMethod = 'uniform', confidence_level: float = _DEFAULT_CONFIDENCE_LEVEL, n_bootstrap_ci: int = _DEFAULT_N_BOOTSTRAP_CI) -> Union[float, EntanglementResult]

Compute the entanglement capability of a quantum encoding.

Entanglement capability quantifies the average amount of entanglement produced by the encoding across random inputs. Higher values indicate the encoding creates more entangled states on average.

Parameters

encoding : BaseEncoding The encoding instance to analyze. Must implement get_circuit(). n_samples : int, default=1000 Number of random inputs to sample for averaging. Higher values give more accurate estimates but increase computation time. Must be at least 10. input_range : tuple[float, float], default=(0, 2*pi) Range for sampling random input values. Features are sampled uniformly from this range. seed : int, optional Random seed for reproducibility. If None, uses system entropy. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Backend for circuit simulation. measure : {"meyer_wallach", "scott"}, default="meyer_wallach" Entanglement measure to use:

- ``"meyer_wallach"``: Average single-qubit purity deficit.
  Faster and most commonly used. Equivalent to Scott measure with k=1.
- ``"scott"``: Scott measure with configurable k-body subsystems.
  More detailed but slower. Use ``scott_k`` to specify subsystem size.
int, optional

Subsystem size for Scott measure. Only used when measure="scott". Must satisfy 1 <= scott_k <= n_qubits - 1.

  • If None (default): Automatically selects min(2, n_qubits - 1). For 2-qubit systems this falls back to k=1 (Meyer-Wallach).
  • If specified: Uses the given k value, raising an error if invalid.

Ignored when measure="meyer_wallach".

bool, default=False

If True, return full result dictionary with statistics. If False, return only the entanglement capability score.

verbose : bool, default=False If True, log progress during computation. parallel : bool or {'thread', 'process'}, default=False Parallel-dispatch mode for the per-sample simulation + entanglement-measure computation.

- ``False`` (default) — sequential, no executor overhead.
- ``True`` or ``'thread'`` — :class:`ThreadPoolExecutor`.
- ``'process'`` — :class:`ProcessPoolExecutor` with the encoding
  pickled once per worker. Workers exchange only float / NumPy
  arrays, so process-pool parallelism works with **all** three
  backends here (unlike ``BaseEncoding.get_circuits`` where
  PennyLane's local-closure qfuncs prevent process-pool use).

Output is numerically identical across all modes for a fixed
``seed`` — the RNG is fully consumed in the main process before
any work is dispatched.

max_workers : int or None, default=None Maximum number of workers when parallel is enabled. sampling : {'uniform', 'sobol'}, default='uniform' Strategy for drawing the random per-sample inputs:

- ``'uniform'`` (default, unchanged): pseudo-random i.i.d.
  uniform draws.
- ``'sobol'``: Sobol' low-discrepancy quasi-random sequence,
  seeded from the same ``seed`` argument. Typically gives the
  same mean accuracy with **30-50% fewer samples**. For best
  statistical properties choose ``n_samples`` as a power of
  two.

confidence_level : float, default=0.95 Two-sided confidence level for the bootstrap CI reported in the result dict (only used when return_details=True). Must lie strictly in (0, 1). n_bootstrap_ci : int, default=200 Number of bootstrap resamples used to estimate the entanglement_capability percentile CI (only when return_details=True). 200 stabilizes the percentile endpoints to ~1% jitter at negligible cost.

Returns

float or EntanglementResult If return_details=False: Entanglement capability in [0, 1].

If ``return_details=True``: Dictionary containing:
    - ``entanglement_capability``: float in [0, 1]
    - ``entanglement_samples``: per-sample entanglement values
    - ``std_error``: standard error of mean
    - ``n_samples``: number of samples used
    - ``per_qubit_entanglement``: average entanglement per qubit
    - ``measure``: the measure used ("meyer_wallach" or "scott")
    - ``scott_k``: the k value used (None for Meyer-Wallach)
Raises

InsufficientSamplesError If n_samples < 10. SimulationError If circuit simulation fails. ValueError If encoding has < 2 qubits (entanglement requires multiple qubits). ValidationError If encoding is invalid or input parameters are malformed.

Warns

UserWarning If n_samples < 100, results may be unreliable.

Examples

Basic usage:

from encoding_atlas import IQPEncoding enc = IQPEncoding(n_features=4, reps=2, entanglement="full") ent = compute_entanglement_capability(enc, seed=42) print(f"Entanglement: {ent:.4f}")

Compare different entanglement patterns:

enc_linear = IQPEncoding(n_features=4, reps=2, entanglement="linear") enc_full = IQPEncoding(n_features=4, reps=2, entanglement="full") print(f"Linear: {compute_entanglement_capability(enc_linear, seed=42):.4f}") print(f"Full: {compute_entanglement_capability(enc_full, seed=42):.4f}")

Get detailed statistics:

result = compute_entanglement_capability( ... enc, n_samples=500, seed=42, return_details=True ... ) print(f"Mean: {result['entanglement_capability']:.4f}") print(f"Std Error: {result['std_error']:.4f}") print(f"Per-qubit: {result['per_qubit_entanglement']}")

Notes

For non-entangling encodings (e.g., AngleEncoding), the entanglement capability will be exactly 0.0, as all generated states are product states.

The computation time scales linearly with n_samples and exponentially with the number of qubits (due to statevector simulation).

See Also

compute_meyer_wallach : Compute MW measure for a single state. compute_scott_measure : Compute Scott measure for a single state. compute_expressibility : Related metric for Hilbert space coverage.

compute_expressibility

compute_expressibility(encoding: BaseEncoding, n_samples: int = ..., n_bins: int | None = ..., input_range: tuple[float, float] = ..., seed: int | None = ..., backend: Literal['pennylane', 'qiskit', 'cirq'] = ..., return_distributions: Literal[False] = ..., verbose: bool = ..., parallel: ParallelArg = ..., max_workers: int | None = ..., sampling: SamplingMethod = ..., confidence_level: float = ..., n_bootstrap_ci: int = ...) -> float
compute_expressibility(encoding: BaseEncoding, n_samples: int = ..., n_bins: int | None = ..., input_range: tuple[float, float] = ..., seed: int | None = ..., backend: Literal['pennylane', 'qiskit', 'cirq'] = ..., return_distributions: Literal[True] = ..., verbose: bool = ..., parallel: ParallelArg = ..., max_workers: int | None = ..., sampling: SamplingMethod = ..., confidence_level: float = ..., n_bootstrap_ci: int = ...) -> ExpressibilityResult
compute_expressibility(encoding: BaseEncoding, n_samples: int = _DEFAULT_N_SAMPLES, n_bins: int | None = None, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', return_distributions: bool = False, verbose: bool = False, parallel: ParallelArg = False, max_workers: int | None = None, sampling: SamplingMethod = 'uniform', confidence_level: float = _DEFAULT_CONFIDENCE_LEVEL, n_bootstrap_ci: int = _DEFAULT_N_BOOTSTRAP_CI) -> float | ExpressibilityResult

Compute the expressibility of a quantum encoding.

Expressibility quantifies how well the encoding can explore the Hilbert space compared to Haar-random states. Higher expressibility (closer to 1.0) indicates the encoding can generate a more diverse set of quantum states, which is generally desirable for variational quantum algorithms.

The computation works by:

  1. Sampling random input pairs and computing their quantum states
  2. Computing fidelities between state pairs
  3. Building a histogram of fidelities
  4. Comparing to the theoretical Haar-random distribution via KL divergence
  5. Normalizing to a [0, 1] score
Parameters

encoding : BaseEncoding The encoding instance to analyze. Must have n_features and n_qubits attributes and implement get_circuit(). n_samples : int, default=5000 Number of random input pairs to sample. Higher values give more accurate results but increase computation time. A minimum of 1000 samples is recommended for reliable results. n_bins : int or None, default=None Number of bins for the fidelity histogram. If None (default), automatically set to min(75, n_samples) so that small sample counts don't require manually tuning the bin count. If provided explicitly, must satisfy 10 ≤ n_bins ≤ n_samples. Rule of thumb: n_bins ≈ √n_samples. input_range : tuple[float, float], default=(0, 2π) Range for sampling random input values. The default covers the full rotation range for typical quantum gates. seed : int, optional Random seed for reproducibility. If None, results will vary between runs. Always specify a seed for reproducible analysis. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Backend to use for circuit simulation.

- ``"pennylane"``: Uses PennyLane's default.qubit simulator (recommended)
- ``"qiskit"``: Uses Qiskit's Statevector class
- ``"cirq"``: Uses Cirq's Simulator for statevector simulation

return_distributions : bool, default=False If True, return full result dictionary including fidelity distributions, bin edges, and statistics. If False, return only the expressibility score (float). verbose : bool, default=False If True, log progress information during computation. parallel : bool or {'thread', 'process'}, default=False Parallel-dispatch mode for the per-sample fidelity computation. Each sample requires two independent statevector simulations, making the loop embarrassingly parallel.

- ``False`` (default) — sequential, no executor overhead.
- ``True`` or ``'thread'`` — :class:`ThreadPoolExecutor`. Useful
  when the backend simulator releases the GIL during numerical
  work (NumPy operations typically do).
- ``'process'`` — :class:`ProcessPoolExecutor` for true CPU
  parallelism. The encoding is pickled once per worker. Best
  for large ``n_samples`` and CPU-bound backends. Has
  process-startup overhead, so only worthwhile for batches of
  roughly 100+ samples. Workers exchange only NumPy arrays /
  floats, so process-pool parallelism works with **all** three
  backends here (unlike ``BaseEncoding.get_circuits`` where
  PennyLane's local-closure qfuncs prevent process-pool use).

Output is numerically identical across all modes for a fixed
``seed``: the RNG is fully consumed in the main process before
any work is dispatched to workers.

max_workers : int or None, default=None Maximum number of workers when parallel is enabled. None defers to the executor's default (typically based on CPU count). sampling : {'uniform', 'sobol'}, default='uniform' Strategy for drawing the random input pairs (X1, X2):

- ``'uniform'`` (default, unchanged behavior): pseudo-random
  i.i.d. samples via :meth:`numpy.random.Generator.uniform`.
- ``'sobol'``: Sobol' low-discrepancy quasi-random sequence
  via :class:`scipy.stats.qmc.Sobol`. Covers the hypercube
  much more evenly than i.i.d. uniform draws and typically
  reaches the same KL accuracy with **30-50% fewer samples**.
  For best statistical properties round ``n_samples`` up to
  the nearest power of two.

Sobol is seeded from the same ``seed`` argument so a given
``(seed, sampling)`` pair is fully reproducible.

confidence_level : float, default=0.95 Two-sided confidence level for the bootstrap CIs reported in the result dict (only used when return_distributions=True). Must lie strictly in (0, 1). Common choices: 0.90, 0.95, 0.99. n_bootstrap_ci : int, default=200 Number of bootstrap resamples used to estimate the expressibility and mean_fidelity percentile CIs (only when return_distributions=True). 200 stabilizes the percentile endpoints to ~1% jitter; raise to ~1000 for publication-grade precision.

Returns

float or ExpressibilityResult If return_distributions=False: Expressibility score in [0, 1]. Higher values indicate more expressive encodings.

If ``return_distributions=True``: Dictionary containing:

- **expressibility**: float in [0, 1], higher = more expressive
- **kl_divergence**: raw KL divergence value
- **fidelity_distribution**: sampled fidelity histogram
- **haar_distribution**: theoretical Haar distribution
- **bin_edges**: histogram bin edges
- **n_samples**: number of samples used
- **n_bins**: number of histogram bins
- **convergence_estimate**: bootstrap standard error estimate
- **mean_fidelity**: mean of sampled fidelities
- **std_fidelity**: standard deviation of sampled fidelities
Raises

InsufficientSamplesError If n_samples < 10 (too few for meaningful statistics). SimulationError If circuit simulation fails for the given backend. AnalysisError If encoding is invalid or computation fails. ValueError If explicit n_bins < 10 or n_bins > n_samples, or if input_range[0] >= input_range[1].

Warns

UserWarning If n_samples < 100 (results may be unreliable). UserWarning If encoding has > 10 qubits (computation may be slow).

Examples

Basic usage with default parameters:

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) expr = compute_expressibility(enc, seed=42) print(f"Expressibility: {expr:.4f}") Expressibility: ...

Get full result with distributions:

result = compute_expressibility(enc, return_distributions=True, seed=42) print(f"KL Divergence: {result['kl_divergence']:.4f}") print(f"Samples used: {result['n_samples']}")

Compare two encodings:

from encoding_atlas import IQPEncoding enc1 = AngleEncoding(n_features=4) # Non-entangling enc2 = IQPEncoding(n_features=4, reps=2) # Entangling expr1 = compute_expressibility(enc1, n_samples=1000, seed=42) expr2 = compute_expressibility(enc2, n_samples=1000, seed=42) print(f"AngleEncoding: {expr1:.4f}") print(f"IQPEncoding: {expr2:.4f}")

IQP typically shows higher expressibility due to entanglement

Notes

Computational Complexity

The complexity is O(n_samples × simulation_cost), where simulation_cost scales as O(2^n_qubits). For encodings with more than 10 qubits, consider reducing n_samples or using approximate methods.

Interpretation

  • Expressibility ≈ 0: Encoding produces states in a narrow region of Hilbert space (e.g., product states only)
  • Expressibility ≈ 1: Encoding produces states distributed like Haar-random states (explores full Hilbert space)

Normalization Formula

The expressibility score is normalized from KL divergence using::

expr = 1.0 - min(1.0, kl_divergence / MAX_KL)

This maps KL divergence to [0, 1] where higher values indicate distributions closer to Haar-random (more expressive).

Statistical Reliability

The convergence_estimate in the result dictionary provides a bootstrap estimate of the standard error. Values < 0.05 generally indicate reliable results.

See Also

compute_fidelity_distribution : Compute fidelity distribution only. compute_haar_distribution : Compute theoretical Haar distribution. compute_entanglement_capability : Related metric for entanglement.

References

.. [1] Sim, S., Johnson, P. D., & Aspuru-Guzik, A. (2019). "Expressibility and entangling capability of parameterized quantum circuits for hybrid quantum-classical algorithms." Advanced Quantum Technologies, 2(12), 1900070.

compute_fidelity

compute_fidelity(state1: StatevectorType, state2: StatevectorType) -> float

Compute the fidelity between two pure quantum states.

The fidelity between two pure states is defined as:

F(|ψ⟩, |φ⟩) = |⟨ψ|φ⟩|²

This measures how "close" two quantum states are, with F = 1 for identical states (up to global phase) and F = 0 for orthogonal states.

Parameters

state1 : StatevectorType First statevector. state2 : StatevectorType Second statevector. Must have the same dimension as state1.

Returns

float Fidelity value in the range [0, 1].

Raises

ValueError If states have different dimensions. ValidationError If states are invalid.

Examples

Identical states have fidelity 1:

import numpy as np state = np.array([1, 0, 0, 0], dtype=complex) fidelity = compute_fidelity(state, state) print(f"Fidelity: {fidelity}") Fidelity: 1.0

Orthogonal states have fidelity 0:

state1 = np.array([1, 0], dtype=complex) # |0⟩ state2 = np.array([0, 1], dtype=complex) # |1⟩ fidelity = compute_fidelity(state1, state2) print(f"Fidelity: {fidelity}") Fidelity: 0.0

States differing by a global phase have fidelity 1:

state1 = np.array([1, 0], dtype=complex) state2 = np.array([1j, 0], dtype=complex) # |0⟩ with phase fidelity = compute_fidelity(state1, state2) print(f"Fidelity: {fidelity:.6f}") Fidelity: 1.000000

Notes

The implementation uses :func:numpy.vdot which conjugates the first argument, computing ⟨state1|state2⟩ = Σᵢ state1ᵢ* · state2ᵢ.

For mixed states (density matrices), use the more general formula: F(ρ, σ) = (Tr√(√ρ σ √ρ))²

See Also

compute_purity : Compute purity of a density matrix.

compute_fidelity_distribution

compute_fidelity_distribution(encoding: BaseEncoding, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', parallel: ParallelArg = False, max_workers: int | None = None, sampling: SamplingMethod = 'uniform') -> NDArray[floating[Any]]

Compute the fidelity distribution for an encoding.

Samples random input pairs and computes the fidelity between their corresponding quantum states. This is useful for analyzing the encoding's behavior without computing the full expressibility.

Parameters

encoding : BaseEncoding The encoding to analyze. n_samples : int, default=5000 Number of random input pairs to sample. input_range : tuple[float, float], default=(0, 2π) Range for random input sampling. seed : int, optional Random seed for reproducibility. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Backend for circuit simulation.

- ``"pennylane"``: Uses PennyLane's default.qubit simulator (recommended)
- ``"qiskit"``: Uses Qiskit's Statevector class
- ``"cirq"``: Uses Cirq's Simulator for statevector simulation

parallel : bool or {'thread', 'process'}, default=False Parallel-dispatch mode for the per-sample fidelity computation. See :func:compute_expressibility for the full discussion. max_workers : int or None, default=None Maximum number of workers when parallel is enabled. sampling : {'uniform', 'sobol'}, default='uniform' Strategy for drawing the random input pairs. See :func:compute_expressibility for the full discussion.

Returns

NDArray[np.floating] Array of fidelity values, shape (n_samples,), each in [0, 1].

Raises

InsufficientSamplesError If n_samples < 10. SimulationError If circuit simulation fails. AnalysisError If encoding is invalid.

Examples

from encoding_atlas import IQPEncoding enc = IQPEncoding(n_features=4, reps=1) fidelities = compute_fidelity_distribution(enc, n_samples=1000, seed=42) print(f"Mean fidelity: {fidelities.mean():.4f}") print(f"Std fidelity: {fidelities.std():.4f}")

Notes

The fidelity F = |⟨ψ₁|ψ₂⟩|² measures the overlap between two quantum states. F = 1 means identical states (up to global phase), F = 0 means orthogonal states.

For Haar-random states in high dimensions, most pairs have F ≈ 0 because random high-dimensional vectors are typically nearly orthogonal.

See Also

compute_expressibility : Full expressibility computation. compute_haar_distribution : Theoretical Haar distribution.

compute_fidelity_kernel

compute_fidelity_kernel(encoding: BaseEncoding, X: NDArray[floating[Any]], *, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', shots: int | None = None, seed: int | None = None) -> NDArray[floating[Any]]

Compute the fidelity (overlap) kernel matrix for X.

K[i, j] = |<phi(x_i)|phi(x_j)>|^2 where |phi(x)> = U(x)|0> is the encoded state. With the default shots=None the matrix is exact: symmetric, PSD, unit diagonal, entries in [0, 1].

Parameters

encoding : BaseEncoding Encoding used for state preparation. encoding.n_features must equal X.shape[1]. X : ndarray, shape (n_samples, n_features) Input data. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend. shots : int or None, default=None When given, return the finite-shot compute-uncompute estimate from shots measurements per entry instead of the exact kernel (see :func:sample_shot_kernel). The result stays symmetric with a unit diagonal but is no longer guaranteed PSD. seed : int or None, default=None Seed for the shot sampling. Ignored when shots is None.

Returns

ndarray, shape (n_samples, n_samples) The fidelity kernel matrix.

Raises

ValueError If shots is given but is not a positive integer.

compute_geometric_difference

compute_geometric_difference(encoding: BaseEncoding, X: NDArray[floating[Any]], *, classical_kernel: Literal['rbf', 'linear'] = 'rbf', gamma: float | None = None, regularization: float = 1e-06, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', shots: int | None = None, seed: int | None = None) -> float

Geometric difference g(K_classical || K_quantum) for an encoding.

A large value means the encoding's fidelity kernel is geometrically distinct from the classical reference kernel — a necessary (not sufficient) condition for a quantum advantage over that classical kernel.

Parameters

encoding : BaseEncoding Encoding under test. X : ndarray, shape (n_samples, n_features) Input data. classical_kernel : {"rbf", "linear"}, default="rbf" Classical reference kernel. gamma : float or None, default=None RBF bandwidth (None -> 1 / n_features, scikit-learn default). regularization : float, default=1e-6 Ridge for the classical-kernel inversion. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector backend. shots : int or None, default=None Measure against a finite-shot kernel estimate instead of the exact one. Shot noise inflates g — noise is a direction no classical kernel reproduces — so a large value under finite shots is not by itself evidence of an advantage. seed : int or None, default=None Seed for the shot sampling. Ignored when shots is None.

compute_gradient_variance

compute_gradient_variance(encoding: BaseEncoding, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, observable: Literal['computational', 'pauli_z', 'global_z'] = 'computational', seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> float

Compute the average variance of parameter gradients.

This is the core metric used for trainability estimation. It measures how much the gradients vary across random parameter initializations. Low variance indicates a flat optimization landscape (barren plateau).

This function is a convenience wrapper that calls :func:estimate_trainability and extracts only the gradient variance.

Parameters

encoding : BaseEncoding The encoding to analyze. n_samples : int, default=500 Number of random parameter samples. input_range : tuple[float, float], default=(0, 2π) Range for sampling random parameters. observable : {"computational", "pauli_z", "global_z"}, default="computational" Observable for gradient computation. seed : int, optional Random seed for reproducibility. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Backend for gradient computation.

Returns

float Average gradient variance across parameters. Values close to zero indicate a barren plateau.

Examples

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) variance = compute_gradient_variance(enc, n_samples=100, seed=42) print(f"Gradient variance: {variance:.2e}")

See Also

estimate_trainability : Full trainability analysis with risk assessment. detect_barren_plateau : Categorize variance into risk levels.

compute_haar_distribution

compute_haar_distribution(n_qubits: int, fidelity_values: NDArray[floating[Any]]) -> NDArray[floating[Any]]

Compute the Haar-random fidelity distribution.

For a d-dimensional Hilbert space (d = 2^n_qubits), the probability density of fidelities between Haar-random states is:

P_Haar(F) = (d - 1)(1 - F)^(d - 2)

This is derived from the fact that Haar-random states are uniformly distributed on the complex unit sphere.

Parameters

n_qubits : int Number of qubits (determines Hilbert space dimension d = 2^n_qubits). fidelity_values : NDArray[np.floating] Array of fidelity values at which to evaluate the distribution. Should be in [0, 1].

Returns

NDArray[np.floating] Probability density values at each fidelity point, normalized to sum to 1 (assuming fidelity_values are evenly spaced bin centers).

Raises

ValueError If n_qubits < 1.

Examples

import numpy as np fidelities = np.linspace(0, 1, 100) P_haar = compute_haar_distribution(n_qubits=4, fidelity_values=fidelities) print(f"P_Haar shape: {P_haar.shape}") print(f"P_Haar sum: {P_haar.sum():.4f}") # Should be close to 1

Notes

Physical Interpretation

The Haar distribution represents what we would observe if we picked two states uniformly at random from the Hilbert space. For large d:

  • Most pairs have very low fidelity (nearly orthogonal)
  • High fidelity pairs are exponentially rare
  • The distribution is concentrated near F = 0

Numerical Considerations

For large d (many qubits), the term (1-F)^(d-2) can cause underflow. We use log-space computation for d > 100 to maintain numerical stability.

Single Qubit Special Case

For d = 2 (single qubit), P_Haar(F) = 1 (uniform distribution), because the Bloch sphere has special geometry where all fidelities are equally likely.

References

.. [1] Życzkowski, K., & Sommers, H.-J. (2001). "Induced measures in the space of mixed quantum states." Journal of Physics A, 34(35), 7111.

compute_kernel_concentration

compute_kernel_concentration(encoding: BaseEncoding, X: NDArray[floating[Any]] | None = None, *, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, sampling: SamplingMethod = 'uniform', threshold: float = CONCENTRATION_THRESHOLD, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> ConcentrationResult

Measure how concentrated an encoding's fidelity kernel is.

Builds the fidelity kernel on X (or on random inputs when X is not supplied) and reduces it to the concentration order parameter, the spread of its off-diagonal entries, and the shot budget that spread implies.

A concentration_ratio near 1 means the kernel's off-diagonal variance has collapsed to the Haar floor: every pair of distinct inputs is equally dissimilar, the matrix is the identity up to sampling noise, and a kernel method built on it cannot generalize regardless of shot budget or noise level. Values well above 1 mean the encoding still separates inputs.

Parameters

encoding : BaseEncoding Encoding to analyse. X : ndarray of shape (n_samples, n_features), optional Inputs to build the kernel from. When None (default), inputs are drawn from input_range, making this a data-free axis comparable across encodings — the same convention as expressibility and entanglement capability. n_samples : int, default=40 Number of random inputs when X is not supplied. Must be >= 2. Ignored when X is given. input_range : (float, float), default=(0, 2*pi) Range for randomly sampled inputs. Concentration depends on the input distribution as well as the circuit; narrowing this range (as feature scaling does) generally reduces it. Ignored when X is given. sampling : {"uniform", "sobol"}, default="uniform" Random-input strategy. Ignored when X is given. threshold : float, default=2.0 concentration_ratio below which is_concentrated is set. seed : int or None, default=None Seed for input sampling. Ignored when X is given. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

ConcentrationResult The measured concentration statistics.

Raises

ValueError If n_samples, input_range, threshold or sampling is invalid, or if X is not a 2D array with at least two rows.

Warns

UserWarning If the encoding is wide enough that the statevector batch becomes memory-hungry (more than 14 qubits).

Examples

An entangling feature map sits at the Haar floor; a non-entangling one does not:

from encoding_atlas import AngleEncoding, IQPEncoding angle = compute_kernel_concentration( ... AngleEncoding(n_features=6), n_samples=30, seed=0 ... ) iqp = compute_kernel_concentration( ... IQPEncoding(n_features=6, reps=2), n_samples=30, seed=0 ... ) angle.concentration_ratio > iqp.concentration_ratio True

See Also

estimate_concentration_scaling : The same measurement across widths. encoding_atlas.analysis.generalization.compute_fidelity_kernel : The kernel.

compute_kernel_target_alignment

compute_kernel_target_alignment(encoding: BaseEncoding, X: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]], *, centered: bool = True, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', shots: int | None = None, seed: int | None = None) -> float

Kernel-target alignment of an encoding's fidelity kernel on (X, y).

Higher alignment predicts stronger quantum-kernel classification accuracy. Uses the centred variant by default.

Parameters

encoding : BaseEncoding Encoding under test. X : ndarray, shape (n_samples, n_features) Input data. y : ndarray, shape (n_samples,) Binary labels. centered : bool, default=True Use the centred alignment (Cortes et al., 2012). backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector backend. shots : int or None, default=None Measure the alignment on a finite-shot kernel estimate instead of the exact one. Alignment aggregates over all n(n-1)/2 entries, so it tolerates shot noise far better than the kernel matrix itself does. seed : int or None, default=None Seed for the shot sampling. Ignored when shots is None.

compute_linear_entropy

compute_linear_entropy(density_matrix: DensityMatrixType) -> float

Compute the linear entropy of a density matrix.

The linear entropy is a measure of mixedness defined as:

S_L(ρ) = 1 - Tr(ρ²) = 1 - γ(ρ)

where γ(ρ) is the purity.

Properties
  • For a pure state: S_L = 0
  • For a maximally mixed state of dimension d: S_L = 1 - 1/d
  • In general: 0 ≤ S_L ≤ 1 - 1/d
Parameters

density_matrix : DensityMatrixType Density matrix (must be square).

Returns

float Linear entropy value in the range [0, 1 - 1/d].

Examples

Pure state has zero linear entropy:

import numpy as np rho_pure = np.array([[1, 0], [0, 0]], dtype=complex) entropy = compute_linear_entropy(rho_pure) print(f"Linear entropy: {entropy}") Linear entropy: 0.0

Maximally mixed state has maximum linear entropy:

rho_mixed = np.array([[0.5, 0], [0, 0.5]], dtype=complex) entropy = compute_linear_entropy(rho_mixed) print(f"Linear entropy: {entropy}") Linear entropy: 0.5

See Also

compute_purity : Compute purity (1 - linear entropy). compute_von_neumann_entropy : Compute von Neumann entropy.

compute_meyer_wallach

compute_meyer_wallach(statevector: StatevectorType, n_qubits: int) -> float

Compute the Meyer-Wallach entanglement measure for a pure state.

The Meyer-Wallach measure Q(|psi>) quantifies global entanglement as the average linear entropy across all single-qubit reduced states:

Q(|psi>) = (2/n) * sum_i (1 - Tr(rho_i^2))

where rho_i is the reduced density matrix of qubit i.

Parameters

statevector : NDArray[np.complexfloating] Pure state vector, shape (2^n_qubits,). n_qubits : int Number of qubits in the system.

Returns

float Meyer-Wallach measure in [0, 1].

- 0 = completely separable (product state)
- 1 = maximally entangled
Raises

ValueError If statevector length doesn't match 2^n_qubits. ValidationError If statevector is invalid (NaN, zero norm, etc.).

Examples

GHZ state: (|000> + |111>) / sqrt(2) has MW = 1:

import numpy as np ghz = np.zeros(8, dtype=complex) ghz[0] = ghz[7] = 1.0 / np.sqrt(2) mw = compute_meyer_wallach(ghz, n_qubits=3) print(f"GHZ Meyer-Wallach: {mw:.4f}") GHZ Meyer-Wallach: 1.0000

Product state |000> has MW = 0:

product = np.zeros(8, dtype=complex) product[0] = 1.0 mw = compute_meyer_wallach(product, n_qubits=3) print(f"Product Meyer-Wallach: {mw:.4f}") Product Meyer-Wallach: 0.0000

Bell state (|00> + |11>) / sqrt(2) has MW = 1:

bell = np.zeros(4, dtype=complex) bell[0] = bell[3] = 1.0 / np.sqrt(2) mw = compute_meyer_wallach(bell, n_qubits=2) print(f"Bell Meyer-Wallach: {mw:.4f}") Bell Meyer-Wallach: 1.0000

Notes

The Meyer-Wallach measure is invariant under local unitary operations. It reaches its maximum value of 1 for maximally entangled states like the GHZ state (|00...0> + |11...1>) / sqrt(2).

See Also

compute_meyer_wallach_with_breakdown : Also returns per-qubit contributions. compute_scott_measure : Generalization to k-body reduced states.

compute_meyer_wallach_with_breakdown

compute_meyer_wallach_with_breakdown(statevector: StatevectorType, n_qubits: int) -> tuple[float, FloatArray]

Compute Meyer-Wallach measure with per-qubit breakdown.

This function returns both the total Meyer-Wallach measure and the contribution from each qubit, which is useful for understanding which qubits are most entangled.

Parameters

statevector : NDArray[np.complexfloating] Pure state vector, shape (2^n_qubits,). n_qubits : int Number of qubits in the system.

Returns

mw_value : float Total Meyer-Wallach measure in [0, 1]. per_qubit_entropy : NDArray[np.floating] Linear entropy (1 - purity) for each qubit's reduced state. Shape: (n_qubits,).

Raises

ValueError If statevector length doesn't match 2^n_qubits, or if n_qubits < 1. ValidationError If statevector is invalid.

Examples

import numpy as np

W state: (|001> + |010> + |100>) / sqrt(3)

w_state = np.zeros(8, dtype=complex) w_state[1] = w_state[2] = w_state[4] = 1.0 / np.sqrt(3) mw, per_qubit = compute_meyer_wallach_with_breakdown(w_state, n_qubits=3) print(f"Total MW: {mw:.4f}") print(f"Per-qubit entropies: {per_qubit}")

Notes

The per-qubit breakdown is useful for:

  1. Identifying which qubits contribute most to entanglement
  2. Detecting asymmetric entanglement structures
  3. Debugging encoding circuits

For the GHZ state, all qubits have equal entropy (= 0.5). For the W state, all qubits also have equal entropy (= ⅔).

compute_noise_resilience

compute_noise_resilience(encoding: BaseEncoding, *, noise_level: str = 'medium', noise_params: dict[str, float] | None = None, n_samples: int = 25, seed: int | None = None, feature_range: tuple[float, float] = _DEFAULT_RANGE) -> NoiseResilienceResult

Measure an encoding's resilience to depolarizing gate noise.

For n_samples random inputs, simulates the ideal state |phi(x)> and the noisy density matrix rho(x) and averages the fidelity F(x) = <phi(x)| rho(x) |phi(x)>. Higher retained fidelity means the encoding degrades less under noise.

Parameters

encoding : BaseEncoding Encoding to analyze (at most 12 qubits; density-matrix memory is O(4^n)). noise_level : {"low", "medium", "high"}, default="medium" Preset depolarizing error rates (ignored if noise_params is given). noise_params : dict or None, default=None Explicit {"single_qubit": p1, "two_qubit": p2} error probabilities, overriding noise_level. n_samples : int, default=25 Number of random inputs to average over. Depolarizing fidelity is nearly input-independent, so modest values are accurate. seed : int or None, default=None Seed for input sampling (reproducibility). feature_range : tuple, default=(0, 2*pi) Range for uniformly sampled input features.

Returns

NoiseResilienceResult Retained fidelity, fidelity decay, and per-sample statistics.

Raises

ValueError If n_samples < 1, the encoding exceeds the qubit cap, or the noise specification is invalid.

compute_parameter_gradient

compute_parameter_gradient(encoding: BaseEncoding, x: NDArray[floating[Any]], param_index: int, observable: Literal['computational', 'global_z', 'local_z', 'pauli_z'] = 'computational', backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> float

Compute gradient of expectation value with respect to one parameter.

Uses the parameter-shift rule for quantum gradients:

∂⟨O⟩/∂θ = (⟨O⟩_{θ+π/2} - ⟨O⟩_{θ-π/2}) / 2

This is exact for gates of the form U(θ) = exp(-iθG/2) where G² = I, which includes all standard rotation gates (RX, RY, RZ).

Parameters

encoding : BaseEncoding The encoding instance. x : NDArray[np.floating] Input data vector. param_index : int Index of the parameter to differentiate with respect to. observable : {"computational", "global_z", "local_z", "pauli_z"}, default="computational" Observable to measure:

- ``"computational"``: Probability of |0...0⟩ state, i.e., ⟨0...0|ρ|0...0⟩
- ``"global_z"``: Expectation value of Z⊗Z⊗...⊗Z (tensor product of Z on
  all qubits). Eigenvalue is (-1)^(number of 1s in bitstring).
- ``"local_z"``: Expectation value of Z on first qubit only, ⟨Z₀⟩.
  Computed as P(0 on qubit 0) - P(1 on qubit 0) = 2·P(0) - 1.
- ``"pauli_z"``: **Deprecated alias for "global_z"**. For clarity,
  use ``"global_z"`` for the global Z-string observable or ``"local_z"``
  for the single-qubit Z observable.
{"pennylane", "qiskit", "cirq"}, default="pennylane"

Simulation backend.

Returns

float Gradient of the expectation value.

Raises

ValidationError If param_index is out of range. ValueError If observable is not a recognized option.

Examples

from encoding_atlas import AngleEncoding import numpy as np

enc = AngleEncoding(n_features=2) x = np.array([0.5, 1.0]) grad = compute_parameter_gradient(enc, x, param_index=0) print(f"Gradient: {grad:.6f}")

Notes

The parameter-shift rule requires two circuit evaluations per gradient component. For full gradient vectors, use :func:compute_all_parameter_gradients.

Observable Semantics

The distinction between "global_z" and "local_z" is important:

  • "global_z" measures the parity of all qubits simultaneously. The expectation value is ⟨Z⊗Z⊗...⊗Z⟩ where each Z acts on a different qubit. This captures global correlations in the encoded state.
  • "local_z" measures only the first qubit (MSB in computational basis ordering), ignoring the state of other qubits. This is useful for local sensitivity analysis.
See Also

compute_all_parameter_gradients : Compute gradients for all parameters.

compute_purity

compute_purity(density_matrix: DensityMatrixType) -> float

Compute the purity of a density matrix.

The purity of a quantum state ρ is defined as:

γ(ρ) = Tr(ρ²)
Properties
  • For a pure state: γ = 1
  • For a maximally mixed state of dimension d: γ = 1/d
  • In general: 1/d ≤ γ ≤ 1
Parameters

density_matrix : DensityMatrixType Density matrix (must be square). Can be any valid quantum state, including mixed states.

Returns

float Purity value in the range [1/d, 1] where d is the dimension.

Raises

ValidationError If the density matrix is not square or contains invalid values.

Examples

Pure state |0⟩ has purity 1:

import numpy as np rho_pure = np.array([[1, 0], [0, 0]], dtype=complex) purity = compute_purity(rho_pure) print(f"Purity: {purity}") Purity: 1.0

Maximally mixed state has purity 1/d:

rho_mixed = np.array([[0.5, 0], [0, 0.5]], dtype=complex) purity = compute_purity(rho_mixed) print(f"Purity: {purity}") Purity: 0.5

Notes

The purity is related to the linear entropy by: S_L(ρ) = 1 - γ(ρ)

And to the participation ratio by

PR = 1/γ(ρ)

which measures the "effective dimension" of the state.

See Also

compute_linear_entropy : Compute linear entropy from purity. compute_fidelity : Compute fidelity between pure states.

compute_scott_measure

compute_scott_measure(statevector: StatevectorType, n_qubits: int, k: int = 2) -> float

Compute the Scott entanglement measure.

The Scott measure generalizes Meyer-Wallach to k-body reduced states, averaging the linear entropy over all k-qubit subsystems.

Parameters

statevector : NDArray[np.complexfloating] Pure state vector. n_qubits : int Number of qubits. k : int, default=2 Size of subsystems to consider. Must satisfy 1 <= k <= n_qubits - 1.

- k=1: Equivalent to Meyer-Wallach measure.
- k=2: Pairwise entanglement (default).
- Higher k: Captures higher-order entanglement structure.

The measure is well-defined for any proper subsystem size.
Note that ``Q_k`` and ``Q_{n-k}`` probe complementary subsystems
and therefore carry related (but not identical) information.
Returns

float Scott measure in [0, 1].

Raises

ValueError If k < 1, k >= n_qubits, or other inputs are invalid. ValidationError If statevector is invalid.

Examples

import numpy as np

GHZ state on 3 qubits

ghz = np.zeros(8, dtype=complex) ghz[0] = ghz[7] = 1.0 / np.sqrt(2) scott_1 = compute_scott_measure(ghz, n_qubits=3, k=1) scott_2 = compute_scott_measure(ghz, n_qubits=3, k=2) print(f"Scott k=1: {scott_1:.4f}") # Same as Meyer-Wallach Scott k=1: 1.0000 print(f"Scott k=2: {scott_2:.4f}") Scott k=2: 0.6667

Notes

The Scott measure averages the normalized linear entropy of all :math:inom{n}{k} reduced density matrices of size k. The mathematical definition is valid for any 1 <= k <= n - 1, where n is the number of qubits [1]_.

Computational cost scales with :math:inom{n}{k}, the number of k-qubit subsystems. For large n, choosing k close to 1 or close to n - 1 is cheaper than choosing k near n / 2.

References

.. [1] Scott, A. J. (2004). "Multipartite entanglement, quantum-error-correcting codes, and entangling power of quantum evolutions." Physical Review A, 69(5), 052330.

compute_von_neumann_entropy

compute_von_neumann_entropy(density_matrix: DensityMatrixType) -> float

Compute the von Neumann entropy of a density matrix.

The von Neumann entropy is the quantum analog of Shannon entropy:

S(ρ) = -Tr(ρ log₂ ρ) = -Σᵢ λᵢ log₂(λᵢ)

where λᵢ are the eigenvalues of ρ.

Properties
  • For a pure state: S = 0
  • For a maximally mixed state of dimension d: S = log₂(d)
  • In general: 0 ≤ S ≤ log₂(d)
Parameters

density_matrix : DensityMatrixType Density matrix (must be square).

Returns

float Von Neumann entropy in bits (log base 2).

Examples

Pure state has zero entropy:

import numpy as np rho_pure = np.array([[1, 0], [0, 0]], dtype=complex) entropy = compute_von_neumann_entropy(rho_pure) print(f"von Neumann entropy: {entropy:.6f}") von Neumann entropy: 0.000000

Maximally mixed state has maximum entropy:

rho_mixed = np.array([[0.5, 0], [0, 0.5]], dtype=complex) entropy = compute_von_neumann_entropy(rho_mixed) print(f"von Neumann entropy: {entropy:.6f}") von Neumann entropy: 1.000000

Notes

The computation uses the eigenvalue decomposition of the density matrix. Eigenvalues that are zero or negative (due to numerical errors) are filtered out to avoid log(0) issues.

See Also

compute_linear_entropy : Compute linear entropy (simpler approximation). compute_purity : Compute state purity.

count_resources

count_resources(encoding: BaseEncoding, x: NDArray[floating[Any]] | None = None, *, detailed: Literal[False] = False) -> ResourceCountSummary
count_resources(encoding: BaseEncoding, x: NDArray[floating[Any]] | None = None, *, detailed: Literal[True]) -> DetailedGateBreakdown
count_resources(encoding: BaseEncoding, x: NDArray[floating[Any]] | None = None, *, detailed: bool = False) -> ResourceCountSummary | DetailedGateBreakdown

Count the computational resources required by an encoding.

Analyzes the encoding circuit structure to determine gate counts, circuit depth, and other resource metrics. This analysis is performed analytically (without simulation) for most encodings.

Parameters

encoding : BaseEncoding The encoding instance to analyze. Must be a valid encoding that inherits from :class:BaseEncoding. x : NDArray[np.floating], optional Input data vector. Required only for data-dependent encodings (e.g., BasisEncoding) where gate count depends on input values. For most encodings, this can be omitted. detailed : bool, default=False If True, return detailed gate-by-gate breakdown. If False, return summary with totals and derived metrics.

Returns

ResourceCountSummary or DetailedGateBreakdown If detailed=False (default): Dictionary with summary metrics including:

- ``n_qubits``: Number of qubits
- ``depth``: Circuit depth
- ``gate_count``: Total gates
- ``single_qubit_gates``: Single-qubit gate count
- ``two_qubit_gates``: Two-qubit gate count
- ``parameter_count``: Number of parameterized gates
- ``two_qubit_ratio``: Fraction of gates that are two-qubit
- ``gates_per_qubit``: Average gates per qubit
- ``encoding_name``: Name of the encoding class
- ``is_data_dependent``: Whether counts depend on input data

If ``detailed=True``: Dictionary with per-gate-type counts including:

- ``rx``, ``ry``, ``rz``: Rotation gate counts
- ``h``: Hadamard count
- ``x``, ``y``, ``z``: Pauli gate counts
- ``cnot``, ``cz``, ``swap``: Two-qubit gate counts
- ``total_single_qubit``, ``total_two_qubit``, ``total``
- ``encoding_name``: Name of the encoding class
Raises

AnalysisError If encoding is not a valid BaseEncoding instance or if resource counting fails. ValidationError If x is required but not provided (for data-dependent encodings), or if x has invalid shape or values.

Examples

Basic resource counting:

from encoding_atlas import AngleEncoding, IQPEncoding from encoding_atlas.analysis import count_resources

enc = AngleEncoding(n_features=4) res = count_resources(enc) print(f"Qubits: {res['n_qubits']}, Depth: {res['depth']}") Qubits: 4, Depth: 1

Comparing two encodings:

enc_simple = AngleEncoding(n_features=4) enc_complex = IQPEncoding(n_features=4, reps=2)

res_simple = count_resources(enc_simple) res_complex = count_resources(enc_complex)

print(f"Simple: {res_simple['gate_count']} gates") print(f"Complex: {res_complex['gate_count']} gates")

Detailed breakdown:

breakdown = count_resources(enc_complex, detailed=True) print(f"CNOT gates: {breakdown['cnot']}") print(f"RZ gates: {breakdown['rz']}")

Data-dependent encoding:

from encoding_atlas import BasisEncoding import numpy as np

enc = BasisEncoding(n_features=4) x = np.array([0.1, 0.9, 0.3, 0.8]) # Will be binarized res = count_resources(enc, x=x) print(f"X gates (depends on input): {res['gate_count']}")

Notes

For most encodings, resource counts are computed analytically from encoding parameters (n_features, reps, entanglement pattern, etc.). This is fast and deterministic.

For data-dependent encodings like BasisEncoding, the gate count depends on the specific input values (e.g., number of 1s in binary input determines number of X gates).

See Also

get_resource_summary : Get summary from encoding properties. get_gate_breakdown : Get detailed gate-by-gate breakdown. compare_resources : Compare resources between multiple encodings. check_simulability : Check if encoding is classically simulable.

create_rng

create_rng(seed: int | None = None) -> Generator

Create a numpy random number generator.

Parameters

seed : int, optional Random seed for reproducibility. If None, uses system entropy.

Returns

Generator NumPy random number generator instance.

Examples

rng = create_rng(42) values = rng.random(5)

Same seed produces same values

rng2 = create_rng(42) values2 = rng2.random(5) np.allclose(values, values2) True

detect_barren_plateau

detect_barren_plateau(gradient_variance: float, n_qubits: int, n_params: int) -> Literal['low', 'medium', 'high']

Detect barren plateau risk based on gradient variance.

Compares observed gradient variance to theoretical scaling for barren plateaus (exponential decay with qubit count) and returns a categorical risk assessment.

The thresholds are adjusted based on system size to account for the natural scaling of gradient variance with qubit count.

Parameters

gradient_variance : float Observed gradient variance from gradient sampling. Must be non-negative. n_qubits : int Number of qubits in the circuit. Used to adjust thresholds based on expected scaling. n_params : int Number of parameters in the circuit. Currently used for logging but may be used for more sophisticated detection in future versions.

Returns

{"low", "medium", "high"} Barren plateau risk level:

- "low": Variance is healthy, gradient-based training should work
- "medium": Variance is borderline, may need careful tuning
- "high": Variance is very low, likely barren plateau
Notes

Risk Threshold Calibration:

The risk thresholds are calibrated based on literature values:

  • McClean et al. (2018) showed variance scales as O(½ⁿ) for random circuits with global cost functions
  • Cerezo et al. (2021) showed local cost functions have better scaling: O(1/n) for certain circuit architectures

The thresholds are adjusted as follows:

  • Base high-risk threshold: 1e-6
  • Base medium-risk threshold: 1e-3
  • Size adjustment: thresholds scale with 2^(-n) to account for natural variance decay

Practical Interpretation:

  • "low" risk: Proceed with standard optimization
  • "medium" risk: Consider using:
  • Adaptive learning rates
  • Better parameter initialization (e.g., identity-initialized)
  • Noise injection during training
  • "high" risk: Fundamental changes needed:
  • Reduce circuit depth
  • Use local cost functions
  • Try hardware-efficient ansatze
  • Consider layer-wise training
Examples

risk = detect_barren_plateau( ... gradient_variance=1e-2, ... n_qubits=4, ... n_params=16 ... ) print(f"Risk level: {risk}") Risk level: low

risk = detect_barren_plateau( ... gradient_variance=1e-8, ... n_qubits=10, ... n_params=40 ... ) print(f"Risk level: {risk}") Risk level: high

See Also

estimate_trainability : Complete trainability analysis. compute_gradient_variance : Compute gradient variance.

estimate_concentration_scaling

estimate_concentration_scaling(factory: Callable[[int], BaseEncoding], *, feature_counts: tuple[int, ...] = _DEFAULT_FEATURE_COUNTS, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, sampling: SamplingMethod = 'uniform', threshold: float = CONCENTRATION_THRESHOLD, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> ScalingResult

Measure kernel concentration across a range of circuit widths.

Calls factory(n_features) for each requested feature count, measures the concentration at each resulting width, and fits a log-linear decay model. Feature counts the factory rejects (an encoding requiring exactly two features, or an even count) are recorded in :attr:ScalingResult.skipped rather than aborting the sweep.

The fit is against the encoding's qubit count, not its feature count, because concentration is a statement about Hilbert-space dimension. The distinction matters for amplitude encoding, where n_qubits = ceil(log2(n_features)).

Parameters

factory : callable n_features -> BaseEncoding, e.g. lambda d: IQPEncoding(n_features=d, reps=2). feature_counts : tuple[int, ...], default=(2, 4, 6, 8) Feature counts to sweep. Must be non-empty and all positive. n_samples : int, default=40 Random inputs per width. Must be >= 2. input_range : (float, float), default=(0, 2*pi) Range for randomly sampled inputs. sampling : {"uniform", "sobol"}, default="uniform" Random-input strategy. threshold : float, default=2.0 concentration_ratio defining the Haar floor. seed : int or None, default=None Base seed. Width i uses seed + i, so widths are independent draws yet the whole sweep is reproducible. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

ScalingResult Per-width measurements plus the fitted decay rates, Haar-normalized slope, and fit quality.

Raises

ValueError If feature_counts is empty or contains non-positive values, or if any shared argument is invalid. RuntimeError If the factory failed for every requested feature count; the per-count reasons are included in the message.

Examples

from encoding_atlas import IQPEncoding scaling = estimate_concentration_scaling( ... lambda d: IQPEncoding(n_features=d, reps=2), ... feature_counts=(2, 4), ... n_samples=12, ... seed=0, ... ) len(scaling.results) 2

See Also

compute_kernel_concentration : Single-width measurement.

estimate_entanglement_bound

estimate_entanglement_bound(encoding: BaseEncoding, n_samples: int = 100, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> float

Estimate upper bound on entanglement entropy.

Samples random inputs and estimates the maximum entanglement entropy across the middle bipartition. Low entanglement suggests the circuit may be simulable using tensor network methods.

The entanglement entropy is computed as the von Neumann entropy of the reduced density matrix for one half of the system:

S = -Tr(ρ_A log₂ ρ_A)

where ρ_A is obtained by tracing out the other half.

Parameters

encoding : BaseEncoding The encoding to analyze. n_samples : int, default=100 Number of random inputs to sample. More samples give more reliable estimates but take longer. seed : int, optional Random seed for reproducibility. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Backend to use for circuit simulation.

- ``"pennylane"``: Uses PennyLane's default.qubit simulator (recommended)
- ``"qiskit"``: Uses Qiskit's Statevector class
- ``"cirq"``: Uses Cirq's Simulator for statevector simulation
Returns

float Estimated maximum entanglement entropy (in bits). For n qubits, the maximum possible value is n/2 bits (achieved by maximally entangled states).

Raises

AnalysisError If encoding is not valid or simulation fails. ValueError If an unknown backend is specified.

Warnings

UserWarning If the encoding has more than 25 qubits, a warning is issued about memory usage for simulation.

Notes

This is a statistical estimate based on random sampling. The true maximum may be higher for adversarial inputs not sampled.

For non-entangling encodings, this will return 0.0.

For tensor network simulability, entanglement entropy bounded by O(log n) suggests efficient MPS simulation is possible.

Examples

from encoding_atlas import AngleEncoding, IQPEncoding

Non-entangling encoding has zero entanglement

enc = AngleEncoding(n_features=4) entropy = estimate_entanglement_bound(enc, n_samples=50, seed=42) entropy < 0.01 # Essentially zero True

Entangling encoding has non-zero entanglement

enc = IQPEncoding(n_features=4, reps=2) entropy = estimate_entanglement_bound(enc, n_samples=50, seed=42) entropy > 0.1 True

Using Cirq backend

entropy_cirq = estimate_entanglement_bound(enc, n_samples=50, seed=42, backend="cirq") entropy_cirq > 0.1 True

See Also

check_simulability : Full simulability analysis. compute_entanglement_capability : Related entanglement measure.

estimate_execution_time

estimate_execution_time(encoding: BaseEncoding, *, single_qubit_gate_time_us: float = _DEFAULT_SINGLE_QUBIT_GATE_TIME_US, two_qubit_gate_time_us: float = _DEFAULT_TWO_QUBIT_GATE_TIME_US, measurement_time_us: float = _DEFAULT_MEASUREMENT_TIME_US, include_measurement: bool = True, parallelization_factor: float = 0.5) -> dict[str, float]

Estimate execution time for an encoding circuit.

Provides a rough estimate of circuit execution time based on gate counts and typical gate times. This is useful for:

  • Comparing relative execution times between encodings
  • Planning batch processing
  • Evaluating hardware constraints
Parameters

encoding : BaseEncoding The encoding to analyze. single_qubit_gate_time_us : float, default=0.02 Time for single-qubit gates in microseconds. Default is 20 ns, typical for superconducting qubits. two_qubit_gate_time_us : float, default=0.2 Time for two-qubit gates in microseconds. Default is 200 ns, typical for superconducting qubits. measurement_time_us : float, default=1.0 Time for measurement in microseconds. include_measurement : bool, default=True Whether to include measurement time in the estimate. parallelization_factor : float, default=0.5 Fraction of gates assumed to run in parallel. 0 = fully serial, 1 = fully parallel. Default of 0.5 assumes moderate parallelism.

Returns

dict[str, float] Dictionary containing:

- ``serial_time_us``: Time assuming no parallelization
- ``estimated_time_us``: Time with parallelization factor
- ``single_qubit_time_us``: Time for single-qubit gates only
- ``two_qubit_time_us``: Time for two-qubit gates only
- ``measurement_time_us``: Measurement time (if included)
- ``parallelization_factor``: The factor used
Examples

from encoding_atlas import IQPEncoding from encoding_atlas.analysis import estimate_execution_time

enc = IQPEncoding(n_features=4, reps=2) times = estimate_execution_time(enc) print(f"Estimated time: {times['estimated_time_us']:.2f} μs")

With custom gate times (trapped ions):

times = estimate_execution_time( ... enc, ... single_qubit_gate_time_us=1.0, # 1 μs ... two_qubit_gate_time_us=100.0, # 100 μs ... )

Notes

This is a rough estimate based on gate counts and does not account for:

  • Circuit topology and routing overhead
  • Hardware-specific gate implementations
  • Error correction overhead
  • Classical control latency

For accurate timing, run the circuit on actual hardware or use a detailed hardware simulator.

The parallelization factor accounts for the fact that independent gates can run in parallel. A factor of 0.5 means the effective time is 50% of the fully serial time.

estimate_trainability

estimate_trainability(encoding: BaseEncoding, n_samples: int = ..., input_range: tuple[float, float] = ..., observable: Literal['computational', 'pauli_z', 'global_z'] = ..., seed: int | None = ..., backend: Literal['pennylane', 'qiskit', 'cirq'] = ..., return_details: Literal[False] = ..., verbose: bool = ..., parallel: ParallelArg = ..., max_workers: int | None = ..., sampling: SamplingMethod = ..., confidence_level: float = ..., n_bootstrap_ci: int = ...) -> float
estimate_trainability(encoding: BaseEncoding, n_samples: int = ..., input_range: tuple[float, float] = ..., observable: Literal['computational', 'pauli_z', 'global_z'] = ..., seed: int | None = ..., backend: Literal['pennylane', 'qiskit', 'cirq'] = ..., return_details: Literal[True] = ..., verbose: bool = ..., parallel: ParallelArg = ..., max_workers: int | None = ..., sampling: SamplingMethod = ..., confidence_level: float = ..., n_bootstrap_ci: int = ...) -> TrainabilityResult
estimate_trainability(encoding: BaseEncoding, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, observable: Literal['computational', 'pauli_z', 'global_z'] = 'computational', seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane', return_details: bool = False, verbose: bool = False, parallel: ParallelArg = False, max_workers: int | None = None, sampling: SamplingMethod = 'uniform', confidence_level: float = _DEFAULT_CONFIDENCE_LEVEL, n_bootstrap_ci: int = _DEFAULT_N_BOOTSTRAP_CI) -> float | TrainabilityResult

Estimate the trainability of a quantum encoding.

Trainability is estimated by analyzing the variance of gradients with respect to encoding parameters. Low variance (especially exponentially decaying with system size) indicates barren plateaus and poor trainability.

This function samples random parameter initializations, computes gradients using the parameter-shift rule, and analyzes the variance of these gradients to detect barren plateaus.

Parameters

encoding : BaseEncoding The encoding instance to analyze. Must be a valid encoding that implements the get_circuit() method. n_samples : int, default=500 Number of random parameter initializations to sample. Higher values give more accurate estimates but take longer to compute. Minimum value is 10, recommended minimum is 50. input_range : tuple[float, float], default=(0, 2π) Range for sampling random input/parameter values as (min, max). The default range covers full rotation angles for quantum gates. observable : {"computational", "pauli_z", "global_z"}, default="computational" Observable to use for gradient computation:

- ``"computational"``: Probability of |0...0⟩ state (local)
- ``"pauli_z"``: Expectation value of Z on first qubit (local)
- ``"global_z"``: Expectation value of Z⊗Z⊗...⊗Z (global)

Local observables are less prone to false positives for barren
plateau detection and are recommended for most use cases.

seed : int, optional Random seed for reproducibility. If None, uses system entropy. For reproducible results, always specify a seed. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Backend for circuit simulation and gradient computation:

- ``"pennylane"``: Uses PennyLane's default.qubit simulator
- ``"qiskit"``: Uses Qiskit's Statevector simulator
- ``"cirq"``: Uses Cirq's Simulator for statevector simulation

return_details : bool, default=False If True, return full result dictionary with detailed statistics. If False, return only the trainability estimate as a float. verbose : bool, default=False If True, log progress information during computation. parallel : bool or {'thread', 'process'}, default=False Parallel-dispatch mode for the per-sample gradient computation. Trainability is the most expensive of the three analyses (2 * n_features simulations per sample for the parameter-shift rule), so parallelism delivers the largest wall-clock speedup here.

- ``False`` (default) — sequential, no executor overhead.
- ``True`` or ``'thread'`` — :class:`ThreadPoolExecutor`.
- ``'process'`` — :class:`ProcessPoolExecutor` with the encoding
  pickled once per worker. Workers exchange only NumPy float
  arrays (gradient vectors), so process-pool parallelism works
  with **all** three backends here.

Output is numerically identical across all modes for a fixed
``seed`` — the RNG is fully consumed in the main process before
any work is dispatched. Failed samples are still counted (so the
``failure_fraction`` check produces the same result as
sequential), and successful gradients are stored contiguously
from index 0 just like the sequential version.

max_workers : int or None, default=None Maximum number of workers when parallel is enabled. sampling : {'uniform', 'sobol'}, default='uniform' Strategy for drawing the random per-sample inputs:

- ``'uniform'`` (default, unchanged): pseudo-random i.i.d.
  uniform draws.
- ``'sobol'``: Sobol' low-discrepancy quasi-random sequence,
  seeded from the same ``seed`` argument. Typically gives the
  same variance accuracy with **30-50% fewer samples**. For
  best statistical properties choose ``n_samples`` as a power
  of two.

confidence_level : float, default=0.95 Two-sided confidence level for the bootstrap CIs on trainability_estimate and gradient_variance reported in the result dict (only used when return_details=True). Must lie strictly in (0, 1). n_bootstrap_ci : int, default=200 Number of bootstrap resamples used to estimate the percentile CIs (only when return_details=True). 200 stabilizes the percentile endpoints to ~1% jitter at negligible cost.

Returns

float or TrainabilityResult If return_details=False: Trainability estimate in [0, 1], where higher values indicate better trainability.

If ``return_details=True``: Dictionary containing:

- ``trainability_estimate``: float in [0, 1]
- ``gradient_variance``: variance computed over successful samples only
- ``barren_plateau_risk``: "low", "medium", or "high"
- ``effective_dimension``: estimated effective parameter dimension
- ``n_samples``: total number of samples requested
- ``n_successful_samples``: samples used for variance computation
- ``per_parameter_variance``: variance for each parameter
- ``n_failed_samples``: number of failed gradient computations
Raises

InsufficientSamplesError If n_samples < 10. SimulationError If too many gradient computations fail (> 20% of samples). AnalysisError If the encoding is invalid or cannot be analyzed.

Warnings

UserWarning If n_samples < 50, a warning is issued about potential statistical unreliability.

Notes

Interpretation:

The trainability score should be interpreted as a relative measure for comparing encodings, not an absolute guarantee. A score of 0.8 means the encoding appears more trainable than one with score 0.5, but actual training success depends on many other factors.

Barren Plateau Risk Levels:

  • "low" (variance ≥ 1e-3): Training should proceed normally
  • "medium" (1e-6 ≤ variance < 1e-3): May need careful tuning
  • "high" (variance < 1e-6): Likely barren plateau, consider:
  • Using fewer qubits
  • Shallower circuits
  • Local cost functions
  • Parameter initialization strategies

Computational Cost:

Each sample requires computing gradients for all parameters, which involves 2 × n_parameters circuit evaluations (parameter-shift rule). Total circuit evaluations = 2 × n_parameters × n_samples.

Failed Sample Handling:

Gradient computation may fail for certain parameter configurations due to numerical instabilities or backend issues. Failed samples are completely excluded from variance computation to ensure unbiased statistical estimates. If more than 20% of samples fail, a :exc:SimulationError is raised. The number of successful samples used is reported in n_successful_samples when return_details=True.

Best Practices:

  1. Use at least 100 samples for reliable estimates
  2. Compare encodings using the same seed for fair comparison
  3. Use local observables ("computational" or "pauli_z") unless specifically studying global cost functions
  4. For publication, report both trainability score and gradient variance with confidence intervals
Examples

Basic usage:

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) train = estimate_trainability(enc, seed=42) print(f"Trainability: {train:.4f}")

Detailed analysis:

result = estimate_trainability( ... enc, n_samples=200, seed=42, return_details=True ... ) print(f"Variance: {result['gradient_variance']:.2e}") print(f"Risk level: {result['barren_plateau_risk']}")

Comparing encodings:

from encoding_atlas import IQPEncoding enc1 = AngleEncoding(n_features=4) enc2 = IQPEncoding(n_features=4, reps=2) t1 = estimate_trainability(enc1, seed=42) t2 = estimate_trainability(enc2, seed=42) print(f"Angle: {t1:.4f}, IQP: {t2:.4f}")

See Also

compute_gradient_variance : Compute only gradient variance. detect_barren_plateau : Detect barren plateau from variance.

generate_random_parameters

generate_random_parameters(encoding_or_n_features: BaseEncoding | int, n_samples: int = 1, param_min: float = _DEFAULT_PARAM_MIN, param_max: float = _DEFAULT_PARAM_MAX, seed: int | None = None) -> FloatArray

Generate random parameter vectors for encoding analysis.

Parameters

encoding_or_n_features : BaseEncoding or int Either an encoding instance (n_features is extracted automatically) or an integer specifying the number of features directly. n_samples : int, default=1 Number of parameter vectors to generate. param_min : float, default=0.0 Minimum parameter value. param_max : float, default=2π Maximum parameter value. seed : int, optional Random seed for reproducibility.

Returns

FloatArray Random parameters of shape (n_samples, n_features) if n_samples > 1, or (n_features,) if n_samples == 1.

Examples

Generate parameters from an encoding:

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) params = generate_random_parameters(enc, n_samples=10, seed=42) print(params.shape) (10, 4)

Generate parameters by specifying n_features directly:

params = generate_random_parameters(4, seed=42) print(params.shape) (4,)

With custom range:

params = generate_random_parameters( ... 4, n_samples=10, param_min=-np.pi, param_max=np.pi ... )

geometric_difference

geometric_difference(K1: NDArray[floating[Any]], K2: NDArray[floating[Any]], *, regularization: float = 1e-06) -> float

Asymmetric geometric difference g(K1 || K2) (Huang et al., 2021).

g(K1 || K2) = sqrt( || sqrt(K2) (K1 + reg I)^-1 sqrt(K2) ||_inf ) where ||.||_inf is the spectral norm and both kernels are trace-normalised to n. It quantifies how well a model expressed in K1's space can reproduce K2's geometry: g is large when K2 explores directions K1 cannot. g(K, K) = 1 (in the reg -> 0 limit).

Parameters

K1, K2 : ndarray, shape (n, n) Kernel matrices (symmetric PSD). K1 is the reference (inverted); for a quantum-advantage diagnostic use K1 = classical and K2 = quantum. regularization : float, default=1e-6 Ridge added to K1 before inversion for numerical stability.

get_gate_breakdown

get_gate_breakdown(encoding: BaseEncoding, x: NDArray[floating[Any]] | None = None) -> DetailedGateBreakdown

Get detailed gate-by-gate breakdown for an encoding.

This is a convenience function that calls :func:count_resources with detailed=True.

Parameters

encoding : BaseEncoding The encoding to analyze. x : NDArray[np.floating], optional Input data for data-dependent encodings. Required for encodings like BasisEncoding where gate count depends on input values.

Returns

DetailedGateBreakdown Dictionary with counts for each gate type.

Raises

AnalysisError If encoding is invalid. ValidationError If x is required but not provided or has invalid values.

Examples

from encoding_atlas import IQPEncoding from encoding_atlas.analysis import get_gate_breakdown

enc = IQPEncoding(n_features=4, reps=2) breakdown = get_gate_breakdown(enc) print(f"CNOT gates: {breakdown['cnot']}") print(f"Hadamard gates: {breakdown['h']}") print(f"Total: {breakdown['total']}")

See Also

count_resources : Full resource counting with summary option. get_resource_summary : Quick summary from cached properties.

get_resource_summary

get_resource_summary(encoding: BaseEncoding) -> ResourceCountSummary

Get a quick resource summary from encoding properties.

This is a lightweight alternative to :func:count_resources that uses cached encoding properties when available. It always returns theoretical (worst-case) values, even for data-dependent encodings.

Parameters

encoding : BaseEncoding The encoding to summarize.

Returns

ResourceCountSummary Dictionary with resource metrics. See Warnings section for details on which fields contain approximations.

Raises

AnalysisError If encoding is not a valid BaseEncoding instance.

Warnings

Approximated Fields: The following fields are approximations based on aggregate property values, not actual gate-by-gate circuit analysis:

  • cnot_count: Approximated as two_qubit_gates. This assumes all two-qubit gates are CNOTs, which may overcount if the encoding uses CZ or SWAP gates instead.
  • cz_count: Always returns 0 (requires detailed circuit analysis).
  • t_gate_count: Always returns 0 (requires detailed circuit analysis).
  • hadamard_count: Always returns 0 (requires detailed circuit analysis).
  • rotation_gates: Approximated as single_qubit_gates. This assumes all single-qubit gates are rotation gates (RX, RY, RZ), which may overcount if the encoding uses non-rotation gates like H, X, Y, Z, S, T.

For accurate gate-type counts, use :func:count_resources with detailed=True, which performs actual gate-by-gate analysis via the encoding's gate_count_breakdown() method when available.

Examples

from encoding_atlas import IQPEncoding from encoding_atlas.analysis import get_resource_summary

enc = IQPEncoding(n_features=4, reps=2) summary = get_resource_summary(enc) print(f"Qubits: {summary['n_qubits']}, Depth: {summary['depth']}")

For accurate gate counts, prefer count_resources with detailed=True:

from encoding_atlas.analysis import count_resources detailed = count_resources(enc, detailed=True) print(f"Actual Hadamard gates: {detailed['h']}")

Notes

This function uses the properties attribute of the encoding, which is typically cached after first access. This makes it very fast for repeated calls.

For data-dependent encodings, this returns worst-case (maximum) gate counts. Use :func:count_resources with input data for actual counts.

See Also

count_resources : Full resource counting with detailed options. get_gate_breakdown : Get detailed gate-by-gate breakdown.

get_simulability_reason

get_simulability_reason(encoding: BaseEncoding) -> str

Get a concise explanation of why an encoding is or isn't simulable.

This is a convenience function that provides a quick summary without the full details returned by :func:check_simulability.

Parameters

encoding : BaseEncoding The encoding to analyze.

Returns

str Human-readable explanation of simulability status.

Raises

AnalysisError If encoding is not a valid BaseEncoding instance.

Examples

from encoding_atlas import AngleEncoding, IQPEncoding enc = AngleEncoding(n_features=4) print(get_simulability_reason(enc)) Simulable: Encoding produces only product states (no entanglement)

enc = IQPEncoding(n_features=4, reps=2) reason = get_simulability_reason(enc) print(reason.startswith("Not simulable:")) True

See Also

check_simulability : Get full simulability analysis with details.

haar_kernel_moments

haar_kernel_moments(n_qubits: int) -> tuple[float, float]

Return the Haar-random (mean, variance) of the fidelity kernel.

For two independent Haar-random pure states in dimension d = 2**n_qubits the overlap |<psi|phi>|^2 follows Beta(1, d - 1), giving

mean = 1 / d ,    variance = (d - 1) / (d^2 (d + 1)) .

The variance is the floor an encoding's kernel decays towards; it is the reference for :attr:ConcentrationResult.concentration_ratio.

Parameters

n_qubits : int Circuit width. Must be a positive integer.

Returns

(float, float) The Haar mean and variance of a single off-diagonal kernel entry.

Raises

ValueError If n_qubits is not a positive integer.

Examples

mean, variance = haar_kernel_moments(4) round(mean, 6) 0.0625 round(variance, 8) 0.00344669

is_clifford_circuit

is_clifford_circuit(encoding: BaseEncoding) -> bool

Check if an encoding uses only Clifford gates.

Clifford circuits can be efficiently simulated using the stabilizer formalism, as proven by the Gottesman-Knill theorem. The Clifford gate set includes: H, S, CNOT, CZ, and Pauli gates (X, Y, Z).

Parameters

encoding : BaseEncoding The encoding to check.

Returns

bool True if the circuit is believed to use only Clifford gates.

Raises

AnalysisError If encoding is not a valid BaseEncoding instance.

Notes

This is a conservative check based on encoding properties and known gate sets. Some encodings may be classified as non-Clifford even if specific parameter choices yield Clifford circuits.

For example, RZ(π/2) is equivalent to S (a Clifford gate), but general RZ(θ) rotations are non-Clifford.

Examples

from encoding_atlas import AngleEncoding, BasisEncoding

AngleEncoding uses parameterized rotations (non-Clifford in general)

enc = AngleEncoding(n_features=4) is_clifford_circuit(enc) False

See Also

check_simulability : Full simulability analysis.

is_matchgate_circuit

is_matchgate_circuit(encoding: BaseEncoding) -> bool

Check if an encoding uses only matchgate operations.

Matchgate circuits with nearest-neighbor connectivity on a line topology can be efficiently simulated classically in polynomial time. This is based on the fact that matchgates preserve fermionic parity and can be mapped to free-fermion systems.

Parameters

encoding : BaseEncoding The encoding to check.

Returns

bool True if the circuit is believed to use only matchgate operations with appropriate (nearest-neighbor) topology.

Raises

AnalysisError If encoding is not a valid BaseEncoding instance.

Notes

What are Matchgates?

Matchgates are a class of two-qubit gates that act on the computational basis states in a specific way that preserves particle number parity. Common matchgates include:

  • iSWAP: Swaps |01⟩ ↔ |10⟩ with a phase
  • fSWAP (fermionic SWAP): Used in fermionic simulations
  • Givens rotations: Parameterized rotations in the {|01⟩, |10⟩} subspace

Simulability Conditions

Matchgate circuits are classically simulable when:

  1. All two-qubit gates are matchgates
  2. Qubits are arranged in a line topology
  3. Two-qubit gates act only on nearest neighbors

Limitations

This check is heuristic and may be conservative. It identifies known matchgate-based encodings by name and checks for linear entanglement patterns. Some matchgate circuits may not be detected if they don't follow recognized naming conventions.

References

.. [1] Jozsa, R., & Miyake, A. (2008). "Matchgates and classical simulation of quantum circuits." Proc. R. Soc. A, 464(2100), 3089-3106.

.. [2] Terhal, B. M., & DiVincenzo, D. P. (2002). "Classical simulation of noninteracting-fermion quantum circuits." Physical Review A, 65(3).

Examples

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) is_matchgate_circuit(enc) False

See Also

is_clifford_circuit : Check for Clifford-only circuits. check_simulability : Full simulability analysis.

kernel_effective_dimension

kernel_effective_dimension(K: NDArray[floating[Any]], *, regularization: float = 1.0, normalize: bool = True) -> float

Effective degrees of freedom of a kernel's eigenspectrum.

d_eff(lambda) = sum_i lambda_i / (lambda_i + lambda) over the eigenvalues lambda_i of K (Zhang, 2005). It is a smooth "soft rank" measuring how many feature-space directions the kernel effectively uses, in [0, n].

Parameters

K : ndarray, shape (n, n) Kernel matrix (symmetric PSD). regularization : float, default=1.0 Ridge lambda. With normalize=True the mean eigenvalue is 1, so lambda = 1 weights each direction by its relative strength. normalize : bool, default=True Trace-normalise K to n (mean eigenvalue 1) before the sum, making the value comparable across encodings and dataset sizes.

kernel_target_alignment

kernel_target_alignment(K: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]]) -> float

Uncentred kernel-target alignment in [-1, 1] (Cristianini, 2002).

A(K, y) = <K, yy^T>_F / (||K||_F ||yy^T||_F) with labels mapped to {-1, +1}. Returns 0.0 for degenerate kernels or single-class labels.

Two-class labels are mapped by partition, so the score does not depend on whether the classes are spelled {0, 1}, {1, 2} or {-1, +1}. Unlike :func:centered_kernel_target_alignment, the uncentred score is not invariant to that choice on its own, which is why the mapping is normalised here.

partial_trace_single_qubit

partial_trace_single_qubit(statevector: StatevectorType, n_qubits: int, keep_qubit: int) -> DensityMatrixType

Compute the partial trace, keeping only a single qubit.

This function traces out all qubits except the specified one, returning the reduced density matrix for that qubit.

Parameters

statevector : StatevectorType Full system statevector of shape (2^n_qubits,). n_qubits : int Total number of qubits in the system. keep_qubit : int Index of the qubit to keep (0-indexed). Qubit 0 is the most significant bit in the computational basis ordering.

Returns

DensityMatrixType Reduced density matrix for the kept qubit, shape (2, 2). This is a valid density matrix: Hermitian, positive semidefinite, with trace 1.

Raises

ValueError If keep_qubit is out of range [0, n_qubits-1]. ValidationError If the statevector is invalid.

Examples

For a separable state |00⟩, each qubit's reduced state is |0⟩:

import numpy as np state_00 = np.array([1, 0, 0, 0], dtype=complex) # |00⟩ rho_0 = partial_trace_single_qubit(state_00, n_qubits=2, keep_qubit=0) print(rho_0) [[1.+0.j 0.+0.j] [0.+0.j 0.+0.j]]

For a Bell state (|00⟩ + |11⟩)/√2, each qubit is maximally mixed:

bell = np.array([1, 0, 0, 1], dtype=complex) / np.sqrt(2) rho_0 = partial_trace_single_qubit(bell, n_qubits=2, keep_qubit=0) print(np.round(rho_0, 3)) [[0.5+0.j 0. +0.j] [0. +0.j 0.5+0.j]]

Notes

The convention used is that qubit 0 is the most significant bit. For a 2-qubit system:

  • Index 0: |00⟩ (qubit 0 = 0, qubit 1 = 0)
  • Index 1: |01⟩ (qubit 0 = 0, qubit 1 = 1)
  • Index 2: |10⟩ (qubit 0 = 1, qubit 1 = 0)
  • Index 3: |11⟩ (qubit 0 = 1, qubit 1 = 1)
See Also

partial_trace_subsystem : Trace out arbitrary qubits. compute_purity : Compute purity of a density matrix.

partial_trace_subsystem

partial_trace_subsystem(statevector: StatevectorType, n_qubits: int, keep_qubits: Sequence[int]) -> DensityMatrixType

Compute partial trace, keeping a specified subsystem.

This function traces out all qubits not in the keep list, returning the reduced density matrix for the kept subsystem.

Parameters

statevector : StatevectorType Full system statevector of shape (2^n_qubits,). n_qubits : int Total number of qubits in the system. keep_qubits : Sequence[int] Indices of qubits to keep (0-indexed). Must be a non-empty sequence of unique integers in range [0, n_qubits-1].

Returns

DensityMatrixType Reduced density matrix for the kept subsystem, shape (2^len(keep_qubits), 2^len(keep_qubits)).

Raises

ValueError If keep_qubits is empty, contains duplicates, or has out-of-range indices. ValidationError If the statevector is invalid.

Examples

Example 1: Keep the first two qubits of a 3-qubit product state

import numpy as np

|000⟩ state

state = np.zeros(8, dtype=complex) state[0] = 1.0 rho_01 = partial_trace_subsystem(state, n_qubits=3, keep_qubits=[0, 1]) print(rho_01.shape) (4, 4)

Result is |00⟩⟨00| (pure state)

print(np.round(rho_01, 3)) [[1.+0.j 0.+0.j 0.+0.j 0.+0.j] [0.+0.j 0.+0.j 0.+0.j 0.+0.j] [0.+0.j 0.+0.j 0.+0.j 0.+0.j] [0.+0.j 0.+0.j 0.+0.j 0.+0.j]]

Example 2: GHZ state partial trace (keeping first qubit)

For the GHZ state (|000⟩ + |111⟩)/√2, tracing out qubits 1 and 2 leaves qubit 0 in a maximally mixed state:

ghz = np.zeros(8, dtype=complex) ghz[0] = 1.0 / np.sqrt(2) # |000⟩ ghz[7] = 1.0 / np.sqrt(2) # |111⟩ rho_0 = partial_trace_subsystem(ghz, n_qubits=3, keep_qubits=[0])

Single qubit is maximally mixed (I/2)

print(np.round(rho_0, 3)) [[0.5+0.j 0. +0.j] [0. +0.j 0.5+0.j]]

Example 3: Non-contiguous qubit selection

Keep qubits 0 and 2, tracing out qubit 1:

|000⟩ state

state = np.zeros(8, dtype=complex) state[0] = 1.0 rho_02 = partial_trace_subsystem(state, n_qubits=3, keep_qubits=[0, 2]) print(rho_02.shape) (4, 4)

Result is |00⟩⟨00| for qubits 0 and 2

print(np.round(np.diag(rho_02).real, 3)) [1. 0. 0. 0.]

Example 4: W state partial trace

The W state (|001⟩ + |010⟩ + |100⟩)/√3 has different entanglement structure than GHZ. Tracing out one qubit leaves a mixed state:

w_state = np.zeros(8, dtype=complex) w_state[1] = 1.0 / np.sqrt(3) # |001⟩ w_state[2] = 1.0 / np.sqrt(3) # |010⟩ w_state[4] = 1.0 / np.sqrt(3) # |100⟩ rho_01 = partial_trace_subsystem(w_state, n_qubits=3, keep_qubits=[0, 1])

Trace = 1, but not maximally mixed

print(f"Trace: {np.trace(rho_01).real:.3f}") Trace: 1.000

Notes

The ordering of qubits in the output follows the order specified in keep_qubits. For example, if keep_qubits=[1, 0], the first index of the output density matrix corresponds to qubit 1.

Qubit Indexing Convention

Qubit 0 is the most significant bit in the computational basis:

  • For 3 qubits: |q0 q1 q2⟩
  • Index 0 = |000⟩, Index 1 = |001⟩, Index 4 = |100⟩, Index 7 = |111⟩

Use Cases

  • Computing entanglement entropy of subsystems
  • Analyzing local properties of entangled states
  • Verifying separability of quantum states

profile_encoding

profile_encoding(encoding: BaseEncoding, *, X: NDArray[floating[Any]] | None = None, y: NDArray[integer[Any]] | None = None, expressibility_samples: int = 500, entanglement_samples: int = 500, trainability_samples: int = 100, noise_level: str = 'medium', noise_samples: int = 20, include_noise: bool = True, concentration_samples: int = 30, seed: int | None = None) -> EncodingCharacterization

Characterise an encoding across every analysis axis.

Parameters

encoding : BaseEncoding The encoding to profile (may be any custom encoding, not just a built-in one). X : ndarray or None, default=None Optional feature matrix (n_features must match the encoding). When given, the kernel-geometry axes are computed. y : ndarray or None, default=None Optional binary labels; required for kernel-target alignment. expressibility_samples, entanglement_samples, trainability_samples : int Sample counts for the corresponding stochastic estimators. noise_level : {"low", "medium", "high"}, default="medium" Noise preset for the noise-resilience axis. noise_samples : int, default=20 Inputs averaged for the noise-resilience axis. include_noise : bool, default=True Whether to compute the (density-matrix) noise axis. concentration_samples : int, default=30 Inputs used to build the kernel for the concentration axis. When X is supplied, X is used instead and this is ignored. seed : int or None, default=None Base random seed for all stochastic axes.

Returns

EncodingCharacterization The computed multi-axis profile. Axes that fail are recorded as None with a note, so the call always returns a profile for a valid encoding.

recommend_feature_range

recommend_feature_range(encoding: BaseEncoding, X: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]], *, ranges: Sequence[tuple[float, float]] = DEFAULT_FEATURE_RANGES, max_samples: int = _DEFAULT_MAX_SAMPLES, centered: bool = True, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> tuple[float, float]

Return the feature range that maximises alignment for encoding.

A thin wrapper over :func:scan_feature_ranges for the common case of "just tell me what to scale into". Ties break towards the narrower range.

Parameters

encoding, X, y, ranges, max_samples, centered, seed, backend As for :func:scan_feature_ranges.

Returns

(float, float) The recommended (low, high). Feed it to :func:scale_to_range.

Examples

from encoding_atlas import IQPEncoding from encoding_atlas.benchmark import get_dataset X, y = get_dataset("moons", n_samples=40, seed=0) low, high = recommend_feature_range( ... IQPEncoding(n_features=2, reps=2), X, y, seed=0 ... ) low == 0.0 True

sample_shot_kernel

sample_shot_kernel(K: NDArray[floating[Any]], shots: int, *, seed: int | None = None) -> NDArray[floating[Any]]

Return the finite-shot estimate of an exact fidelity kernel.

Models the compute-uncompute estimator used on hardware: for each pair (i, j) the circuit U(x_j)^dagger U(x_i)|0> is measured shots times and the fraction of all-zeros outcomes is taken as K[i, j]. Because the all-zeros probability is K[i, j], that count is exactly Binomial(shots, K[i, j]) — so drawing from the binomial is statistically identical to simulating the circuits, at a fraction of the cost.

The estimate is unbiased entrywise and symmetric, and its diagonal is exactly 1 (the compute-uncompute circuit for x_i = x_i returns all-zeros with certainty). It is not guaranteed to be positive semidefinite: independent noise on each entry routinely pushes the smallest eigenvalues negative. Project with :func:encoding_atlas.benchmark.kernel.ensure_psd before handing it to a precomputed-kernel estimator.

Parameters

K : ndarray, shape (n, n) Exact kernel with entries in [0, 1], e.g. from :func:compute_fidelity_kernel. shots : int Measurements per entry. Must be a positive integer. seed : int or None, default=None Seed for the binomial draws.

Returns

ndarray, shape (n, n) Symmetric finite-shot estimate with unit diagonal. Entries are multiples of 1 / shots.

Raises

ValueError If shots is not a positive integer, or K is not a square 2D matrix with entries in [0, 1].

Examples

import numpy as np K = np.array([[1.0, 0.5], [0.5, 1.0]]) K_hat = sample_shot_kernel(K, shots=1000, seed=0) bool(K_hat[0, 0] == 1.0 and K_hat[0, 1] == K_hat[1, 0]) True

Notes

The variance of each off-diagonal estimate is K (1 - K) / shots. When the kernel has concentrated — every off-diagonal entry near 2**-n — that noise swamps the signal unless shots grows with the Hilbert-space dimension; see :func:encoding_atlas.analysis.concentration.compute_kernel_concentration for the resulting budget.

scale_to_range

scale_to_range(X: NDArray[floating[Any]], low: float, high: float, *, reference: NDArray[floating[Any]] | None = None) -> NDArray[float64]

Min-max scale each feature of X into [low, high].

Parameters

X : ndarray of shape (n_samples, n_features) Data to transform. low, high : float Target interval. Must satisfy low < high and both be finite. reference : ndarray of shape (m, n_features), optional Fit the per-feature minimum and span on this array instead of on X. Pass the training fold here to transform a test fold without leaking its extremes into the scaler — transformed values may then fall outside [low, high], which is correct and matches :class:sklearn.preprocessing.MinMaxScaler. Defaults to X.

Returns

ndarray of shape (n_samples, n_features) The scaled data. Constant features map to low.

Raises

ValueError If X is not 2D, the range is invalid or non-finite, or reference has a different feature count.

Examples

import numpy as np X = np.array([[0.0, 10.0], [1.0, 20.0]]) np.round(scale_to_range(X, 0.0, np.pi), 4) array([[0. , 0. ], [3.1416, 3.1416]])

scan_feature_ranges

scan_feature_ranges(encoding: BaseEncoding, X: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]], *, ranges: Sequence[tuple[float, float]] = DEFAULT_FEATURE_RANGES, max_samples: int = _DEFAULT_MAX_SAMPLES, centered: bool = True, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> FeatureRangeScan

Measure an encoding's kernel geometry across feature-scaling ranges.

For each candidate range the data is min-max scaled into it, the fidelity kernel is built, and both the kernel-target alignment and the kernel concentration are recorded. No training is involved.

Use it to choose a range (see :func:recommend_feature_range) and to see how much the choice matters — :attr:FeatureRangeScan.alignment_spread is often comparable to the spread across encodings.

Parameters

encoding : BaseEncoding Encoding to scan. Built once and reused across ranges. X : ndarray of shape (n_samples, n_features) Unscaled feature matrix. Whatever scaling it already carries is overwritten, so pass raw features. y : ndarray of shape (n_samples,) Two-class labels. Any two distinct values work. ranges : sequence of (float, float), default=:data:DEFAULT_FEATURE_RANGES Candidate ranges. Each must satisfy low < high. max_samples : int, default=200 Sub-sample above this many rows. Must be at least 2. centered : bool, default=True Use the centred alignment (Cortes et al., 2012). seed : int or None, default=None Seed for sub-sampling. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

FeatureRangeScan One measurement per range, plus the best range and the spread.

Raises

ValueError If X/y are malformed or mismatched, y is not two-class, max_samples is invalid, or any range is invalid.

Examples

from encoding_atlas import IQPEncoding from encoding_atlas.benchmark import get_dataset X, y = get_dataset("moons", n_samples=40, seed=0) scan = scan_feature_ranges(IQPEncoding(n_features=2, reps=2), X, y, seed=0) len(scan.results) 4

See Also

recommend_feature_range : Just the best range. encoding_atlas.analysis.compute_kernel_concentration : The concentration axis.

simulate_encoding_statevector

simulate_encoding_statevector(encoding: BaseEncoding, x: NDArray[floating[Any]], backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> StatevectorType

Simulate an encoding circuit and return the resulting statevector.

This function executes a quantum circuit defined by the encoding with the given input data and returns the final quantum state as a complex vector in the computational basis.

Parameters

encoding : BaseEncoding The encoding instance to simulate. x : NDArray[np.floating] Input data vector of shape (n_features,). The values are used as parameters for the encoding circuit. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" The quantum simulation backend to use.

- ``"pennylane"``: Uses PennyLane's default.qubit simulator
- ``"qiskit"``: Uses Qiskit's Statevector class
- ``"cirq"``: Uses Cirq's Simulator for statevector simulation
Returns

StatevectorType Complex statevector of shape (2^n_qubits,) representing the final quantum state in the computational basis. The state is normalized (sum of |amplitude|² = 1).

Raises

SimulationError If the simulation fails due to backend errors, missing dependencies, or invalid circuit structure. ValidationError If the encoding or input data is invalid. ValueError If an unknown backend is specified.

Warnings

UserWarning If the number of qubits exceeds _SIMULATION_QUBIT_WARNING_THRESHOLD (15 by default), a warning is issued about memory usage.

Examples

Simulate an angle encoding circuit:

from encoding_atlas import AngleEncoding import numpy as np

enc = AngleEncoding(n_features=3) x = np.array([0.1, 0.2, 0.3]) state = simulate_encoding_statevector(enc, x) print(f"State shape: {state.shape}") State shape: (8,) print(f"State norm: {np.linalg.norm(state):.6f}") State norm: 1.000000

Using Qiskit backend:

state_qiskit = simulate_encoding_statevector(enc, x, backend="qiskit")

Results should be equivalent (up to global phase)

fidelity = np.abs(np.vdot(state, state_qiskit))**2 print(f"Cross-backend fidelity: {fidelity:.6f}") Cross-backend fidelity: 1.000000

Notes

The statevector is ordered in the computational basis with the standard convention where qubit 0 is the most significant bit:

  • Index 0 corresponds to |00...0⟩
  • Index 1 corresponds to |00...1⟩
  • Index 2^n - 1 corresponds to |11...1⟩

Memory Requirements

Memory grows exponentially with qubit count:

  • Statevector: 2^n × 16 bytes (e.g., 15 qubits → 0.5 MB, 20 qubits → 16 MB)
  • Density matrix (for analysis): 2^(2n) × 16 bytes (e.g., 15 qubits → 16 GB)

Analysis operations like entanglement capability create full density matrices, so their practical limit is around 13-15 qubits. For larger systems, consider tensor network methods or sampling-based approaches.

See Also

simulate_encoding_statevectors_batch : Simulate multiple inputs at once. compute_fidelity : Compute fidelity between two statevectors.

simulate_encoding_statevectors_batch

simulate_encoding_statevectors_batch(encoding: BaseEncoding, X: NDArray[floating[Any]], backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> NDArray[complexfloating[Any, Any]]

Simulate encoding circuits for multiple input vectors.

This is a convenience function that applies :func:simulate_encoding_statevector to each row of a 2D input array and returns a pre-allocated 2D array of statevectors.

Parameters

encoding : BaseEncoding The encoding instance to simulate. X : NDArray[np.floating] Input data array of shape (n_samples, n_features). backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" The quantum simulation backend to use.

Returns

NDArray[np.complexfloating], shape (n_samples, 2**n_qubits) 2D array of statevectors, one row per input sample.

Raises

SimulationError If simulation fails for any input. ValidationError If input shape is incorrect.

Examples

from encoding_atlas import AngleEncoding import numpy as np

enc = AngleEncoding(n_features=2) X = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]) states = simulate_encoding_statevectors_batch(enc, X) print(f"Number of states: {len(states)}") Number of states: 3

simulate_noisy_density_matrix

simulate_noisy_density_matrix(encoding: BaseEncoding, x: NDArray[floating[Any]], *, single_qubit_error: float, two_qubit_error: float) -> NDArray[complexfloating[Any, Any]]

Simulate the encoding under depolarizing noise; return the density matrix.

A depolarizing channel is inserted after each gate: single_qubit_error after one-qubit gates, and two_qubit_error on each qubit of two-qubit (or larger) gates. Uses PennyLane's default.mixed device.

Parameters

encoding : BaseEncoding Encoding to simulate. x : ndarray, shape (n_features,) Input to encode. single_qubit_error, two_qubit_error : float Depolarizing probabilities in [0, 1].

Returns

ndarray, shape (2**n_qubits, 2**n_qubits) The noisy density matrix.

summarize_kernel_concentration

summarize_kernel_concentration(K: NDArray[floating[Any]], n_qubits: int, *, threshold: float = CONCENTRATION_THRESHOLD) -> ConcentrationResult

Reduce an already-computed fidelity kernel to concentration statistics.

Split out from :func:compute_kernel_concentration so a caller that already holds the kernel — screening several encodings, for instance — gets the concentration axis without paying for a second simulation, while both paths keep a single definition of the statistics.

Parameters

K : ndarray of shape (n, n) Fidelity kernel with entries in [0, 1]. Must have at least two rows so an off-diagonal pair exists. n_qubits : int Circuit width the kernel was produced at; sets the Haar reference. threshold : float, default=2.0 concentration_ratio below which is_concentrated is set.

Returns

ConcentrationResult The same statistics :func:compute_kernel_concentration returns.

Raises

ValueError If K is not a square 2D matrix of size >= 2, n_qubits is not a positive integer, or threshold is not positive.

validate_binary_labels

validate_binary_labels(y: NDArray[integer[Any] | floating[Any]]) -> NDArray[float64]

Validate that y is a 1D, finite, exactly-two-class label vector.

Kernel-target alignment is a two-class quantity, so anything else — a single class, a multi-class vector, or a continuous regression target — would be scored meaninglessly rather than rejected. Callers that rank or select encodings by alignment should gate on this first.

Parameters

y : ndarray of shape (n_samples,) Candidate label vector. The two classes may be any two distinct values; see :func:_signed_labels for how they are mapped.

Returns

ndarray of shape (n_samples,), dtype float64 The labels as float64, unchanged apart from the cast.

Raises

ValueError If y is not 1D, contains non-finite values, or does not have exactly two distinct values.

Examples

import numpy as np validate_binary_labels(np.array([0, 1, 1, 0])).dtype dtype('float64')

validate_encoding_for_analysis

validate_encoding_for_analysis(encoding: BaseEncoding) -> None

Validate that an encoding is suitable for analysis operations.

This function performs comprehensive validation to ensure the encoding can be safely used in analysis functions. It checks the encoding type, configuration, and derived properties.

Parameters

encoding : BaseEncoding The encoding instance to validate.

Raises

AnalysisError If the encoding is not a valid BaseEncoding instance. ValidationError If the encoding has invalid configuration (e.g., zero features or qubits).

Examples

from encoding_atlas import AngleEncoding enc = AngleEncoding(n_features=4) validate_encoding_for_analysis(enc) # No exception raised

validate_encoding_for_analysis("not an encoding") Traceback (most recent call last): ... AnalysisError: Expected BaseEncoding instance, got str

Notes

This function is called internally by analysis functions. You generally don't need to call it directly unless implementing custom analysis.

validate_statevector

validate_statevector(statevector: NDArray[Any], expected_qubits: int | None = None, check_normalization: bool = True, tolerance: float = 1e-10) -> StatevectorType

Validate and normalize a statevector.

Parameters

statevector : NDArray The statevector to validate. expected_qubits : int, optional If provided, verify the statevector has dimension 2^expected_qubits. check_normalization : bool, default=True If True, verify the statevector is normalized and renormalize if needed. tolerance : float, default=1e-10 Tolerance for normalization check.

Returns

StatevectorType Validated (and possibly renormalized) statevector as complex128.

Raises

ValidationError If the statevector has invalid shape or values. NumericalInstabilityError If the statevector has near-zero norm.

Examples

import numpy as np state = np.array([1, 0, 0, 0], dtype=complex) validated = validate_statevector(state, expected_qubits=2) validated.dtype dtype('complex128')

Kernel-Concentration Diagnostics

How an encoding's fidelity kernel approaches the Haar floor as the circuit widens — the axis that says whether a fixed-width result transfers. See Kernel Concentration for the theory.

compute_kernel_concentration

compute_kernel_concentration(encoding: BaseEncoding, X: NDArray[floating[Any]] | None = None, *, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, sampling: SamplingMethod = 'uniform', threshold: float = CONCENTRATION_THRESHOLD, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> ConcentrationResult

Measure how concentrated an encoding's fidelity kernel is.

Builds the fidelity kernel on X (or on random inputs when X is not supplied) and reduces it to the concentration order parameter, the spread of its off-diagonal entries, and the shot budget that spread implies.

A concentration_ratio near 1 means the kernel's off-diagonal variance has collapsed to the Haar floor: every pair of distinct inputs is equally dissimilar, the matrix is the identity up to sampling noise, and a kernel method built on it cannot generalize regardless of shot budget or noise level. Values well above 1 mean the encoding still separates inputs.

Parameters

encoding : BaseEncoding Encoding to analyse. X : ndarray of shape (n_samples, n_features), optional Inputs to build the kernel from. When None (default), inputs are drawn from input_range, making this a data-free axis comparable across encodings — the same convention as expressibility and entanglement capability. n_samples : int, default=40 Number of random inputs when X is not supplied. Must be >= 2. Ignored when X is given. input_range : (float, float), default=(0, 2*pi) Range for randomly sampled inputs. Concentration depends on the input distribution as well as the circuit; narrowing this range (as feature scaling does) generally reduces it. Ignored when X is given. sampling : {"uniform", "sobol"}, default="uniform" Random-input strategy. Ignored when X is given. threshold : float, default=2.0 concentration_ratio below which is_concentrated is set. seed : int or None, default=None Seed for input sampling. Ignored when X is given. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

ConcentrationResult The measured concentration statistics.

Raises

ValueError If n_samples, input_range, threshold or sampling is invalid, or if X is not a 2D array with at least two rows.

Warns

UserWarning If the encoding is wide enough that the statevector batch becomes memory-hungry (more than 14 qubits).

Examples

An entangling feature map sits at the Haar floor; a non-entangling one does not:

from encoding_atlas import AngleEncoding, IQPEncoding angle = compute_kernel_concentration( ... AngleEncoding(n_features=6), n_samples=30, seed=0 ... ) iqp = compute_kernel_concentration( ... IQPEncoding(n_features=6, reps=2), n_samples=30, seed=0 ... ) angle.concentration_ratio > iqp.concentration_ratio True

See Also

estimate_concentration_scaling : The same measurement across widths. encoding_atlas.analysis.generalization.compute_fidelity_kernel : The kernel.

Source code in src/encoding_atlas/analysis/concentration.py
def compute_kernel_concentration(
    encoding: BaseEncoding,
    X: NDArray[np.floating[Any]] | None = None,
    *,
    n_samples: int = _DEFAULT_N_SAMPLES,
    input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE,
    sampling: SamplingMethod = "uniform",
    threshold: float = CONCENTRATION_THRESHOLD,
    seed: int | None = None,
    backend: Literal["pennylane", "qiskit", "cirq"] = "pennylane",
) -> ConcentrationResult:
    """Measure how concentrated an encoding's fidelity kernel is.

    Builds the fidelity kernel on ``X`` (or on random inputs when ``X`` is not
    supplied) and reduces it to the concentration order parameter, the spread
    of its off-diagonal entries, and the shot budget that spread implies.

    A ``concentration_ratio`` near 1 means the kernel's off-diagonal variance
    has collapsed to the Haar floor: every pair of distinct inputs is equally
    dissimilar, the matrix is the identity up to sampling noise, and a kernel
    method built on it cannot generalize regardless of shot budget or noise
    level. Values well above 1 mean the encoding still separates inputs.

    Parameters
    ----------
    encoding : BaseEncoding
        Encoding to analyse.
    X : ndarray of shape (n_samples, n_features), optional
        Inputs to build the kernel from. When ``None`` (default), inputs are
        drawn from ``input_range``, making this a data-free axis comparable
        across encodings — the same convention as expressibility and
        entanglement capability.
    n_samples : int, default=40
        Number of random inputs when ``X`` is not supplied. Must be >= 2.
        Ignored when ``X`` is given.
    input_range : (float, float), default=(0, 2*pi)
        Range for randomly sampled inputs. Concentration depends on the input
        distribution as well as the circuit; narrowing this range (as feature
        scaling does) generally reduces it. Ignored when ``X`` is given.
    sampling : {"uniform", "sobol"}, default="uniform"
        Random-input strategy. Ignored when ``X`` is given.
    threshold : float, default=2.0
        ``concentration_ratio`` below which ``is_concentrated`` is set.
    seed : int or None, default=None
        Seed for input sampling. Ignored when ``X`` is given.
    backend : {"pennylane", "qiskit", "cirq"}, default="pennylane"
        Statevector simulation backend.

    Returns
    -------
    ConcentrationResult
        The measured concentration statistics.

    Raises
    ------
    ValueError
        If ``n_samples``, ``input_range``, ``threshold`` or ``sampling`` is
        invalid, or if ``X`` is not a 2D array with at least two rows.

    Warns
    -----
    UserWarning
        If the encoding is wide enough that the statevector batch becomes
        memory-hungry (more than 14 qubits).

    Examples
    --------
    An entangling feature map sits at the Haar floor; a non-entangling one
    does not:

    >>> from encoding_atlas import AngleEncoding, IQPEncoding
    >>> angle = compute_kernel_concentration(
    ...     AngleEncoding(n_features=6), n_samples=30, seed=0
    ... )
    >>> iqp = compute_kernel_concentration(
    ...     IQPEncoding(n_features=6, reps=2), n_samples=30, seed=0
    ... )
    >>> angle.concentration_ratio > iqp.concentration_ratio
    True

    See Also
    --------
    estimate_concentration_scaling : The same measurement across widths.
    encoding_atlas.analysis.generalization.compute_fidelity_kernel : The kernel.
    """
    validate_sampling(sampling)
    if threshold <= 0.0:
        raise ValueError(f"threshold must be positive, got {threshold}")

    n_qubits = int(encoding.n_qubits)
    if n_qubits > _WARN_QUBITS:
        n_states = n_samples if X is None else len(X)
        warnings.warn(
            f"Kernel concentration at {n_qubits} qubits simulates a "
            f"{n_states} x 2^{n_qubits} statevector batch "
            f"(~{n_states * (2**n_qubits) * 16 / 1024**2:.0f} MB) and may be "
            f"slow. Consider fewer samples or a narrower circuit.",
            UserWarning,
            stacklevel=2,
        )

    if X is None:
        _validate_common(n_samples, input_range)
        rng = np.random.default_rng(seed)
        X_used = generate_sample_batch(
            int(n_samples),
            int(encoding.n_features),
            input_range,
            rng,
            sampling,
        )
    else:
        X_used = np.asarray(X, dtype=np.float64)
        if X_used.ndim != 2:
            raise ValueError(f"X must be a 2D array, got shape {X_used.shape}")
        if X_used.shape[0] < 2:
            raise ValueError(
                f"X must contain at least 2 samples to form an off-diagonal "
                f"pair, got {X_used.shape[0]}"
            )

    K = compute_fidelity_kernel(encoding, X_used, backend=backend)
    result = summarize_kernel_concentration(K, n_qubits, threshold=threshold)

    logger.debug(
        "Concentration for %s at %d qubits: var=%.6g ratio=%.3f",
        type(encoding).__name__,
        n_qubits,
        result.offdiagonal_variance,
        result.concentration_ratio,
    )
    return result

estimate_concentration_scaling

estimate_concentration_scaling(factory: Callable[[int], BaseEncoding], *, feature_counts: tuple[int, ...] = _DEFAULT_FEATURE_COUNTS, n_samples: int = _DEFAULT_N_SAMPLES, input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE, sampling: SamplingMethod = 'uniform', threshold: float = CONCENTRATION_THRESHOLD, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> ScalingResult

Measure kernel concentration across a range of circuit widths.

Calls factory(n_features) for each requested feature count, measures the concentration at each resulting width, and fits a log-linear decay model. Feature counts the factory rejects (an encoding requiring exactly two features, or an even count) are recorded in :attr:ScalingResult.skipped rather than aborting the sweep.

The fit is against the encoding's qubit count, not its feature count, because concentration is a statement about Hilbert-space dimension. The distinction matters for amplitude encoding, where n_qubits = ceil(log2(n_features)).

Parameters

factory : callable n_features -> BaseEncoding, e.g. lambda d: IQPEncoding(n_features=d, reps=2). feature_counts : tuple[int, ...], default=(2, 4, 6, 8) Feature counts to sweep. Must be non-empty and all positive. n_samples : int, default=40 Random inputs per width. Must be >= 2. input_range : (float, float), default=(0, 2*pi) Range for randomly sampled inputs. sampling : {"uniform", "sobol"}, default="uniform" Random-input strategy. threshold : float, default=2.0 concentration_ratio defining the Haar floor. seed : int or None, default=None Base seed. Width i uses seed + i, so widths are independent draws yet the whole sweep is reproducible. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

ScalingResult Per-width measurements plus the fitted decay rates, Haar-normalized slope, and fit quality.

Raises

ValueError If feature_counts is empty or contains non-positive values, or if any shared argument is invalid. RuntimeError If the factory failed for every requested feature count; the per-count reasons are included in the message.

Examples

from encoding_atlas import IQPEncoding scaling = estimate_concentration_scaling( ... lambda d: IQPEncoding(n_features=d, reps=2), ... feature_counts=(2, 4), ... n_samples=12, ... seed=0, ... ) len(scaling.results) 2

See Also

compute_kernel_concentration : Single-width measurement.

Source code in src/encoding_atlas/analysis/concentration.py
def estimate_concentration_scaling(
    factory: Callable[[int], BaseEncoding],
    *,
    feature_counts: tuple[int, ...] = _DEFAULT_FEATURE_COUNTS,
    n_samples: int = _DEFAULT_N_SAMPLES,
    input_range: tuple[float, float] = _DEFAULT_INPUT_RANGE,
    sampling: SamplingMethod = "uniform",
    threshold: float = CONCENTRATION_THRESHOLD,
    seed: int | None = None,
    backend: Literal["pennylane", "qiskit", "cirq"] = "pennylane",
) -> ScalingResult:
    """Measure kernel concentration across a range of circuit widths.

    Calls ``factory(n_features)`` for each requested feature count, measures
    the concentration at each resulting width, and fits a log-linear decay
    model. Feature counts the factory rejects (an encoding requiring exactly
    two features, or an even count) are recorded in
    :attr:`ScalingResult.skipped` rather than aborting the sweep.

    The fit is against the encoding's **qubit count**, not its feature count,
    because concentration is a statement about Hilbert-space dimension. The
    distinction matters for amplitude encoding, where ``n_qubits =
    ceil(log2(n_features))``.

    Parameters
    ----------
    factory : callable
        ``n_features -> BaseEncoding``, e.g.
        ``lambda d: IQPEncoding(n_features=d, reps=2)``.
    feature_counts : tuple[int, ...], default=(2, 4, 6, 8)
        Feature counts to sweep. Must be non-empty and all positive.
    n_samples : int, default=40
        Random inputs per width. Must be >= 2.
    input_range : (float, float), default=(0, 2*pi)
        Range for randomly sampled inputs.
    sampling : {"uniform", "sobol"}, default="uniform"
        Random-input strategy.
    threshold : float, default=2.0
        ``concentration_ratio`` defining the Haar floor.
    seed : int or None, default=None
        Base seed. Width ``i`` uses ``seed + i``, so widths are independent
        draws yet the whole sweep is reproducible.
    backend : {"pennylane", "qiskit", "cirq"}, default="pennylane"
        Statevector simulation backend.

    Returns
    -------
    ScalingResult
        Per-width measurements plus the fitted decay rates, Haar-normalized
        slope, and fit quality.

    Raises
    ------
    ValueError
        If ``feature_counts`` is empty or contains non-positive values, or if
        any shared argument is invalid.
    RuntimeError
        If the factory failed for *every* requested feature count; the
        per-count reasons are included in the message.

    Examples
    --------
    >>> from encoding_atlas import IQPEncoding
    >>> scaling = estimate_concentration_scaling(
    ...     lambda d: IQPEncoding(n_features=d, reps=2),
    ...     feature_counts=(2, 4),
    ...     n_samples=12,
    ...     seed=0,
    ... )
    >>> len(scaling.results)
    2

    See Also
    --------
    compute_kernel_concentration : Single-width measurement.
    """
    validate_sampling(sampling)
    _validate_common(n_samples, input_range)
    if threshold <= 0.0:
        raise ValueError(f"threshold must be positive, got {threshold}")

    counts = tuple(int(c) for c in feature_counts)
    if not counts:
        raise ValueError("feature_counts must not be empty")
    if any(c < 1 for c in counts):
        raise ValueError(f"feature_counts must all be positive, got {feature_counts!r}")

    encoding_name = ""
    measured: list[tuple[int, int, ConcentrationResult]] = []
    skipped: dict[int, str] = {}

    for index, n_features in enumerate(sorted(set(counts))):
        try:
            encoding = factory(n_features)
            result = compute_kernel_concentration(
                encoding,
                n_samples=n_samples,
                input_range=input_range,
                sampling=sampling,
                threshold=threshold,
                seed=None if seed is None else seed + index,
                backend=backend,
            )
        except Exception as exc:  # noqa: BLE001 - one bad width must not abort
            skipped[n_features] = f"{type(exc).__name__}: {exc}"
            logger.debug(
                "Concentration sweep skipped n_features=%d: %s", n_features, exc
            )
            continue
        encoding_name = encoding_name or type(encoding).__name__
        measured.append((int(encoding.n_qubits), n_features, result))

    if not measured:
        detail = "; ".join(f"n_features={k}: {v}" for k, v in sorted(skipped.items()))
        raise RuntimeError(
            f"Concentration sweep produced no measurable widths. Reasons: {detail}"
        )

    # A factory may map distinct feature counts onto the same qubit count
    # (amplitude encoding). Sort by width and keep every measurement; the fit
    # tolerates repeated abscissae.
    measured.sort(key=lambda item: (item[0], item[1]))
    qubit_counts = tuple(item[0] for item in measured)
    used_features = tuple(item[1] for item in measured)
    results = tuple(item[2] for item in measured)

    x = np.asarray(qubit_counts, dtype=np.float64)
    variances = np.asarray([r.offdiagonal_variance for r in results], dtype=np.float64)
    means = np.asarray([r.offdiagonal_mean for r in results], dtype=np.float64)
    ratios = np.asarray([r.concentration_ratio for r in results], dtype=np.float64)

    variance_slope, r_squared = _fit_log_linear(x, variances)
    mean_slope, _ = _fit_log_linear(x, means)
    ratio_slope, _ = _fit_log_linear(x, ratios)

    return ScalingResult(
        encoding_name=encoding_name,
        n_qubits=qubit_counts,
        feature_counts=used_features,
        results=results,
        decay_rate=(
            math.exp(-variance_slope) if math.isfinite(variance_slope) else math.nan
        ),
        mean_decay_rate=(
            math.exp(-mean_slope) if math.isfinite(mean_slope) else math.nan
        ),
        haar_normalized_slope=ratio_slope,
        r_squared=r_squared,
        skipped=skipped,
    )

haar_kernel_moments

haar_kernel_moments(n_qubits: int) -> tuple[float, float]

Return the Haar-random (mean, variance) of the fidelity kernel.

For two independent Haar-random pure states in dimension d = 2**n_qubits the overlap |<psi|phi>|^2 follows Beta(1, d - 1), giving

mean = 1 / d ,    variance = (d - 1) / (d^2 (d + 1)) .

The variance is the floor an encoding's kernel decays towards; it is the reference for :attr:ConcentrationResult.concentration_ratio.

Parameters

n_qubits : int Circuit width. Must be a positive integer.

Returns

(float, float) The Haar mean and variance of a single off-diagonal kernel entry.

Raises

ValueError If n_qubits is not a positive integer.

Examples

mean, variance = haar_kernel_moments(4) round(mean, 6) 0.0625 round(variance, 8) 0.00344669

Source code in src/encoding_atlas/analysis/concentration.py
def haar_kernel_moments(n_qubits: int) -> tuple[float, float]:
    """Return the Haar-random ``(mean, variance)`` of the fidelity kernel.

    For two independent Haar-random pure states in dimension ``d = 2**n_qubits``
    the overlap ``|<psi|phi>|^2`` follows ``Beta(1, d - 1)``, giving

        mean = 1 / d ,    variance = (d - 1) / (d^2 (d + 1)) .

    The variance is the floor an encoding's kernel decays *towards*; it is the
    reference for :attr:`ConcentrationResult.concentration_ratio`.

    Parameters
    ----------
    n_qubits : int
        Circuit width. Must be a positive integer.

    Returns
    -------
    (float, float)
        The Haar mean and variance of a single off-diagonal kernel entry.

    Raises
    ------
    ValueError
        If ``n_qubits`` is not a positive integer.

    Examples
    --------
    >>> mean, variance = haar_kernel_moments(4)
    >>> round(mean, 6)
    0.0625
    >>> round(variance, 8)
    0.00344669
    """
    if isinstance(n_qubits, bool) or not isinstance(n_qubits, (int, np.integer)):
        raise ValueError(f"n_qubits must be a positive integer, got {n_qubits!r}")
    if n_qubits < 1:
        raise ValueError(f"n_qubits must be a positive integer, got {n_qubits!r}")
    d = float(2 ** int(n_qubits))
    return 1.0 / d, (d - 1.0) / (d * d * (d + 1.0))

ConcentrationResult dataclass

ConcentrationResult(n_qubits: int, n_samples: int, offdiagonal_mean: float, offdiagonal_variance: float, offdiagonal_std: float, offdiagonal_min: float, offdiagonal_max: float, haar_mean: float, haar_variance: float, concentration_ratio: float, mean_ratio: float, identity_distance: float, is_concentrated: bool, threshold: float, shots_per_entry: float)

Fidelity-kernel concentration measured at one circuit width.

Attributes

n_qubits : int Circuit width the kernel was measured at. n_samples : int Number of inputs used; statistics are over the n_samples (n_samples - 1) / 2 off-diagonal pairs. offdiagonal_mean : float Mean off-diagonal kernel entry, in [0, 1]. offdiagonal_variance, offdiagonal_std : float Spread of the off-diagonal entries — the signal a kernel method actually uses. Its vanishing is concentration. offdiagonal_min, offdiagonal_max : float Extremes of the off-diagonal entries. haar_mean, haar_variance : float Haar-random reference moments at this width, from :func:haar_kernel_moments. concentration_ratio : float offdiagonal_variance / haar_variancethe order parameter. Near 1 means the kernel's spread has reached the Haar floor and carries no usable geometry; larger means structure remains. Directly comparable across qubit counts. mean_ratio : float offdiagonal_mean / haar_mean. Reported for completeness only: it is 1 for any ensemble of independent random single-qubit states as well as for Haar, so it cannot distinguish the two regimes. Do not use it as a concentration measure. identity_distance : float Root-mean-square off-diagonal entry, i.e. ||K - I||_F / sqrt(n (n - 1)). Zero means K is exactly I. is_concentrated : bool Whether concentration_ratio is below :attr:threshold. threshold : float The concentration_ratio cut used for :attr:is_concentrated. shots_per_entry : float Shots per kernel entry for the compute-uncompute estimator's standard error to fall below half the observed spread — 4 K_bar (1 - K_bar) / Var. inf if the entries have no spread. A noiseless lower bound on hardware cost.

shots_for_dataset

shots_for_dataset(n_samples: int) -> float

Total shots to estimate a full n_samples x n_samples kernel.

The kernel is symmetric with a deterministic unit diagonal, so only n (n - 1) / 2 entries need measuring.

Parameters

n_samples : int Dataset size. Must be a positive integer.

Returns

float shots_per_entry * n (n - 1) / 2; inf if the per-entry cost is unbounded.

Raises

ValueError If n_samples is not a positive integer.

Source code in src/encoding_atlas/analysis/concentration.py
def shots_for_dataset(self, n_samples: int) -> float:
    """Total shots to estimate a full ``n_samples x n_samples`` kernel.

    The kernel is symmetric with a deterministic unit diagonal, so only
    ``n (n - 1) / 2`` entries need measuring.

    Parameters
    ----------
    n_samples : int
        Dataset size. Must be a positive integer.

    Returns
    -------
    float
        ``shots_per_entry * n (n - 1) / 2``; ``inf`` if the per-entry cost
        is unbounded.

    Raises
    ------
    ValueError
        If ``n_samples`` is not a positive integer.
    """
    if isinstance(n_samples, bool) or not isinstance(n_samples, (int, np.integer)):
        raise ValueError(f"n_samples must be a positive integer, got {n_samples!r}")
    if n_samples < 1:
        raise ValueError(f"n_samples must be a positive integer, got {n_samples!r}")
    n = int(n_samples)
    return self.shots_per_entry * (n * (n - 1) / 2.0)

ScalingResult dataclass

ScalingResult(encoding_name: str, n_qubits: tuple[int, ...], feature_counts: tuple[int, ...], results: tuple[ConcentrationResult, ...], decay_rate: float, mean_decay_rate: float, haar_normalized_slope: float, r_squared: float, skipped: dict[int, str])

How an encoding's kernel concentration scales with circuit width.

Attributes

encoding_name : str Class name of the encodings the factory produced. n_qubits : tuple[int, ...] Circuit widths successfully measured, ascending. feature_counts : tuple[int, ...] Feature counts that produced those widths, in the same order. results : tuple[ConcentrationResult, ...] Per-width measurements, in the same order. decay_rate : float Per-qubit factor by which the off-diagonal variance shrinks, from a least-squares fit of log(variance) against n_qubits. 1.0 means no decay. The Haar floor itself decays at 4.0, so a rate near 4 means the encoding is scrambling as fast as it possibly can. nan if fewer than two widths were measurable. mean_decay_rate : float The same quantity for the off-diagonal mean. The Haar floor decays at 2.0. nan if unfittable. haar_normalized_slope : float Slope of log(concentration_ratio) against n_qubits. Negative means the kernel is collapsing towards the Haar floor as the circuit widens; non-negative means it holds structure or pulls away. This is the honest discriminator — a large decay_rate alone proves nothing, because the floor is falling too. r_squared : float Coefficient of determination of the log(variance) fit, in [0, 1]. Well below ~0.9 means the log-linear model — and any extrapolation from it — should not be trusted. nan for fewer than two points. skipped : dict[int, str] Feature counts that could not be measured, mapped to the reason.

concentration_ratios property

concentration_ratios: tuple[float, ...]

The measured concentration_ratio at each width.

offdiagonal_variances property

offdiagonal_variances: tuple[float, ...]

The measured off-diagonal variance at each width.

offdiagonal_means property

offdiagonal_means: tuple[float, ...]

The measured mean off-diagonal entry at each width.

concentration_horizon

concentration_horizon(threshold: float = CONCENTRATION_THRESHOLD) -> int | None

Smallest qubit count at which the kernel reaches the Haar floor.

Resolution order:

  1. If the measured widths show a sustained crossing — some width from which every wider measured point is also below threshold — the smallest such width is returned. No extrapolation involved. The crossing must be sustained: a single dip below the threshold at the narrowest width, followed by growth away from the floor, is not a horizon.
  2. Otherwise, if :attr:haar_normalized_slope is negative, the fit is extrapolated from the widest measured point to the first integer width whose predicted ratio falls below threshold.
  3. Otherwise None: the encoding shows no sign of approaching the floor over the measured range.
Parameters

threshold : float, default=:data:CONCENTRATION_THRESHOLD concentration_ratio defining the floor. Must be positive.

Returns

int or None The horizon, or None if the measurement does not support one.

Raises

ValueError If threshold is not positive.

Warnings

Case 2 is an extrapolation from a fitted log-linear model. Check :attr:r_squared before relying on it.

Source code in src/encoding_atlas/analysis/concentration.py
def concentration_horizon(
    self, threshold: float = CONCENTRATION_THRESHOLD
) -> int | None:
    """Smallest qubit count at which the kernel reaches the Haar floor.

    Resolution order:

    1. If the measured widths show a *sustained* crossing — some width
       from which every wider measured point is also below ``threshold``
       — the smallest such width is returned. No extrapolation involved.
       The crossing must be sustained: a single dip below the threshold at
       the narrowest width, followed by growth away from the floor, is not
       a horizon.
    2. Otherwise, if :attr:`haar_normalized_slope` is negative, the fit is
       extrapolated from the widest measured point to the first integer
       width whose predicted ratio falls below ``threshold``.
    3. Otherwise ``None``: the encoding shows no sign of approaching the
       floor over the measured range.

    Parameters
    ----------
    threshold : float, default=:data:`CONCENTRATION_THRESHOLD`
        ``concentration_ratio`` defining the floor. Must be positive.

    Returns
    -------
    int or None
        The horizon, or ``None`` if the measurement does not support one.

    Raises
    ------
    ValueError
        If ``threshold`` is not positive.

    Warnings
    --------
    Case 2 is an extrapolation from a fitted log-linear model. Check
    :attr:`r_squared` before relying on it.
    """
    if threshold <= 0.0:
        raise ValueError(f"threshold must be positive, got {threshold}")

    # Walk inwards from the widest measurement: the horizon is the start of
    # the longest unbroken run of below-threshold ratios that reaches the
    # widest width measured.
    ratios = self.concentration_ratios
    sustained: int | None = None
    for index in range(len(ratios) - 1, -1, -1):
        if ratios[index] >= threshold:
            break
        sustained = self.n_qubits[index]
    if sustained is not None:
        return sustained

    slope = self.haar_normalized_slope
    if not math.isfinite(slope) or slope >= 0.0:
        return None

    # log(ratio(n)) is linear in n. Anchor the extrapolation on the widest
    # measured point so it starts from data, not from the fitted intercept.
    n_last = self.n_qubits[-1]
    ratio_last = self.concentration_ratios[-1]
    if ratio_last <= 0.0:
        return n_last
    steps = (math.log(threshold) - math.log(ratio_last)) / slope
    return int(n_last + math.ceil(max(steps, 0.0)))

shots_per_entry_at

shots_per_entry_at(n_qubits: int) -> float

Extrapolate the per-entry shot requirement to a wider circuit.

Propagates the mean and the variance forward with their own fitted decay rates and re-evaluates 4 K_bar (1 - K_bar) / Var. Using both fits matters: the relative spread of the entries is not preserved as the circuit widens, so scaling the standard deviation with the mean would be wrong by an exponential factor.

Parameters

n_qubits : int Target circuit width. Must be a positive integer. Widths at or below the measured range return the nearest measurement instead of an extrapolation.

Returns

float Extrapolated shots per kernel entry, or nan if no fit exists.

Raises

ValueError If n_qubits is not a positive integer.

Warnings

Beyond the measured range this is an extrapolation, not a measurement. Check :attr:r_squared.

Source code in src/encoding_atlas/analysis/concentration.py
def shots_per_entry_at(self, n_qubits: int) -> float:
    """Extrapolate the per-entry shot requirement to a wider circuit.

    Propagates the mean and the variance forward with their own fitted
    decay rates and re-evaluates ``4 K_bar (1 - K_bar) / Var``. Using both
    fits matters: the relative spread of the entries is not preserved as
    the circuit widens, so scaling the standard deviation with the mean
    would be wrong by an exponential factor.

    Parameters
    ----------
    n_qubits : int
        Target circuit width. Must be a positive integer. Widths at or
        below the measured range return the nearest measurement instead of
        an extrapolation.

    Returns
    -------
    float
        Extrapolated shots per kernel entry, or ``nan`` if no fit exists.

    Raises
    ------
    ValueError
        If ``n_qubits`` is not a positive integer.

    Warnings
    --------
    Beyond the measured range this is an extrapolation, not a measurement.
    Check :attr:`r_squared`.
    """
    if isinstance(n_qubits, bool) or not isinstance(n_qubits, (int, np.integer)):
        raise ValueError(f"n_qubits must be a positive integer, got {n_qubits!r}")
    if n_qubits < 1:
        raise ValueError(f"n_qubits must be a positive integer, got {n_qubits!r}")
    if not self.results:
        return math.nan

    target = int(n_qubits)
    steps = target - self.n_qubits[-1]
    if steps <= 0:
        closest = min(self.results, key=lambda r: abs(r.n_qubits - target))
        return closest.shots_per_entry

    if not (
        math.isfinite(self.decay_rate)
        and math.isfinite(self.mean_decay_rate)
        and self.decay_rate > 0.0
        and self.mean_decay_rate > 0.0
    ):
        return math.nan

    last = self.results[-1]
    mean = last.offdiagonal_mean / (self.mean_decay_rate**steps)
    variance = last.offdiagonal_variance / (self.decay_rate**steps)
    return _shots_to_resolve(mean, variance)

Feature-Scaling Sensitivity

How much the range features are scaled into changes an encoding's kernel geometry — for several encodings, more than the choice of encoding does. See Feature Scaling.

scan_feature_ranges

scan_feature_ranges(encoding: BaseEncoding, X: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]], *, ranges: Sequence[tuple[float, float]] = DEFAULT_FEATURE_RANGES, max_samples: int = _DEFAULT_MAX_SAMPLES, centered: bool = True, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> FeatureRangeScan

Measure an encoding's kernel geometry across feature-scaling ranges.

For each candidate range the data is min-max scaled into it, the fidelity kernel is built, and both the kernel-target alignment and the kernel concentration are recorded. No training is involved.

Use it to choose a range (see :func:recommend_feature_range) and to see how much the choice matters — :attr:FeatureRangeScan.alignment_spread is often comparable to the spread across encodings.

Parameters

encoding : BaseEncoding Encoding to scan. Built once and reused across ranges. X : ndarray of shape (n_samples, n_features) Unscaled feature matrix. Whatever scaling it already carries is overwritten, so pass raw features. y : ndarray of shape (n_samples,) Two-class labels. Any two distinct values work. ranges : sequence of (float, float), default=:data:DEFAULT_FEATURE_RANGES Candidate ranges. Each must satisfy low < high. max_samples : int, default=200 Sub-sample above this many rows. Must be at least 2. centered : bool, default=True Use the centred alignment (Cortes et al., 2012). seed : int or None, default=None Seed for sub-sampling. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

FeatureRangeScan One measurement per range, plus the best range and the spread.

Raises

ValueError If X/y are malformed or mismatched, y is not two-class, max_samples is invalid, or any range is invalid.

Examples

from encoding_atlas import IQPEncoding from encoding_atlas.benchmark import get_dataset X, y = get_dataset("moons", n_samples=40, seed=0) scan = scan_feature_ranges(IQPEncoding(n_features=2, reps=2), X, y, seed=0) len(scan.results) 4

See Also

recommend_feature_range : Just the best range. encoding_atlas.analysis.compute_kernel_concentration : The concentration axis.

Source code in src/encoding_atlas/analysis/scaling.py
def scan_feature_ranges(
    encoding: BaseEncoding,
    X: NDArray[np.floating[Any]],
    y: NDArray[np.integer[Any] | np.floating[Any]],
    *,
    ranges: Sequence[tuple[float, float]] = DEFAULT_FEATURE_RANGES,
    max_samples: int = _DEFAULT_MAX_SAMPLES,
    centered: bool = True,
    seed: int | None = None,
    backend: Literal["pennylane", "qiskit", "cirq"] = "pennylane",
) -> FeatureRangeScan:
    """Measure an encoding's kernel geometry across feature-scaling ranges.

    For each candidate range the data is min-max scaled into it, the fidelity
    kernel is built, and both the kernel-target alignment and the kernel
    concentration are recorded. No training is involved.

    Use it to choose a range (see :func:`recommend_feature_range`) and to see
    how much the choice matters — :attr:`FeatureRangeScan.alignment_spread`
    is often comparable to the spread across *encodings*.

    Parameters
    ----------
    encoding : BaseEncoding
        Encoding to scan. Built once and reused across ranges.
    X : ndarray of shape (n_samples, n_features)
        Unscaled feature matrix. Whatever scaling it already carries is
        overwritten, so pass raw features.
    y : ndarray of shape (n_samples,)
        Two-class labels. Any two distinct values work.
    ranges : sequence of (float, float), default=:data:`DEFAULT_FEATURE_RANGES`
        Candidate ranges. Each must satisfy ``low < high``.
    max_samples : int, default=200
        Sub-sample above this many rows. Must be at least 2.
    centered : bool, default=True
        Use the centred alignment (Cortes et al., 2012).
    seed : int or None, default=None
        Seed for sub-sampling.
    backend : {"pennylane", "qiskit", "cirq"}, default="pennylane"
        Statevector simulation backend.

    Returns
    -------
    FeatureRangeScan
        One measurement per range, plus the best range and the spread.

    Raises
    ------
    ValueError
        If ``X``/``y`` are malformed or mismatched, ``y`` is not two-class,
        ``max_samples`` is invalid, or any range is invalid.

    Examples
    --------
    >>> from encoding_atlas import IQPEncoding
    >>> from encoding_atlas.benchmark import get_dataset
    >>> X, y = get_dataset("moons", n_samples=40, seed=0)
    >>> scan = scan_feature_ranges(IQPEncoding(n_features=2, reps=2), X, y, seed=0)
    >>> len(scan.results)
    4

    See Also
    --------
    recommend_feature_range : Just the best range.
    encoding_atlas.analysis.compute_kernel_concentration : The concentration axis.
    """
    from encoding_atlas.analysis.concentration import summarize_kernel_concentration
    from encoding_atlas.analysis.generalization import (
        centered_kernel_target_alignment,
        compute_fidelity_kernel,
        kernel_target_alignment,
        validate_binary_labels,
    )

    if isinstance(max_samples, bool) or not isinstance(max_samples, int):
        raise ValueError(f"max_samples must be an integer >= 2, got {max_samples!r}")
    if max_samples < 2:
        raise ValueError(f"max_samples must be an integer >= 2, got {max_samples!r}")

    resolved_ranges = _validate_ranges(ranges)

    X_array = np.asarray(X, dtype=np.float64)
    if X_array.ndim != 2:
        raise ValueError(f"X must be a 2D array, got shape {X_array.shape}")
    if X_array.shape[0] < 2:
        raise ValueError(f"X must contain at least 2 samples, got {X_array.shape[0]}")
    if not np.all(np.isfinite(X_array)):
        raise ValueError("X contains NaN or infinite values")

    y_float = validate_binary_labels(y)
    if y_float.shape[0] != X_array.shape[0]:
        raise ValueError(
            f"X and y must have the same length, got {X_array.shape[0]} and "
            f"{y_float.shape[0]}"
        )

    n_supplied = X_array.shape[0]
    indices = _subsample(n_supplied, max_samples, seed)
    X_used, y_used = X_array[indices], y_float[indices]

    score = centered_kernel_target_alignment if centered else kernel_target_alignment
    n_qubits = int(encoding.n_qubits)

    results: list[FeatureRangeResult] = []
    for low, high in resolved_ranges:
        K = compute_fidelity_kernel(
            encoding, scale_to_range(X_used, low, high), backend=backend
        )
        summary = summarize_kernel_concentration(K, n_qubits)
        results.append(
            FeatureRangeResult(
                low=low,
                high=high,
                width=high - low,
                alignment=float(score(K, y_used)),
                concentration_ratio=summary.concentration_ratio,
                offdiagonal_mean=summary.offdiagonal_mean,
                offdiagonal_variance=summary.offdiagonal_variance,
            )
        )

    logger.debug(
        "Feature-range scan for %s: best=%s spread=%.3f",
        type(encoding).__name__,
        max(results, key=lambda r: r.alignment) if results else None,
        max(r.alignment for r in results) - min(r.alignment for r in results),
    )

    return FeatureRangeScan(
        encoding_name=type(encoding).__name__,
        n_qubits=n_qubits,
        n_features=int(X_array.shape[1]),
        n_samples_used=int(len(X_used)),
        n_samples_supplied=int(n_supplied),
        centered=bool(centered),
        results=tuple(results),
    )

recommend_feature_range

recommend_feature_range(encoding: BaseEncoding, X: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]], *, ranges: Sequence[tuple[float, float]] = DEFAULT_FEATURE_RANGES, max_samples: int = _DEFAULT_MAX_SAMPLES, centered: bool = True, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> tuple[float, float]

Return the feature range that maximises alignment for encoding.

A thin wrapper over :func:scan_feature_ranges for the common case of "just tell me what to scale into". Ties break towards the narrower range.

Parameters

encoding, X, y, ranges, max_samples, centered, seed, backend As for :func:scan_feature_ranges.

Returns

(float, float) The recommended (low, high). Feed it to :func:scale_to_range.

Examples

from encoding_atlas import IQPEncoding from encoding_atlas.benchmark import get_dataset X, y = get_dataset("moons", n_samples=40, seed=0) low, high = recommend_feature_range( ... IQPEncoding(n_features=2, reps=2), X, y, seed=0 ... ) low == 0.0 True

Source code in src/encoding_atlas/analysis/scaling.py
def recommend_feature_range(
    encoding: BaseEncoding,
    X: NDArray[np.floating[Any]],
    y: NDArray[np.integer[Any] | np.floating[Any]],
    *,
    ranges: Sequence[tuple[float, float]] = DEFAULT_FEATURE_RANGES,
    max_samples: int = _DEFAULT_MAX_SAMPLES,
    centered: bool = True,
    seed: int | None = None,
    backend: Literal["pennylane", "qiskit", "cirq"] = "pennylane",
) -> tuple[float, float]:
    """Return the feature range that maximises alignment for ``encoding``.

    A thin wrapper over :func:`scan_feature_ranges` for the common case of
    "just tell me what to scale into". Ties break towards the narrower range.

    Parameters
    ----------
    encoding, X, y, ranges, max_samples, centered, seed, backend
        As for :func:`scan_feature_ranges`.

    Returns
    -------
    (float, float)
        The recommended ``(low, high)``. Feed it to :func:`scale_to_range`.

    Examples
    --------
    >>> from encoding_atlas import IQPEncoding
    >>> from encoding_atlas.benchmark import get_dataset
    >>> X, y = get_dataset("moons", n_samples=40, seed=0)
    >>> low, high = recommend_feature_range(
    ...     IQPEncoding(n_features=2, reps=2), X, y, seed=0
    ... )
    >>> low == 0.0
    True
    """
    return scan_feature_ranges(
        encoding,
        X,
        y,
        ranges=ranges,
        max_samples=max_samples,
        centered=centered,
        seed=seed,
        backend=backend,
    ).best_range

scale_to_range

scale_to_range(X: NDArray[floating[Any]], low: float, high: float, *, reference: NDArray[floating[Any]] | None = None) -> NDArray[float64]

Min-max scale each feature of X into [low, high].

Parameters

X : ndarray of shape (n_samples, n_features) Data to transform. low, high : float Target interval. Must satisfy low < high and both be finite. reference : ndarray of shape (m, n_features), optional Fit the per-feature minimum and span on this array instead of on X. Pass the training fold here to transform a test fold without leaking its extremes into the scaler — transformed values may then fall outside [low, high], which is correct and matches :class:sklearn.preprocessing.MinMaxScaler. Defaults to X.

Returns

ndarray of shape (n_samples, n_features) The scaled data. Constant features map to low.

Raises

ValueError If X is not 2D, the range is invalid or non-finite, or reference has a different feature count.

Examples

import numpy as np X = np.array([[0.0, 10.0], [1.0, 20.0]]) np.round(scale_to_range(X, 0.0, np.pi), 4) array([[0. , 0. ], [3.1416, 3.1416]])

Source code in src/encoding_atlas/analysis/scaling.py
def scale_to_range(
    X: NDArray[np.floating[Any]],
    low: float,
    high: float,
    *,
    reference: NDArray[np.floating[Any]] | None = None,
) -> NDArray[np.float64]:
    """Min-max scale each feature of ``X`` into ``[low, high]``.

    Parameters
    ----------
    X : ndarray of shape (n_samples, n_features)
        Data to transform.
    low, high : float
        Target interval. Must satisfy ``low < high`` and both be finite.
    reference : ndarray of shape (m, n_features), optional
        Fit the per-feature minimum and span on this array instead of on
        ``X``. Pass the training fold here to transform a test fold without
        leaking its extremes into the scaler — transformed values may then
        fall outside ``[low, high]``, which is correct and matches
        :class:`sklearn.preprocessing.MinMaxScaler`. Defaults to ``X``.

    Returns
    -------
    ndarray of shape (n_samples, n_features)
        The scaled data. Constant features map to ``low``.

    Raises
    ------
    ValueError
        If ``X`` is not 2D, the range is invalid or non-finite, or
        ``reference`` has a different feature count.

    Examples
    --------
    >>> import numpy as np
    >>> X = np.array([[0.0, 10.0], [1.0, 20.0]])
    >>> np.round(scale_to_range(X, 0.0, np.pi), 4)
    array([[0.    , 0.    ],
           [3.1416, 3.1416]])
    """
    if not (math.isfinite(low) and math.isfinite(high)):
        raise ValueError(f"range must be finite, got ({low}, {high})")
    if low >= high:
        raise ValueError(f"range must satisfy low < high, got ({low}, {high})")

    X_array = np.asarray(X, dtype=np.float64)
    if X_array.ndim != 2:
        raise ValueError(f"X must be a 2D array, got shape {X_array.shape}")

    source = X_array if reference is None else np.asarray(reference, dtype=np.float64)
    if source.ndim != 2:
        raise ValueError(f"reference must be a 2D array, got shape {source.shape}")
    if source.shape[1] != X_array.shape[1]:
        raise ValueError(
            f"reference has {source.shape[1]} features but X has " f"{X_array.shape[1]}"
        )
    if source.shape[0] == 0:
        raise ValueError("reference must contain at least one row")

    mins = source.min(axis=0)
    span = source.max(axis=0) - mins
    span = np.where(span <= _CONSTANT_FEATURE_EPS, 1.0, span)
    scaled: NDArray[np.float64] = low + (X_array - mins) / span * (high - low)
    # A constant feature has no information; pin it to the range floor rather
    # than letting the placeholder span produce an arbitrary offset.
    constant = (source.max(axis=0) - mins) <= _CONSTANT_FEATURE_EPS
    if np.any(constant):
        scaled[:, constant] = low
    return scaled

FeatureRangeScan dataclass

FeatureRangeScan(encoding_name: str, n_qubits: int, n_features: int, n_samples_used: int, n_samples_supplied: int, centered: bool, results: tuple[FeatureRangeResult, ...])

How an encoding's kernel geometry responds to feature scaling.

Attributes

encoding_name : str Class name of the scanned encoding. n_qubits, n_features : int Circuit width and input dimensionality. n_samples_used, n_samples_supplied : int Rows used after sub-sampling, and rows supplied. centered : bool Whether the centred alignment variant was used. results : tuple[FeatureRangeResult, ...] One measurement per range, in the order scanned.

best property

The range with the highest alignment.

Ties break towards the narrower range, which is the more conservative choice: it keeps the kernel further from the concentration floor.

Raises

RuntimeError If the scan produced no measurements.

best_range property

best_range: tuple[float, float]

(low, high) of the highest-aligned range.

alignment_spread property

alignment_spread: float

Highest minus lowest alignment across the scanned ranges.

A large spread means the scaling choice matters as much as, or more than, the choice of encoding — so it should be reported, not assumed.

at

at(low: float, high: float) -> FeatureRangeResult | None

Return the measurement for a given range, or None.

Source code in src/encoding_atlas/analysis/scaling.py
def at(self, low: float, high: float) -> FeatureRangeResult | None:
    """Return the measurement for a given range, or ``None``."""
    for result in self.results:
        if math.isclose(result.low, low) and math.isclose(result.high, high):
            return result
    return None

FeatureRangeResult dataclass

FeatureRangeResult(low: float, high: float, width: float, alignment: float, concentration_ratio: float, offdiagonal_mean: float, offdiagonal_variance: float)

An encoding measured at one feature-scaling range.

Attributes

low, high : float The range the features were scaled into. width : float high - low, as a convenience for plotting against period coverage. alignment : float Kernel-target alignment at this range, in [-1, 1]. Higher predicts better downstream accuracy. concentration_ratio : float Off-diagonal kernel variance over the Haar variance. Near 1 and with offdiagonal_mean near 1/2**n_qubits means the kernel has collapsed onto the Haar floor. Not monotone in the range width on its own — see the module docstring. offdiagonal_mean, offdiagonal_variance : float Raw moments of the kernel's off-diagonal entries. The mean is the cleaner read on how close the kernel is to the floor: it decreases towards 1/2**n_qubits as the range widens.

Finite-Shot Kernel Estimation

sample_shot_kernel

sample_shot_kernel(K: NDArray[floating[Any]], shots: int, *, seed: int | None = None) -> NDArray[floating[Any]]

Return the finite-shot estimate of an exact fidelity kernel.

Models the compute-uncompute estimator used on hardware: for each pair (i, j) the circuit U(x_j)^dagger U(x_i)|0> is measured shots times and the fraction of all-zeros outcomes is taken as K[i, j]. Because the all-zeros probability is K[i, j], that count is exactly Binomial(shots, K[i, j]) — so drawing from the binomial is statistically identical to simulating the circuits, at a fraction of the cost.

The estimate is unbiased entrywise and symmetric, and its diagonal is exactly 1 (the compute-uncompute circuit for x_i = x_i returns all-zeros with certainty). It is not guaranteed to be positive semidefinite: independent noise on each entry routinely pushes the smallest eigenvalues negative. Project with :func:encoding_atlas.benchmark.kernel.ensure_psd before handing it to a precomputed-kernel estimator.

Parameters

K : ndarray, shape (n, n) Exact kernel with entries in [0, 1], e.g. from :func:compute_fidelity_kernel. shots : int Measurements per entry. Must be a positive integer. seed : int or None, default=None Seed for the binomial draws.

Returns

ndarray, shape (n, n) Symmetric finite-shot estimate with unit diagonal. Entries are multiples of 1 / shots.

Raises

ValueError If shots is not a positive integer, or K is not a square 2D matrix with entries in [0, 1].

Examples

import numpy as np K = np.array([[1.0, 0.5], [0.5, 1.0]]) K_hat = sample_shot_kernel(K, shots=1000, seed=0) bool(K_hat[0, 0] == 1.0 and K_hat[0, 1] == K_hat[1, 0]) True

Notes

The variance of each off-diagonal estimate is K (1 - K) / shots. When the kernel has concentrated — every off-diagonal entry near 2**-n — that noise swamps the signal unless shots grows with the Hilbert-space dimension; see :func:encoding_atlas.analysis.concentration.compute_kernel_concentration for the resulting budget.

Source code in src/encoding_atlas/analysis/generalization.py
def sample_shot_kernel(
    K: NDArray[np.floating[Any]],
    shots: int,
    *,
    seed: int | None = None,
) -> NDArray[np.floating[Any]]:
    """Return the finite-shot estimate of an exact fidelity kernel.

    Models the *compute-uncompute* estimator used on hardware: for each pair
    ``(i, j)`` the circuit ``U(x_j)^dagger U(x_i)|0>`` is measured ``shots``
    times and the fraction of all-zeros outcomes is taken as ``K[i, j]``.
    Because the all-zeros probability *is* ``K[i, j]``, that count is exactly
    ``Binomial(shots, K[i, j])`` — so drawing from the binomial is
    statistically identical to simulating the circuits, at a fraction of the
    cost.

    The estimate is unbiased entrywise and symmetric, and its diagonal is
    exactly 1 (the compute-uncompute circuit for ``x_i = x_i`` returns
    all-zeros with certainty). It is **not** guaranteed to be positive
    semidefinite: independent noise on each entry routinely pushes the
    smallest eigenvalues negative. Project with
    :func:`encoding_atlas.benchmark.kernel.ensure_psd` before handing it to a
    precomputed-kernel estimator.

    Parameters
    ----------
    K : ndarray, shape (n, n)
        Exact kernel with entries in ``[0, 1]``, e.g. from
        :func:`compute_fidelity_kernel`.
    shots : int
        Measurements per entry. Must be a positive integer.
    seed : int or None, default=None
        Seed for the binomial draws.

    Returns
    -------
    ndarray, shape (n, n)
        Symmetric finite-shot estimate with unit diagonal. Entries are
        multiples of ``1 / shots``.

    Raises
    ------
    ValueError
        If ``shots`` is not a positive integer, or ``K`` is not a square 2D
        matrix with entries in ``[0, 1]``.

    Examples
    --------
    >>> import numpy as np
    >>> K = np.array([[1.0, 0.5], [0.5, 1.0]])
    >>> K_hat = sample_shot_kernel(K, shots=1000, seed=0)
    >>> bool(K_hat[0, 0] == 1.0 and K_hat[0, 1] == K_hat[1, 0])
    True

    Notes
    -----
    The variance of each off-diagonal estimate is ``K (1 - K) / shots``. When
    the kernel has concentrated — every off-diagonal entry near ``2**-n`` —
    that noise swamps the signal unless ``shots`` grows with the Hilbert-space
    dimension; see
    :func:`encoding_atlas.analysis.concentration.compute_kernel_concentration`
    for the resulting budget.
    """
    if isinstance(shots, bool) or not isinstance(shots, (int, np.integer)):
        raise ValueError(f"shots must be a positive integer, got {shots!r}")
    if shots < 1:
        raise ValueError(f"shots must be a positive integer, got {shots!r}")

    K_arr = np.asarray(K, dtype=np.float64)
    if K_arr.ndim != 2 or K_arr.shape[0] != K_arr.shape[1]:
        raise ValueError(f"K must be a square 2D matrix, got shape {K_arr.shape}")
    if K_arr.size and (float(K_arr.min()) < -_EPS or float(K_arr.max()) > 1.0 + _EPS):
        raise ValueError(
            f"K entries must lie in [0, 1], got "
            f"[{float(K_arr.min()):.6g}, {float(K_arr.max()):.6g}]"
        )

    n = K_arr.shape[0]
    estimate = np.eye(n, dtype=np.float64)
    if n < 2:
        return estimate

    rng = np.random.default_rng(seed)
    upper = np.triu_indices(n, k=1)
    probabilities = np.clip(K_arr[upper], 0.0, 1.0)
    sampled = rng.binomial(int(shots), probabilities) / float(shots)
    estimate[upper] = sampled
    estimate[(upper[1], upper[0])] = sampled
    return estimate

summarize_kernel_concentration

summarize_kernel_concentration(K: NDArray[floating[Any]], n_qubits: int, *, threshold: float = CONCENTRATION_THRESHOLD) -> ConcentrationResult

Reduce an already-computed fidelity kernel to concentration statistics.

Split out from :func:compute_kernel_concentration so a caller that already holds the kernel — screening several encodings, for instance — gets the concentration axis without paying for a second simulation, while both paths keep a single definition of the statistics.

Parameters

K : ndarray of shape (n, n) Fidelity kernel with entries in [0, 1]. Must have at least two rows so an off-diagonal pair exists. n_qubits : int Circuit width the kernel was produced at; sets the Haar reference. threshold : float, default=2.0 concentration_ratio below which is_concentrated is set.

Returns

ConcentrationResult The same statistics :func:compute_kernel_concentration returns.

Raises

ValueError If K is not a square 2D matrix of size >= 2, n_qubits is not a positive integer, or threshold is not positive.

Source code in src/encoding_atlas/analysis/concentration.py
def summarize_kernel_concentration(
    K: NDArray[np.floating[Any]],
    n_qubits: int,
    *,
    threshold: float = CONCENTRATION_THRESHOLD,
) -> ConcentrationResult:
    """Reduce an already-computed fidelity kernel to concentration statistics.

    Split out from :func:`compute_kernel_concentration` so a caller that
    already holds the kernel — screening several encodings, for instance —
    gets the concentration axis without paying for a second simulation, while
    both paths keep a single definition of the statistics.

    Parameters
    ----------
    K : ndarray of shape (n, n)
        Fidelity kernel with entries in ``[0, 1]``. Must have at least two
        rows so an off-diagonal pair exists.
    n_qubits : int
        Circuit width the kernel was produced at; sets the Haar reference.
    threshold : float, default=2.0
        ``concentration_ratio`` below which ``is_concentrated`` is set.

    Returns
    -------
    ConcentrationResult
        The same statistics :func:`compute_kernel_concentration` returns.

    Raises
    ------
    ValueError
        If ``K`` is not a square 2D matrix of size >= 2, ``n_qubits`` is not a
        positive integer, or ``threshold`` is not positive.
    """
    if threshold <= 0.0:
        raise ValueError(f"threshold must be positive, got {threshold}")
    K_arr = np.asarray(K, dtype=np.float64)
    if K_arr.ndim != 2 or K_arr.shape[0] != K_arr.shape[1]:
        raise ValueError(f"K must be a square 2D matrix, got shape {K_arr.shape}")
    if K_arr.shape[0] < 2:
        raise ValueError(
            f"K must be at least 2x2 to have an off-diagonal pair, got "
            f"{K_arr.shape[0]}x{K_arr.shape[0]}"
        )

    n = K_arr.shape[0]
    offdiag = K_arr[np.triu_indices(n, k=1)]
    mean = float(offdiag.mean())
    variance = float(offdiag.var())
    haar_mean, haar_variance = haar_kernel_moments(n_qubits)
    ratio = variance / haar_variance if haar_variance > 0.0 else math.inf

    return ConcentrationResult(
        n_qubits=int(n_qubits),
        n_samples=n,
        offdiagonal_mean=mean,
        offdiagonal_variance=variance,
        offdiagonal_std=float(math.sqrt(variance)),
        offdiagonal_min=float(offdiag.min()),
        offdiagonal_max=float(offdiag.max()),
        haar_mean=haar_mean,
        haar_variance=haar_variance,
        concentration_ratio=ratio,
        mean_ratio=mean / haar_mean if haar_mean > 0.0 else math.inf,
        identity_distance=float(math.sqrt(float(np.mean(offdiag**2)))),
        is_concentrated=bool(ratio < threshold),
        threshold=float(threshold),
        shots_per_entry=_shots_to_resolve(mean, variance),
    )

Guide Module

recommend_encoding

recommend_encoding(n_features: int, n_samples: int = 500, task: Literal['classification', 'regression'] = 'classification', hardware: str = 'simulator', priority: Literal['accuracy', 'trainability', 'speed', 'noise_resilience'] = 'accuracy', *, data_type: Literal['continuous', 'binary', 'discrete'] = 'continuous', symmetry: Literal['rotation', 'cyclic', 'permutation_pairs', 'general'] | None = None, trainable: bool = False, problem_structure: Literal['combinatorial', 'physics_simulation', 'time_series'] | None = None, feature_interactions: Literal['polynomial', 'custom_pauli'] | None = None) -> Recommendation

Recommend an encoding based on problem characteristics.

The recommendation is produced in two phases:

  1. Hard filter — encodings whose structural preconditions are not satisfied (data type, feature count, symmetry, trainability) are eliminated.
  2. Soft scoring — remaining candidates are scored on priority match, problem structure, task type, hardware suitability, and feature count. The highest-scoring encoding becomes the primary recommendation.

Parameters

n_features : int Number of input features (must be >= 1). n_samples : int Number of training samples (default 500, must be >= 1). task : {"classification", "regression"} Machine learning task type. Classification boosts kernel-method encodings; regression boosts universal-approximation encodings. hardware : str Target hardware ("simulator", "ibm", "ionq", etc.). priority : {"accuracy", "trainability", "speed", "noise_resilience"} Optimisation priority. data_type : {"continuous", "binary", "discrete"} Nature of the input features. symmetry : {"rotation", "cyclic", "permutation_pairs", "general"} | None Known symmetry in the data. None means no known symmetry. trainable : bool Whether the encoding should have learnable parameters. problem_structure : {"combinatorial", "physics_simulation", "time_series"} | None Domain structure of the problem. feature_interactions : {"polynomial", "custom_pauli"} | None Desired feature interaction type.

Returns

Recommendation The recommended encoding with explanation, alternatives, and confidence score.

Raises

ValueError If any parameter value is outside its valid set.

Source code in src/encoding_atlas/guide/recommender.py
def recommend_encoding(
    n_features: int,
    n_samples: int = 500,
    task: Literal["classification", "regression"] = "classification",
    hardware: str = "simulator",
    priority: Literal[
        "accuracy", "trainability", "speed", "noise_resilience"
    ] = "accuracy",
    *,
    data_type: Literal["continuous", "binary", "discrete"] = "continuous",
    symmetry: (
        Literal["rotation", "cyclic", "permutation_pairs", "general"] | None
    ) = None,
    trainable: bool = False,
    problem_structure: (
        Literal["combinatorial", "physics_simulation", "time_series"] | None
    ) = None,
    feature_interactions: Literal["polynomial", "custom_pauli"] | None = None,
) -> Recommendation:
    """Recommend an encoding based on problem characteristics.

    The recommendation is produced in two phases:

    1. **Hard filter** — encodings whose structural preconditions are not
       satisfied (data type, feature count, symmetry, trainability) are
       eliminated.
    2. **Soft scoring** — remaining candidates are scored on priority match,
       problem structure, task type, hardware suitability, and feature count.
       The highest-scoring encoding becomes the primary recommendation.

    Parameters
    ----------
    n_features : int
        Number of input features (must be >= 1).
    n_samples : int
        Number of training samples (default 500, must be >= 1).
    task : {"classification", "regression"}
        Machine learning task type.  Classification boosts kernel-method
        encodings; regression boosts universal-approximation encodings.
    hardware : str
        Target hardware (``"simulator"``, ``"ibm"``, ``"ionq"``, etc.).
    priority : {"accuracy", "trainability", "speed", "noise_resilience"}
        Optimisation priority.
    data_type : {"continuous", "binary", "discrete"}
        Nature of the input features.
    symmetry : {"rotation", "cyclic", "permutation_pairs", "general"} | None
        Known symmetry in the data.  ``None`` means no known symmetry.
    trainable : bool
        Whether the encoding should have learnable parameters.
    problem_structure : {"combinatorial", "physics_simulation", "time_series"} | None
        Domain structure of the problem.
    feature_interactions : {"polynomial", "custom_pauli"} | None
        Desired feature interaction type.

    Returns
    -------
    Recommendation
        The recommended encoding with explanation, alternatives, and
        confidence score.

    Raises
    ------
    ValueError
        If any parameter value is outside its valid set.
    """
    _validate_recommend_inputs(
        n_features=n_features,
        n_samples=n_samples,
        task=task,
        hardware=hardware,
        priority=priority,
        data_type=data_type,
        symmetry=symmetry,
        trainable=trainable,
        problem_structure=problem_structure,
        feature_interactions=feature_interactions,
    )

    # Phase A — hard filter
    candidates: dict[str, EncodingRule] = {}
    for name, rules in ENCODING_RULES.items():
        if _passes_hard_constraints(
            rules,
            n_features=n_features,
            data_type=data_type,
            symmetry=symmetry,
            trainable=trainable,
        ):
            candidates[name] = rules

    # Defensive fallback — unreachable in practice because ``angle`` has no
    # hard constraints, but kept for safety.  Uses ``_score_to_confidence``
    # to remain consistent with the documented confidence range [0.50, 0.95].
    if not candidates:
        return Recommendation(
            encoding_name="angle",
            explanation=(
                "No encoding matches all constraints; angle encoding "
                "is the safest general-purpose fallback"
            ),
            alternatives=[],
            confidence=_score_to_confidence(0.0),
        )

    # Check whether the user's symmetry constraint survived the hard filter.
    # If no candidate natively supports the requested symmetry, we append a
    # note to the explanation so the user understands the fallback.
    _symmetry_note = ""
    if symmetry is not None:
        symmetry_survived = any(
            r["requires_symmetry"] == symmetry for r in candidates.values()
        )
        if not symmetry_survived:
            _symmetry_note = (
                f" Note: no encoding with {symmetry!r} symmetry support "
                f"is compatible with n_features={n_features}; "
                f"falling back to general recommendation."
            )

    # Phase B — score candidates
    scores: dict[str, float] = {}
    for name, rules in candidates.items():
        scores[name] = _compute_score(
            name,
            rules,
            n_features=n_features,
            n_samples=n_samples,
            task=task,
            hardware=hardware,
            priority=priority,
            data_type=data_type,
            symmetry=symmetry,
            trainable=trainable,
            problem_structure=problem_structure,
            feature_interactions=feature_interactions,
        )

    # Rank and select — descending score, alphabetical name for ties.
    ranked = sorted(scores.items(), key=lambda item: (-item[1], item[0]))
    best_name, best_score = ranked[0]
    alternatives = [name for name, _ in ranked[1:4]]

    confidence = _score_to_confidence(best_score)

    explanation = (
        _generate_explanation(
            best_name,
            candidates[best_name],
            priority=priority,
            n_features=n_features,
        )
        + _symmetry_note
    )

    return Recommendation(
        encoding_name=best_name,
        explanation=explanation,
        alternatives=alternatives,
        confidence=confidence,
        scale_warning=_scale_warning(best_name, n_features, task),
    )

Recommendation dataclass

Recommendation(encoding_name: str, explanation: str, alternatives: list[str], confidence: float, scale_warning: str | None = None)

Encoding recommendation result.

Attributes

encoding_name : str Canonical name of the recommended encoding (matches registry keys). explanation : str Human-readable rationale for the recommendation. alternatives : list[str] Up to three runner-up encoding names, ranked by score. confidence : float Confidence in the recommendation, in [0, 1]. scale_warning : str or None Set when the recommended encoding's fidelity kernel is measured to sit at the Haar floor at this feature count — meaning a quantum-kernel method built on it has no geometry left to learn from, and the shot cost per kernel entry grows exponentially with circuit width. None when no such warning applies. Kept out of explanation so callers that format the rationale are unaffected; see :mod:encoding_atlas.atlas.concentration for the underlying scan.

get_matching_encodings

get_matching_encodings(requirements: list[str], constraints: list[str] | None = None, *, n_features: int | None = None, data_type: str = 'continuous', symmetry: str | None = None, trainable: bool = False) -> list[str]

Get encodings matching requirements, constraints, and hard filters.

This performs a two-phase check for each encoding:

  1. Hard filter — eliminates encodings whose structural preconditions are not met (data type, feature count, symmetry, trainability).
  2. Soft match — among survivors, selects those whose best_for tags overlap with requirements and whose avoid_when tags do not overlap with constraints.

Parameters

requirements : list[str] Tags the encoding should be good at (matched against best_for). constraints : list[str] | None Tags the encoding should not be associated with (matched against avoid_when). None means no soft constraints. n_features : int | None Number of input features for hard-filter checks. data_type : str Data type for hard-filter checks. symmetry : str | None Symmetry type for hard-filter checks. trainable : bool Trainable flag for hard-filter checks.

Returns

list[str] Encoding names that pass both hard and soft filters.

Source code in src/encoding_atlas/guide/rules.py
def get_matching_encodings(
    requirements: list[str],
    constraints: list[str] | None = None,
    *,
    n_features: int | None = None,
    data_type: str = "continuous",
    symmetry: str | None = None,
    trainable: bool = False,
) -> list[str]:
    """Get encodings matching requirements, constraints, and hard filters.

    This performs a two-phase check for each encoding:

    1. **Hard filter** — eliminates encodings whose structural preconditions
       are not met (data type, feature count, symmetry, trainability).
    2. **Soft match** — among survivors, selects those whose ``best_for`` tags
       overlap with *requirements* and whose ``avoid_when`` tags do not overlap
       with *constraints*.

    Parameters
    ----------
    requirements : list[str]
        Tags the encoding should be good at (matched against ``best_for``).
    constraints : list[str] | None
        Tags the encoding should *not* be associated with (matched against
        ``avoid_when``).  ``None`` means no soft constraints.
    n_features : int | None
        Number of input features for hard-filter checks.
    data_type : str
        Data type for hard-filter checks.
    symmetry : str | None
        Symmetry type for hard-filter checks.
    trainable : bool
        Trainable flag for hard-filter checks.

    Returns
    -------
    list[str]
        Encoding names that pass both hard and soft filters.
    """
    matches: list[str] = []

    for name, rules in ENCODING_RULES.items():
        # Phase A — hard filter
        if not _passes_hard_constraints(
            rules,
            n_features=n_features,
            data_type=data_type,
            symmetry=symmetry,
            trainable=trainable,
        ):
            continue

        # Phase B — soft tag match
        if any(req in rules["best_for"] for req in requirements):
            if constraints:
                if not any(c in rules["avoid_when"] for c in constraints):
                    matches.append(name)
            else:
                matches.append(name)

    return matches

Data-Driven Screening

Rank candidate encodings by kernel-target alignment measured on your own dataset — the training-free predictor of kernel accuracy. See Screening on Your Data.

screen_encodings

screen_encodings(X: NDArray[floating[Any]], y: NDArray[integer[Any] | floating[Any]], *, candidates: Sequence[str] | None = None, max_samples: int = _DEFAULT_MAX_SAMPLES, centered: bool = True, include_concentration: bool = False, feature_ranges: Sequence[tuple[float, float]] | None = None, shots: int | None = None, seed: int | None = None, backend: Literal['pennylane', 'qiskit', 'cirq'] = 'pennylane') -> ScreeningResult

Rank candidate encodings by kernel-target alignment on your data.

Builds each candidate at X.shape[1] features, computes its fidelity kernel on the data, and scores it by alignment with the labels — no training. Encodings that cannot be built at this width (SO(2) equivariance needs exactly two features, the swap-equivariant map an even count) are reported in :attr:ScreeningResult.skipped rather than raising.

Use the result as a shortlist: train the top few and compare them properly. The top-1 pick alone is not reliably better than the library's metadata recommendation — see the module docstring for the measured numbers.

Parameters

X : ndarray of shape (n_samples, n_features) Feature matrix. Scale it as you would before training; alignment depends on the input distribution, not just the circuit. y : ndarray of shape (n_samples,) Two-class labels. Any two distinct values work — {0, 1}, {1, 2} and {-1, +1} all describe the same split and score the same. Multi-class and continuous targets are rejected. candidates : sequence of str, optional Restrict to these encodings. Defaults to all 16 benchmarked ones, at the benchmark's own parameters. max_samples : int, default=200 Sub-sample (stratified, seeded) above this many rows to keep the screen fast. Must be at least 2. centered : bool, default=True Use the centred alignment (Cortes et al., 2012). This is the variant the benchmark validated; leave it on unless you have a reason not to. include_concentration : bool, default=False Also annotate each candidate with its kernel concentration on your data. Computed from the same kernel, so it costs no extra simulation. It does not affect the ranking. feature_ranges : sequence of (float, float), optional Also sweep the range the features are min-max scaled into, ranking over (encoding, range) pairs. The range is not a minor knob: for several encodings it moves accuracy further than the choice of encoding does, because a full 2*pi sweep drives the kernel onto its concentration floor. Pass :data:encoding_atlas.analysis.DEFAULT_FEATURE_RANGES for the usual sweep. When None (default) the data is used exactly as supplied. shots : int or None, default=None Screen on finite-shot kernel estimates instead of exact ones, to see what a device would report. Alignment aggregates over all pairs and so tolerates shot noise well. seed : int or None, default=None Seed for sub-sampling and shot sampling. Fixes the whole screen. backend : {"pennylane", "qiskit", "cirq"}, default="pennylane" Statevector simulation backend.

Returns

ScreeningResult Ranked candidates plus the reasons any were skipped.

Raises

ValueError If X/y are malformed or mismatched, y is not two-class, max_samples is invalid, or candidates names an unknown encoding.

Examples

from encoding_atlas.benchmark import get_dataset from encoding_atlas.guide import screen_encodings X, y = get_dataset("moons", n_samples=60, seed=0) result = screen_encodings(X, y, seed=0) shortlist = result.names(3) len(shortlist) 3

See Also

encoding_atlas.guide.recommend_encoding : Metadata-based recommendation. encoding_atlas.analysis.compute_kernel_target_alignment : The score. encoding_atlas.benchmark.evaluate_encoding : Train the shortlist.

Source code in src/encoding_atlas/guide/screening.py
def screen_encodings(
    X: NDArray[np.floating[Any]],
    y: NDArray[np.integer[Any] | np.floating[Any]],
    *,
    candidates: Sequence[str] | None = None,
    max_samples: int = _DEFAULT_MAX_SAMPLES,
    centered: bool = True,
    include_concentration: bool = False,
    feature_ranges: Sequence[tuple[float, float]] | None = None,
    shots: int | None = None,
    seed: int | None = None,
    backend: Literal["pennylane", "qiskit", "cirq"] = "pennylane",
) -> ScreeningResult:
    """Rank candidate encodings by kernel-target alignment on your data.

    Builds each candidate at ``X.shape[1]`` features, computes its fidelity
    kernel on the data, and scores it by alignment with the labels — no
    training. Encodings that cannot be built at this width (SO(2) equivariance
    needs exactly two features, the swap-equivariant map an even count) are
    reported in :attr:`ScreeningResult.skipped` rather than raising.

    Use the result as a **shortlist**: train the top few and compare them
    properly. The top-1 pick alone is not reliably better than the library's
    metadata recommendation — see the module docstring for the measured
    numbers.

    Parameters
    ----------
    X : ndarray of shape (n_samples, n_features)
        Feature matrix. Scale it as you would before training; alignment
        depends on the input distribution, not just the circuit.
    y : ndarray of shape (n_samples,)
        Two-class labels. Any two distinct values work — ``{0, 1}``,
        ``{1, 2}`` and ``{-1, +1}`` all describe the same split and score the
        same. Multi-class and continuous targets are rejected.
    candidates : sequence of str, optional
        Restrict to these encodings. Defaults to all 16 benchmarked ones, at
        the benchmark's own parameters.
    max_samples : int, default=200
        Sub-sample (stratified, seeded) above this many rows to keep the
        screen fast. Must be at least 2.
    centered : bool, default=True
        Use the centred alignment (Cortes et al., 2012). This is the variant
        the benchmark validated; leave it on unless you have a reason not to.
    include_concentration : bool, default=False
        Also annotate each candidate with its kernel concentration on your
        data. Computed from the same kernel, so it costs no extra simulation.
        It does not affect the ranking.
    feature_ranges : sequence of (float, float), optional
        Also sweep the range the features are min-max scaled into, ranking
        over (encoding, range) pairs. The range is not a minor knob: for
        several encodings it moves accuracy further than the choice of
        encoding does, because a full ``2*pi`` sweep drives the kernel onto
        its concentration floor. Pass
        :data:`encoding_atlas.analysis.DEFAULT_FEATURE_RANGES` for the usual
        sweep. When ``None`` (default) the data is used exactly as supplied.
    shots : int or None, default=None
        Screen on finite-shot kernel estimates instead of exact ones, to see
        what a device would report. Alignment aggregates over all pairs and so
        tolerates shot noise well.
    seed : int or None, default=None
        Seed for sub-sampling and shot sampling. Fixes the whole screen.
    backend : {"pennylane", "qiskit", "cirq"}, default="pennylane"
        Statevector simulation backend.

    Returns
    -------
    ScreeningResult
        Ranked candidates plus the reasons any were skipped.

    Raises
    ------
    ValueError
        If ``X``/``y`` are malformed or mismatched, ``y`` is not two-class,
        ``max_samples`` is invalid, or ``candidates`` names an unknown
        encoding.

    Examples
    --------
    >>> from encoding_atlas.benchmark import get_dataset
    >>> from encoding_atlas.guide import screen_encodings
    >>> X, y = get_dataset("moons", n_samples=60, seed=0)
    >>> result = screen_encodings(X, y, seed=0)
    >>> shortlist = result.names(3)
    >>> len(shortlist)
    3

    See Also
    --------
    encoding_atlas.guide.recommend_encoding : Metadata-based recommendation.
    encoding_atlas.analysis.compute_kernel_target_alignment : The score.
    encoding_atlas.benchmark.evaluate_encoding : Train the shortlist.
    """
    from encoding_atlas.analysis.concentration import summarize_kernel_concentration
    from encoding_atlas.analysis.generalization import (
        centered_kernel_target_alignment,
        compute_fidelity_kernel,
        kernel_target_alignment,
        validate_binary_labels,
    )
    from encoding_atlas.analysis.scaling import scale_to_range

    if isinstance(max_samples, bool) or not isinstance(max_samples, int):
        raise ValueError(f"max_samples must be an integer >= 2, got {max_samples!r}")
    if max_samples < 2:
        raise ValueError(f"max_samples must be an integer >= 2, got {max_samples!r}")

    X_array = np.asarray(X, dtype=np.float64)
    if X_array.ndim != 2:
        raise ValueError(f"X must be a 2D array, got shape {X_array.shape}")
    if X_array.shape[0] < 2:
        raise ValueError(f"X must contain at least 2 samples, got {X_array.shape[0]}")
    if not np.all(np.isfinite(X_array)):
        raise ValueError("X contains NaN or infinite values")

    y_float = validate_binary_labels(y)
    if y_float.shape[0] != X_array.shape[0]:
        raise ValueError(
            f"X and y must have the same length, got {X_array.shape[0]} and "
            f"{y_float.shape[0]}"
        )

    n_supplied, n_features = X_array.shape
    indices = _stratified_subsample(n_supplied, y_float, max_samples, seed)
    X_used, y_used = X_array[indices], y_float[indices]

    built, skipped = build_candidates(n_features, candidates)
    logger.debug(
        "Screening %d candidate(s) at n_features=%d on %d sample(s)",
        len(built),
        n_features,
        len(X_used),
    )

    resolved_ranges = _validate_feature_ranges(feature_ranges)

    score = centered_kernel_target_alignment if centered else kernel_target_alignment
    scored: list[_Scored] = []
    for name, encoding in built:
        for feature_range in resolved_ranges:
            X_scaled = (
                X_used
                if feature_range is None
                else scale_to_range(X_used, feature_range[0], feature_range[1])
            )
            try:
                K = compute_fidelity_kernel(
                    encoding, X_scaled, backend=backend, shots=shots, seed=seed
                )
                alignment = float(score(K, y_used))
                ratio: float | None = None
                concentrated: bool | None = None
                if include_concentration:
                    summary = summarize_kernel_concentration(K, int(encoding.n_qubits))
                    ratio = summary.concentration_ratio
                    concentrated = summary.is_concentrated
            except Exception as exc:  # noqa: BLE001 - one bad candidate must not abort
                skipped[name] = f"{type(exc).__name__}: {exc}"
                logger.debug("Screening skipped %s: %s", name, exc)
                continue
            scored.append(
                _Scored(name, encoding, alignment, feature_range, ratio, concentrated)
            )

    # Highest alignment first. Ties break on the narrower range (further from
    # the concentration floor) then on name, so the order is deterministic.
    scored.sort(
        key=lambda item: (
            -item.alignment,
            (
                item.feature_range[1] - item.feature_range[0]
                if item.feature_range
                else 0.0
            ),
            item.name,
        )
    )

    atlas_ranks, atlas_alignments = _atlas_reference()
    ranked = tuple(
        ScreenedEncoding(
            name=item.name,
            display_name=type(item.encoding).__name__,
            encoding=item.encoding,
            params=dict(BENCHMARK_PARAMS[item.name]),
            n_qubits=int(item.encoding.n_qubits),
            alignment=item.alignment,
            rank=position,
            feature_range=item.feature_range,
            atlas_rank=atlas_ranks.get(item.name),
            atlas_alignment=atlas_alignments.get(item.name),
            concentration_ratio=item.concentration_ratio,
            is_concentrated=item.is_concentrated,
        )
        for position, item in enumerate(scored, start=1)
    )

    return ScreeningResult(
        candidates=ranked,
        skipped=skipped,
        n_samples_used=int(len(X_used)),
        n_samples_supplied=int(n_supplied),
        n_features=int(n_features),
        centered=bool(centered),
        shots=shots,
        feature_ranges=(
            None if feature_ranges is None else tuple(resolved_ranges)  # type: ignore[arg-type]
        ),
    )

ScreeningResult dataclass

ScreeningResult(candidates: tuple[ScreenedEncoding, ...], skipped: Mapping[str, str], n_samples_used: int, n_samples_supplied: int, n_features: int, centered: bool, shots: int | None, feature_ranges: tuple[tuple[float, float], ...] | None = None)

Ranked outcome of screening candidate encodings on a dataset.

Attributes

candidates : tuple[ScreenedEncoding, ...] Successfully scored encodings, highest alignment first. Ties break on name so the order is deterministic. skipped : Mapping[str, str] Encodings that could not be built or scored at this feature count, mapped to the reason. n_samples_used : int Rows actually used, after any sub-sampling. n_samples_supplied : int Rows supplied by the caller. n_features : int Feature count screened at. centered : bool Whether the centred alignment variant was used. shots : int or None Shot budget per kernel entry, or None for exact kernels. feature_ranges : tuple[tuple[float, float], ...] or None Ranges swept, or None if the data was used as supplied.

top

top(k: int = _DEFAULT_TOP_K) -> list[ScreenedEncoding]

Return the k highest-aligned candidates.

Defaults to three, the shortlist size the benchmark supports: it captures nearly all achievable accuracy, whereas the single top pick is no better than the angle default. See the module docstring.

Parameters

k : int, default=3 Shortlist size. Must be a positive integer. Values beyond the number of scored candidates return all of them.

Returns

list[ScreenedEncoding]

Raises

ValueError If k is not a positive integer.

Source code in src/encoding_atlas/guide/screening.py
def top(self, k: int = _DEFAULT_TOP_K) -> list[ScreenedEncoding]:
    """Return the ``k`` highest-aligned candidates.

    Defaults to three, the shortlist size the benchmark supports: it
    captures nearly all achievable accuracy, whereas the single top pick
    is no better than the ``angle`` default. See the module docstring.

    Parameters
    ----------
    k : int, default=3
        Shortlist size. Must be a positive integer. Values beyond the
        number of scored candidates return all of them.

    Returns
    -------
    list[ScreenedEncoding]

    Raises
    ------
    ValueError
        If ``k`` is not a positive integer.
    """
    if isinstance(k, bool) or not isinstance(k, int):
        raise ValueError(f"k must be a positive integer, got {k!r}")
    if k < 1:
        raise ValueError(f"k must be a positive integer, got {k!r}")
    return list(self.candidates[:k])

names

names(k: int | None = None) -> list[str]

Return candidate names in ranked order, optionally the top k.

Source code in src/encoding_atlas/guide/screening.py
def names(self, k: int | None = None) -> list[str]:
    """Return candidate names in ranked order, optionally the top ``k``."""
    if k is None:
        return [c.name for c in self.candidates]
    return [c.name for c in self.top(k)]

best

best() -> ScreenedEncoding

Return the highest-aligned candidate.

Raises

RuntimeError If no candidate could be scored.

Source code in src/encoding_atlas/guide/screening.py
def best(self) -> ScreenedEncoding:
    """Return the highest-aligned candidate.

    Raises
    ------
    RuntimeError
        If no candidate could be scored.
    """
    if not self.candidates:
        raise RuntimeError(
            "No encoding could be screened; inspect `skipped` for reasons."
        )
    return self.candidates[0]

get

get(name: str) -> ScreenedEncoding | None

Return the scored candidate called name, or None.

Source code in src/encoding_atlas/guide/screening.py
def get(self, name: str) -> ScreenedEncoding | None:
    """Return the scored candidate called ``name``, or ``None``."""
    key = name.strip().lower()
    for candidate in self.candidates:
        if candidate.name.lower() == key:
            return candidate
    return None

ScreenedEncoding dataclass

ScreenedEncoding(name: str, display_name: str, encoding: BaseEncoding, params: Mapping[str, Any], n_qubits: int, alignment: float, rank: int, feature_range: tuple[float, float] | None, atlas_rank: int | None, atlas_alignment: float | None, concentration_ratio: float | None, is_concentrated: bool | None)

One candidate encoding, scored on the user's data.

Attributes

name : str Canonical encoding name, e.g. "angle". display_name : str Class name of the built encoding, e.g. "AngleEncoding". encoding : BaseEncoding The built instance, ready to train with — no need to rebuild it. params : Mapping[str, Any] Fixed parameters used, matching the benchmark's configuration. n_qubits : int Circuit width at the caller's feature count. alignment : float Kernel-target alignment on the supplied data, in [-1, 1]. Centred by default. This is the ranking key. rank : int 1-based position in the screen, 1 = highest alignment. atlas_rank : int or None The encoding's overall benchmark rank, or None if not benchmarked. atlas_alignment : float or None Mean alignment the benchmark measured, for reference. A large gap against :attr:alignment means your data behaves differently from the benchmark's, which is exactly when screening pays off. feature_range : tuple[float, float] or None The range the features were scaled into for this measurement, when feature_ranges was supplied. None means the data was used as given. Candidates are ranked across (encoding, range) pairs, so the same encoding can appear more than once at different ranges. concentration_ratio : float or None Kernel variance relative to the Haar floor on your data, when include_concentration=True. Near 1 means the kernel carries no usable geometry at this width. is_concentrated : bool or None Whether concentration_ratio fell below the concentration threshold. None when concentration was not requested.


Benchmark Module

Evaluate encodings on classification tasks with variational quantum classifiers and quantum-kernel SVMs, paired stratified cross-validation, classical baselines, and statistical comparison (Wilcoxon + Holm–Bonferroni + Cliff's delta).

All four estimators follow scikit-learn's estimator contract, so they compose with cross_val_score, GridSearchCV, Pipeline, VotingClassifier and other meta-estimators. Hyper-parameters are validated at fit time rather than in the constructor, as scikit-learn requires so that clone and set_params can rebuild an estimator from get_params().

EncodingBenchmark

EncodingBenchmark(encodings: list[BaseEncoding], datasets: list[str], n_runs: int = 10, seed: int | None = None, *, methods: tuple[str, ...] = ('vqc', 'kernel'), task: str = 'classification', n_folds: int = 5, baselines: tuple[str, ...] = (), custom_datasets: dict[str, tuple[Any, Any]] | None = None, n_samples: int = 200, vqc_layers: int = 2, vqc_epochs: int = 30, vqc_lr: float = 0.05, kernel_C: float = 1.0, kernel_alpha: float = 1.0, scale_range: tuple[float, float] = _DEFAULT_SCALE)

Framework for benchmarking encodings on classification datasets.

Parameters

encodings : list[BaseEncoding] Encodings to benchmark. Each must accept the datasets' feature count. datasets : list[str] Built-in dataset names (see :func:encoding_atlas.benchmark.list_datasets). n_runs : int, default=10 Independent repetitions per configuration (each a different CV split). seed : int or None, default=None Base random seed (None -> 0). methods : tuple, default=("vqc", "kernel") Quantum methods to evaluate; subset of ("vqc", "kernel"). task : {"classification", "regression"}, default="classification" Learning task. Regression draws from the regression dataset registry, uses K-fold splits, and reports R^2. n_folds : int, default=5 CV folds per run. baselines : tuple, default=() Classical baseline names for calibration — classifier names (e.g. ("svm_rbf",)) for classification, regressor names (e.g. ("svr_rbf",)) for regression. custom_datasets : dict or None, default=None Optional mapping name -> (X, y) of user-provided datasets, evaluated alongside the named datasets. n_samples : int, default=200 Sample count requested from the built-in dataset generators. vqc_layers, vqc_epochs, vqc_lr, kernel_C, scale_range Method hyper-parameters (see :func:evaluate_encoding).

Notes

Cost scales as len(encodings) x len(datasets) x len(methods) x n_runs x n_folds fold evaluations (VQC folds train a circuit and are the expensive part). Start small.

Source code in src/encoding_atlas/benchmark/runner.py
def __init__(
    self,
    encodings: list[BaseEncoding],
    datasets: list[str],
    n_runs: int = 10,
    seed: int | None = None,
    *,
    methods: tuple[str, ...] = ("vqc", "kernel"),
    task: str = "classification",
    n_folds: int = 5,
    baselines: tuple[str, ...] = (),
    custom_datasets: dict[str, tuple[Any, Any]] | None = None,
    n_samples: int = 200,
    vqc_layers: int = 2,
    vqc_epochs: int = 30,
    vqc_lr: float = 0.05,
    kernel_C: float = 1.0,
    kernel_alpha: float = 1.0,
    scale_range: tuple[float, float] = _DEFAULT_SCALE,
) -> None:
    if not encodings:
        raise ValueError("encodings must be a non-empty list")
    if not datasets and not custom_datasets:
        raise ValueError("provide at least one named or custom dataset")
    invalid = [m for m in methods if m not in _VALID_METHODS]
    if invalid:
        raise ValueError(f"invalid methods {invalid}; valid: {_VALID_METHODS}")
    if task not in _VALID_TASKS:
        raise ValueError(f"task must be one of {_VALID_TASKS}, got {task!r}")
    if n_runs < 1 or n_folds < 2:
        raise ValueError("n_runs must be >= 1 and n_folds must be >= 2")

    self.encodings = encodings
    self.datasets = datasets
    self.n_runs = n_runs
    self.seed = 0 if seed is None else seed
    self.methods = tuple(methods)
    self.task = task
    self.n_folds = n_folds
    self.baselines = tuple(baselines)
    self.custom_datasets = custom_datasets or {}
    self.n_samples = n_samples
    self._params = {
        "vqc_layers": vqc_layers,
        "vqc_epochs": vqc_epochs,
        "vqc_lr": vqc_lr,
        "kernel_C": kernel_C,
        "kernel_alpha": kernel_alpha,
    }
    self.scale_range = scale_range

    self.labels = _label_encodings(encodings)
    self.results: dict[str, Any] = {}
    # Raw per-(method, dataset) scores keyed by encoding label, for stats.
    self._raw: dict[tuple[str, str], dict[str, list[float]]] = {}

run

run() -> dict[str, Any]

Run the full benchmark and return a structured results dictionary.

Returns

dict {"config", "encodings", "datasets", "results", "baselines"}. results[method][label][dataset] holds the accuracy summary; mismatched (encoding, dataset) feature counts are recorded with status="skipped".

Source code in src/encoding_atlas/benchmark/runner.py
def run(self) -> dict[str, Any]:
    """Run the full benchmark and return a structured results dictionary.

    Returns
    -------
    dict
        ``{"config", "encodings", "datasets", "results", "baselines"}``.
        ``results[method][label][dataset]`` holds the accuracy summary;
        mismatched (encoding, dataset) feature counts are recorded with
        ``status="skipped"``.
    """
    data = self._resolve_datasets()
    results: dict[str, Any] = {
        m: {label: {} for label in self.labels} for m in self.methods
    }
    baselines: dict[str, Any] = {b: {} for b in self.baselines}
    score_key = _SCORE_KEY[self.task]
    bounded = self.task == "classification"

    for dname, (X, y) in data.items():
        # Pre-compute the shared fold splits per run for paired comparison.
        run_folds = [
            [
                _scale_fold(fold, self.scale_range)
                for fold in _make_folds(
                    X, y, self.n_folds, seed=self.seed + r, task=self.task
                )
            ]
            for r in range(self.n_runs)
        ]

        for method in self.methods:
            for enc, label in zip(self.encodings, self.labels):
                if getattr(enc, "n_features", X.shape[1]) != X.shape[1]:
                    results[method][label][dname] = {
                        "status": "skipped",
                        "reason": (
                            f"encoding n_features={enc.n_features} != "
                            f"dataset n_features={X.shape[1]}"
                        ),
                    }
                    continue

                scores: list[float] = []
                for r, folds in enumerate(run_folds):
                    for fold_idx, fold in enumerate(folds):
                        res = _run_method_fold(
                            method,
                            enc,
                            fold,
                            seed=(self.seed + r) * 100 + fold_idx,
                            params=self._params,
                            task=self.task,
                        )
                        if res["status"] == "success":
                            scores.append(res[score_key])

                results[method][label][dname] = {
                    "status": "success" if scores else "failed",
                    **_summarize(scores, bounded=bounded),
                }
                self._raw[(method, dname)] = self._raw.get((method, dname), {})
                self._raw[(method, dname)][label] = scores

        # Classical baselines (encoding-independent) — one pass per dataset.
        baseline_runner = (
            run_regression_baseline_single_fold
            if self.task == "regression"
            else run_baseline_single_fold
        )
        for bname in self.baselines:
            bscores: list[float] = []
            for r, folds in enumerate(run_folds):
                for fold_idx, (Xtr, Xte, ytr, yte) in enumerate(folds):
                    res = baseline_runner(
                        bname,
                        Xtr,
                        Xte,
                        ytr,
                        yte,
                        seed=(self.seed + r) * 100 + fold_idx,
                    )
                    if res["status"] == "success":
                        bscores.append(res[score_key])
            baselines[bname][dname] = {
                "status": "success" if bscores else "failed",
                **_summarize(bscores, bounded=bounded),
            }

    self.results = {
        "config": {
            "n_runs": self.n_runs,
            "n_folds": self.n_folds,
            "methods": list(self.methods),
            "task": self.task,
            "score_metric": _SCORE_METRIC[self.task],
            "baselines": list(self.baselines),
            "seed": self.seed,
            **self._params,
        },
        "encodings": list(self.labels),
        "datasets": list(data.keys()),
        "results": results,
        "baselines": baselines,
    }
    return self.results

statistical_tests

statistical_tests(*, alpha: float = 0.05) -> dict[str, Any]

Pairwise encoding comparison per (method, dataset).

Runs :func:compare_encodings_corrected (Wilcoxon + Holm-Bonferroni + Cliff's delta) on the paired per-fold scores. Requires :meth:run to have been called first.

Returns

dict {f"{method}/{dataset}": {<corrected comparison>}}.

Source code in src/encoding_atlas/benchmark/runner.py
def statistical_tests(self, *, alpha: float = 0.05) -> dict[str, Any]:
    """Pairwise encoding comparison per (method, dataset).

    Runs :func:`compare_encodings_corrected` (Wilcoxon + Holm-Bonferroni +
    Cliff's delta) on the paired per-fold scores. Requires :meth:`run` to
    have been called first.

    Returns
    -------
    dict
        ``{f"{method}/{dataset}": {<corrected comparison>}}``.
    """
    if not self._raw:
        raise RuntimeError("Call run() before statistical_tests().")

    out: dict[str, Any] = {}
    for (method, dname), per_label in self._raw.items():
        usable = {
            label: scores for label, scores in per_label.items() if len(scores) >= 2
        }
        lengths = {len(s) for s in usable.values()}
        if len(usable) < 2 or len(lengths) != 1:
            # Need >= 2 encodings with equal-length paired scores.
            continue
        out[f"{method}/{dname}"] = compare_encodings_corrected(usable, alpha=alpha)
    return out

plot_comparison

plot_comparison(*, method: str | None = None) -> Any

Return a matplotlib bar chart of mean accuracy per encoding/dataset.

Parameters

method : str or None Which method to plot. Defaults to the first configured method.

Raises

RuntimeError If :meth:run has not been called. ImportError If matplotlib is not installed.

Source code in src/encoding_atlas/benchmark/runner.py
def plot_comparison(self, *, method: str | None = None) -> Any:
    """Return a matplotlib bar chart of mean accuracy per encoding/dataset.

    Parameters
    ----------
    method : str or None
        Which method to plot. Defaults to the first configured method.

    Raises
    ------
    RuntimeError
        If :meth:`run` has not been called.
    ImportError
        If matplotlib is not installed.
    """
    if not self.results:
        raise RuntimeError("Call run() before plot_comparison().")
    try:
        import matplotlib.pyplot as plt
    except ImportError as exc:
        raise ImportError("matplotlib required for plotting") from exc

    method = method or self.methods[0]
    datasets = self.results["datasets"]
    labels = self.results["encodings"]
    method_results = self.results["results"][method]

    x = np.arange(len(datasets))
    width = 0.8 / max(len(labels), 1)
    fig, ax = plt.subplots(figsize=(max(8, 2 * len(datasets)), 6))
    for i, label in enumerate(labels):
        means = [
            method_results[label].get(d, {}).get("mean", float("nan"))
            for d in datasets
        ]
        ax.bar(x + i * width, means, width, label=label)
    ax.set_xticks(x + width * (len(labels) - 1) / 2)
    ax.set_xticklabels(datasets, rotation=45, ha="right")
    if self.task == "regression":
        # R^2 is unbounded below, so let matplotlib pick the limits.
        ax.set_ylabel("Mean test $R^2$")
        ax.axhline(0.0, color="grey", linewidth=0.8)
    else:
        ax.set_ylabel("Mean test accuracy")
        ax.set_ylim(0, 1)
    ax.set_title(f"Encoding comparison ({method}, {self.task})")
    ax.legend(fontsize="small")
    fig.tight_layout()
    return fig

save_results

save_results(path: str) -> None

Write the benchmark results to path as JSON.

Raises

RuntimeError If :meth:run has not been called.

Source code in src/encoding_atlas/benchmark/runner.py
def save_results(self, path: str) -> None:
    """Write the benchmark results to ``path`` as JSON.

    Raises
    ------
    RuntimeError
        If :meth:`run` has not been called.
    """
    if not self.results:
        raise RuntimeError("Call run() before save_results().")
    with open(path, "w", encoding="utf-8") as handle:
        json.dump(self.results, handle, indent=2)

evaluate_encoding

evaluate_encoding(encoding: Any, X: NDArray[floating[Any]], y: NDArray[intp], *, method: str = 'kernel', task: str = 'classification', n_runs: int = 1, n_folds: int = 5, seed: int = 42, scale: bool = True, scale_range: tuple[float, float] = _DEFAULT_SCALE, vqc_layers: int = 2, vqc_epochs: int = 30, vqc_lr: float = 0.05, kernel_C: float = 1.0, kernel_alpha: float = 1.0) -> dict[str, Any]

Evaluate one encoding on a single dataset via cross-validation.

This is the entry point for benchmarking on custom data: pass any feature matrix X with either class labels or continuous targets y.

Parameters

encoding : BaseEncoding Encoding whose n_features must equal X.shape[1]. X, y : ndarray Feature matrix and targets — class labels for task="classification" or continuous values for task="regression". method : {"vqc", "kernel"}, default="kernel" Quantum model family. task : {"classification", "regression"}, default="classification" Learning task. Classification reports accuracy and uses stratified folds; regression reports R^2 and uses plain K-fold. n_runs : int, default=1 Independent repetitions, each with a different CV split seed. n_folds : int, default=5 CV folds per run. seed : int, default=42 Base random seed. scale : bool, default=True Min-max scale features into scale_range before encoding. scale_range : tuple, default=(0, 2*pi) Target range for feature scaling. vqc_layers, vqc_epochs, vqc_lr : int, int, float VQC ansatz depth, training epochs, and learning rate. kernel_C : float, default=1.0 SVM regularisation (classification kernel method). kernel_alpha : float, default=1.0 Ridge regularisation (regression kernel method).

Returns

dict {"method", "task", "score_metric", "scores", "n_failed", ...summary} where the summary keys are mean/std/ci_low/ci_high/n_scores. The score is accuracy for classification and R^2 for regression.

Raises

ValueError If method/task is invalid or the encoding's feature count does not match X.

Source code in src/encoding_atlas/benchmark/runner.py
def evaluate_encoding(
    encoding: Any,
    X: NDArray[np.floating[Any]],
    y: NDArray[np.intp],
    *,
    method: str = "kernel",
    task: str = "classification",
    n_runs: int = 1,
    n_folds: int = 5,
    seed: int = 42,
    scale: bool = True,
    scale_range: tuple[float, float] = _DEFAULT_SCALE,
    vqc_layers: int = 2,
    vqc_epochs: int = 30,
    vqc_lr: float = 0.05,
    kernel_C: float = 1.0,
    kernel_alpha: float = 1.0,
) -> dict[str, Any]:
    """Evaluate one encoding on a single dataset via cross-validation.

    This is the entry point for benchmarking on *custom* data: pass any feature
    matrix ``X`` with either class labels or continuous targets ``y``.

    Parameters
    ----------
    encoding : BaseEncoding
        Encoding whose ``n_features`` must equal ``X.shape[1]``.
    X, y : ndarray
        Feature matrix and targets — class labels for ``task="classification"``
        or continuous values for ``task="regression"``.
    method : {"vqc", "kernel"}, default="kernel"
        Quantum model family.
    task : {"classification", "regression"}, default="classification"
        Learning task. Classification reports accuracy and uses stratified
        folds; regression reports R^2 and uses plain K-fold.
    n_runs : int, default=1
        Independent repetitions, each with a different CV split seed.
    n_folds : int, default=5
        CV folds per run.
    seed : int, default=42
        Base random seed.
    scale : bool, default=True
        Min-max scale features into ``scale_range`` before encoding.
    scale_range : tuple, default=(0, 2*pi)
        Target range for feature scaling.
    vqc_layers, vqc_epochs, vqc_lr : int, int, float
        VQC ansatz depth, training epochs, and learning rate.
    kernel_C : float, default=1.0
        SVM regularisation (classification kernel method).
    kernel_alpha : float, default=1.0
        Ridge regularisation (regression kernel method).

    Returns
    -------
    dict
        ``{"method", "task", "score_metric", "scores", "n_failed", ...summary}``
        where the summary keys are
        ``mean``/``std``/``ci_low``/``ci_high``/``n_scores``. The score is
        accuracy for classification and R^2 for regression.

    Raises
    ------
    ValueError
        If ``method``/``task`` is invalid or the encoding's feature count does
        not match ``X``.
    """
    if method not in _VALID_METHODS:
        raise ValueError(f"method must be one of {_VALID_METHODS}, got {method!r}")
    if task not in _VALID_TASKS:
        raise ValueError(f"task must be one of {_VALID_TASKS}, got {task!r}")

    X = np.asarray(X, dtype=np.float64)
    # Continuous targets must not be truncated to integers.
    y = np.asarray(y, dtype=np.float64 if task == "regression" else np.intp)
    if X.ndim != 2:
        raise ValueError(f"X must be 2-D, got shape {X.shape}")
    if getattr(encoding, "n_features", X.shape[1]) != X.shape[1]:
        raise ValueError(
            f"Encoding expects {encoding.n_features} features but X has "
            f"{X.shape[1]}; construct the encoding with matching n_features."
        )

    params = {
        "vqc_layers": vqc_layers,
        "vqc_epochs": vqc_epochs,
        "vqc_lr": vqc_lr,
        "kernel_C": kernel_C,
        "kernel_alpha": kernel_alpha,
    }
    score_key = _SCORE_KEY[task]

    scores: list[float] = []
    n_failed = 0
    for run in range(n_runs):
        folds = _make_folds(X, y, n_folds, seed=seed + run, task=task)
        if scale:
            folds = [_scale_fold(f, scale_range) for f in folds]
        for fold_idx, fold in enumerate(folds):
            result = _run_method_fold(
                method,
                encoding,
                fold,
                seed=(seed + run) * 100 + fold_idx,
                params=params,
                task=task,
            )
            if result["status"] == "success":
                scores.append(result[score_key])
            else:
                n_failed += 1

    summary = _summarize(scores, bounded=task == "classification")
    return {
        "method": method,
        "task": task,
        "score_metric": _SCORE_METRIC[task],
        "scores": scores,
        "n_failed": n_failed,
        **summary,
    }

VQCClassifier

VQCClassifier(encoding: Any = None, n_var_layers: int = 2, lr: float = 0.05, epochs: int = 30, seed: int | None = None)

Bases: ClassifierMixin, BaseEstimator

Variational quantum classifier with a configurable encoding.

Architecture::

|0> -- Encoding(x) -- [RY(theta) + CNOT chain] x L -- <Z_0> -- class

Parameters

encoding : BaseEncoding The quantum encoding to benchmark. Must expose n_qubits and a get_circuit(x, backend="pennylane") method. n_var_layers : int, default=2 Number of variational layers after the encoding. lr : float, default=0.05 Adam learning rate. epochs : int, default=30 Number of training epochs. seed : int or None, default=None Random seed for parameter initialisation and shuffling.

Follows scikit-learn's estimator contract, so it composes with cross_val_score, GridSearchCV, Pipeline and other meta-estimators. Hyper-parameters are validated at fit time rather than in the constructor; see :mod:encoding_atlas.benchmark._estimator.

Attributes

params_ : ndarray Trained variational parameters of shape (n_var_layers, n_qubits). Set by fit; absent before it. loss_history_ : list[float] Mean training loss per epoch. status_ : str "success" or "diverged". Set by fit. classes_ : ndarray Class labels seen during fit. n_features_in_ : int Feature count seen during fit.

Examples

from sklearn.model_selection import GridSearchCV from encoding_atlas import AngleEncoding from encoding_atlas.benchmark import get_dataset X, y = get_dataset("moons", n_samples=24, seed=0) search = GridSearchCV( ... VQCClassifier(AngleEncoding(n_features=2), epochs=2), ... {"n_var_layers": [1, 2]}, ... cv=2, ... ).fit(X, y) search.best_params_["n_var_layers"] in (1, 2) True

Source code in src/encoding_atlas/benchmark/vqc.py
def __init__(
    self,
    encoding: Any = None,
    n_var_layers: int = 2,
    lr: float = 0.05,
    epochs: int = 30,
    seed: int | None = None,
) -> None:
    # Store verbatim only. Reading ``encoding.n_qubits`` here would go
    # stale under ``set_params(encoding=...)``; it is read in ``fit``.
    self.encoding = encoding
    self.n_var_layers = n_var_layers
    self.lr = lr
    self.epochs = epochs
    self.seed = seed

fit

fit(X: NDArray[floating[Any]], y: NDArray[intp]) -> VQCClassifier

Train on X and labels y.

Two-class problems (labels in {0, 1}) train a single VQC that reads out <Z_0>. Problems with more than two classes train a one-vs-rest ensemble of binary VQCs.

Source code in src/encoding_atlas/benchmark/vqc.py
def fit(self, X: NDArray[np.floating[Any]], y: NDArray[np.intp]) -> VQCClassifier:
    """Train on ``X`` and labels ``y``.

    Two-class problems (labels in ``{0, 1}``) train a single VQC that reads
    out ``<Z_0>``. Problems with more than two classes train a one-vs-rest
    ensemble of binary VQCs.
    """
    self._validate_hyperparameters()
    X = check_feature_matrix(self, X, reset=True)
    y = check_targets(y, X.shape[0], dtype=np.intp)
    self.n_qubits_ = int(self.encoding.n_qubits)
    self.classes_ = np.unique(y)
    if len(self.classes_) > 2:
        return self._fit_multiclass(X, y)

    # --- Binary path (labels in {0, 1}) -------------------------------
    self._ovr_models = None
    self._qnode = None
    import pennylane as qml
    from pennylane import numpy as pnp

    if self._qnode is None:
        self._build_circuit()
    assert self._qnode is not None  # set by _build_circuit
    qnode = self._qnode

    rng = np.random.default_rng(self.seed)
    self.params_ = pnp.array(
        rng.uniform(-np.pi, np.pi, size=(self.n_var_layers, self.n_qubits_)),
        requires_grad=True,
    )

    opt = qml.AdamOptimizer(stepsize=self.lr)
    self.loss_history_ = []
    self.status_ = "success"
    n_samples = len(X)

    for epoch in range(self.epochs):
        epoch_loss = 0.0
        indices = rng.permutation(n_samples)
        X_epoch, y_epoch = X[indices], y[indices]

        for xi, yi in zip(X_epoch, y_epoch):

            def cost(params: NDArray[np.floating[Any]]) -> Any:
                pred = qnode(xi, params)
                p = (pred + 1) / 2
                p = pnp.clip(p, 1e-7, 1 - 1e-7)
                return -yi * pnp.log(p) - (1 - yi) * pnp.log(1 - p)

            self.params_, loss = opt.step_and_cost(cost, self.params_)
            epoch_loss += float(loss)

        avg_loss = epoch_loss / n_samples
        self.loss_history_.append(avg_loss)

        if np.isnan(avg_loss) or avg_loss > _MAX_LOSS:
            logger.warning("VQC training diverged at epoch %d", epoch)
            self.status_ = "diverged"
            break

    return self

predict

predict(X: NDArray[floating[Any]]) -> NDArray[intp]

Return predicted labels (in {0, 1} for binary tasks).

Source code in src/encoding_atlas/benchmark/vqc.py
def predict(self, X: NDArray[np.floating[Any]]) -> NDArray[np.intp]:
    """Return predicted labels (in ``{0, 1}`` for binary tasks)."""
    check_is_fitted(self)
    if self._ovr_models is not None:
        indices = np.argmax(self.predict_proba(X), axis=1)
        labels: NDArray[np.intp] = self.classes_[indices].astype(np.intp)
        return labels
    proba = self.predict_proba(X)
    return (proba[:, 1] >= 0.5).astype(np.intp)

QuantumKernelClassifier

QuantumKernelClassifier(encoding: Any = None, *, C: float = 1.0, seed: int | None = None)

Bases: ClassifierMixin, BaseEstimator

Fidelity-kernel SVM classifier for a fixed encoding.

Computes the quantum kernel for the training data, enforces PSD, and fits a precomputed-kernel :class:sklearn.svm.SVC. Prediction uses the test/train cross kernel.

Follows scikit-learn's estimator contract, so it composes with cross_val_score, GridSearchCV, Pipeline and other meta-estimators. As there, hyper-parameters are validated at fit time rather than in the constructor; see :mod:encoding_atlas.benchmark._estimator.

Parameters

encoding : BaseEncoding Encoding used for state preparation. Fixes the accepted feature count. C : float, default=1.0 SVM regularisation strength. Must be positive. seed : int or None, default=None Seed forwarded to the SVM for reproducibility.

Attributes

classes_ : ndarray Class labels seen during fit. n_features_in_ : int Feature count seen during fit. kernel_regularized_ : bool Whether the training kernel needed PSD projection.

Examples

from sklearn.model_selection import cross_val_score from encoding_atlas import AngleEncoding from encoding_atlas.benchmark import get_dataset X, y = get_dataset("moons", n_samples=30, seed=0) clf = QuantumKernelClassifier(AngleEncoding(n_features=2)) scores = cross_val_score(clf, X, y, cv=3) len(scores) 3

Source code in src/encoding_atlas/benchmark/kernel.py
def __init__(
    self, encoding: Any = None, *, C: float = 1.0, seed: int | None = None
) -> None:
    # Store verbatim only: no validation, no derived state. See
    # encoding_atlas.benchmark._estimator for why.
    self.encoding = encoding
    self.C = C
    self.seed = seed

fit

fit(X: NDArray[floating[Any]], y: NDArray[intp]) -> QuantumKernelClassifier

Fit the precomputed-kernel SVM on (X, y).

Raises

ValueError If C is not positive, encoding is unset, or X/y are malformed or mismatched.

Source code in src/encoding_atlas/benchmark/kernel.py
def fit(
    self, X: NDArray[np.floating[Any]], y: NDArray[np.intp]
) -> QuantumKernelClassifier:
    """Fit the precomputed-kernel SVM on ``(X, y)``.

    Raises
    ------
    ValueError
        If ``C`` is not positive, ``encoding`` is unset, or ``X``/``y`` are
        malformed or mismatched.
    """
    from sklearn.svm import SVC

    require_positive("C", self.C)
    if self.encoding is None:
        raise ValueError("encoding must be set before fitting")
    X = check_feature_matrix(self, X, reset=True)
    y = check_targets(y, X.shape[0], dtype=np.intp)
    self.classes_ = np.unique(y)

    K_train, states = compute_kernel_matrix(self.encoding, X, return_states=True)
    K_train_psd, self.kernel_regularized_ = ensure_psd(K_train)
    self.svm_ = SVC(kernel="precomputed", C=self.C, random_state=self.seed)
    self.svm_.fit(K_train_psd, y)
    self.X_train_ = X
    self.train_states_ = states
    return self

predict

predict(X: NDArray[floating[Any]]) -> NDArray[intp]

Predict labels for X using the test/train cross kernel.

Source code in src/encoding_atlas/benchmark/kernel.py
def predict(self, X: NDArray[np.floating[Any]]) -> NDArray[np.intp]:
    """Predict labels for ``X`` using the test/train cross kernel."""
    # Build the kernel first: attribute access on ``svm_`` would otherwise
    # be evaluated before the fitted check inside ``_cross_kernel``.
    K_test = self._cross_kernel(X)
    return cast("NDArray[np.intp]", self.svm_.predict(K_test).astype(np.intp))

decision_function

decision_function(X: NDArray[floating[Any]]) -> NDArray[floating[Any]]

Signed distance to the separating hyperplane in feature space.

Exposed so the classifier works with threshold-based metrics such as ROC-AUC, and with probability calibration.

Source code in src/encoding_atlas/benchmark/kernel.py
def decision_function(
    self, X: NDArray[np.floating[Any]]
) -> NDArray[np.floating[Any]]:
    """Signed distance to the separating hyperplane in feature space.

    Exposed so the classifier works with threshold-based metrics such as
    ROC-AUC, and with probability calibration.
    """
    K_test = self._cross_kernel(X)
    return np.asarray(self.svm_.decision_function(K_test), dtype=np.float64)

VQCRegressor

VQCRegressor(encoding: Any = None, n_var_layers: int = 2, lr: float = 0.05, epochs: int = 30, seed: int | None = None)

Bases: RegressorMixin, BaseEstimator

Variational quantum regressor with a configurable encoding.

Uses the same circuit as :class:VQCClassifier but reads <Z_0> as a continuous prediction and trains with mean-squared error. Targets are min-max scaled into [-1, 1] (the range of <Z_0>) using training statistics only, and predictions are mapped back to the original scale.

Parameters

encoding : BaseEncoding The quantum encoding to benchmark. n_var_layers : int, default=2 Number of variational layers after the encoding. lr : float, default=0.05 Adam learning rate. epochs : int, default=30 Number of training epochs. seed : int or None, default=None Random seed for parameter initialisation and shuffling.

Attributes

params_ : ndarray Trained variational parameters. Set by fit; absent before it. loss_history_ : list[float] Mean training loss per epoch. status_ : str "success" or "diverged". Set by fit. n_features_in_ : int Feature count seen during fit.

Source code in src/encoding_atlas/benchmark/vqc.py
def __init__(
    self,
    encoding: Any = None,
    n_var_layers: int = 2,
    lr: float = 0.05,
    epochs: int = 30,
    seed: int | None = None,
) -> None:
    self.encoding = encoding
    self.n_var_layers = n_var_layers
    self.lr = lr
    self.epochs = epochs
    self.seed = seed

fit

fit(X: NDArray[floating[Any]], y: NDArray[floating[Any]]) -> VQCRegressor

Train on X and continuous targets y using MSE loss.

Source code in src/encoding_atlas/benchmark/vqc.py
def fit(
    self, X: NDArray[np.floating[Any]], y: NDArray[np.floating[Any]]
) -> VQCRegressor:
    """Train on ``X`` and continuous targets ``y`` using MSE loss."""
    import pennylane as qml
    from pennylane import numpy as pnp

    self._validate_hyperparameters()
    X = check_feature_matrix(self, X, reset=True)
    y = check_targets(y, X.shape[0], dtype=np.float64)
    self.n_qubits_ = int(self.encoding.n_qubits)

    self._qnode = None
    self._build_circuit()
    assert self._qnode is not None  # set by _build_circuit
    qnode = self._qnode

    self._y_min = float(y.min())
    span = float(y.max()) - self._y_min
    self._y_span = span if span > 0.0 else 1.0
    # Map targets onto [-1, 1], the range of <Z_0>.
    y_scaled = 2.0 * (y - self._y_min) / self._y_span - 1.0

    rng = np.random.default_rng(self.seed)
    self.params_ = pnp.array(
        rng.uniform(-np.pi, np.pi, size=(self.n_var_layers, self.n_qubits_)),
        requires_grad=True,
    )

    opt = qml.AdamOptimizer(stepsize=self.lr)
    self.loss_history_ = []
    self.status_ = "success"
    n_samples = len(X)

    for epoch in range(self.epochs):
        epoch_loss = 0.0
        indices = rng.permutation(n_samples)
        X_epoch, y_epoch = X[indices], y_scaled[indices]

        for xi, ti in zip(X_epoch, y_epoch):

            def cost(params: NDArray[np.floating[Any]]) -> Any:
                return (qnode(xi, params) - ti) ** 2

            self.params_, loss = opt.step_and_cost(cost, self.params_)
            epoch_loss += float(loss)

        avg_loss = epoch_loss / n_samples
        self.loss_history_.append(avg_loss)

        if np.isnan(avg_loss) or avg_loss > _MAX_LOSS:
            logger.warning("VQC regression diverged at epoch %d", epoch)
            self.status_ = "diverged"
            break

    return self

predict

predict(X: NDArray[floating[Any]]) -> NDArray[floating[Any]]

Predict continuous targets on the original target scale.

Source code in src/encoding_atlas/benchmark/vqc.py
def predict(self, X: NDArray[np.floating[Any]]) -> NDArray[np.floating[Any]]:
    """Predict continuous targets on the original target scale."""
    check_is_fitted(self)
    X = check_feature_matrix(self, X, reset=False)
    if self._qnode is None:
        self._build_circuit()
    assert self._qnode is not None  # set by _build_circuit

    raw = np.array([float(self._qnode(xi, self.params_)) for xi in X])
    return (raw + 1.0) / 2.0 * self._y_span + self._y_min

QuantumKernelRegressor

QuantumKernelRegressor(encoding: Any = None, *, alpha: float = 1.0)

Bases: RegressorMixin, BaseEstimator

Fidelity-kernel ridge regressor for a fixed encoding.

Computes the quantum kernel for the training data, enforces PSD, and fits a precomputed-kernel :class:sklearn.kernel_ridge.KernelRidge. Prediction uses the test/train cross kernel.

Follows scikit-learn's estimator contract; score returns R^2 through :class:sklearn.base.RegressorMixin.

Parameters

encoding : BaseEncoding Encoding used for state preparation. alpha : float, default=1.0 Ridge regularisation strength. Must be positive.

Attributes

n_features_in_ : int Feature count seen during fit. kernel_regularized_ : bool Whether the training kernel needed PSD projection.

Source code in src/encoding_atlas/benchmark/kernel.py
def __init__(self, encoding: Any = None, *, alpha: float = 1.0) -> None:
    self.encoding = encoding
    self.alpha = alpha

fit

fit(X: NDArray[floating[Any]], y: NDArray[floating[Any]]) -> QuantumKernelRegressor

Fit the precomputed-kernel ridge regressor on (X, y).

Raises

ValueError If alpha is not positive, encoding is unset, or X/y are malformed or mismatched.

Source code in src/encoding_atlas/benchmark/kernel.py
def fit(
    self, X: NDArray[np.floating[Any]], y: NDArray[np.floating[Any]]
) -> QuantumKernelRegressor:
    """Fit the precomputed-kernel ridge regressor on ``(X, y)``.

    Raises
    ------
    ValueError
        If ``alpha`` is not positive, ``encoding`` is unset, or ``X``/``y``
        are malformed or mismatched.
    """
    from sklearn.kernel_ridge import KernelRidge

    require_positive("alpha", self.alpha)
    if self.encoding is None:
        raise ValueError("encoding must be set before fitting")
    X = check_feature_matrix(self, X, reset=True)
    y = check_targets(y, X.shape[0], dtype=np.float64)

    K_train, states = compute_kernel_matrix(self.encoding, X, return_states=True)
    K_train_psd, self.kernel_regularized_ = ensure_psd(K_train)
    self.model_ = KernelRidge(kernel="precomputed", alpha=self.alpha)
    self.model_.fit(K_train_psd, y)
    self.X_train_ = X
    self.train_states_ = states
    return self

predict

predict(X: NDArray[floating[Any]]) -> NDArray[floating[Any]]

Predict continuous targets using the test/train cross kernel.

Source code in src/encoding_atlas/benchmark/kernel.py
def predict(self, X: NDArray[np.floating[Any]]) -> NDArray[np.floating[Any]]:
    """Predict continuous targets using the test/train cross kernel."""
    check_is_fitted(self)
    X = check_feature_matrix(self, X, reset=False)
    K_test = compute_kernel_matrix_cross(
        self.encoding, self.X_train_, X, train_states=self.train_states_
    )
    return np.asarray(self.model_.predict(K_test), dtype=np.float64)

compute_kernel_matrix

compute_kernel_matrix(encoding: BaseEncoding, X: NDArray[floating[Any]], *, backend: str = 'pennylane', return_states: bool = False) -> Any

Compute the symmetric n x n fidelity kernel matrix for X.

Statevectors are simulated once per sample and reused for all pairwise fidelities. When return_states=True a (K, states) tuple is returned so the states can be reused for the cross kernel.

Source code in src/encoding_atlas/benchmark/kernel.py
def compute_kernel_matrix(
    encoding: BaseEncoding,
    X: NDArray[np.floating[Any]],
    *,
    backend: str = "pennylane",
    return_states: bool = False,
) -> Any:
    """Compute the symmetric ``n x n`` fidelity kernel matrix for ``X``.

    Statevectors are simulated once per sample and reused for all pairwise
    fidelities. When ``return_states=True`` a ``(K, states)`` tuple is returned
    so the states can be reused for the cross kernel.
    """
    n = len(X)
    K: NDArray[np.float64] = np.zeros((n, n), dtype=np.float64)
    states = [simulate_encoding_state(encoding, xi, backend) for xi in X]

    for i in range(n):
        K[i, i] = 1.0
        for j in range(i + 1, n):
            K[i, j] = compute_kernel_entry(states[i], states[j])
            K[j, i] = K[i, j]

    if return_states:
        return K, states
    return K

kernel_target_alignment module-attribute

kernel_target_alignment = _kernel_target_alignment

compare_encodings_corrected

compare_encodings_corrected(results: dict[str, list[float]], *, alpha: float = 0.05) -> dict[str, Any]

Full pairwise comparison: Wilcoxon + Holm-Bonferroni + Cliff's delta.

Parameters

results : dict Mapping of encoding name -> list of paired per-fold/run scores. All lists must have equal length (paired across the same CV splits). alpha : float, default=0.05 Family-wise significance level used to flag significant pairs.

Returns

dict {"n_comparisons": int, "alpha": float, "pairs": [...]} where each pair record holds the two encodings, raw and corrected p-values, Wilcoxon statistic, Cliff's delta with magnitude, the mean-score difference, and a significant boolean (corrected p < alpha).

Source code in src/encoding_atlas/benchmark/statistical.py
def compare_encodings_corrected(
    results: dict[str, list[float]],
    *,
    alpha: float = 0.05,
) -> dict[str, Any]:
    """Full pairwise comparison: Wilcoxon + Holm-Bonferroni + Cliff's delta.

    Parameters
    ----------
    results : dict
        Mapping of encoding name -> list of paired per-fold/run scores. All
        lists must have equal length (paired across the same CV splits).
    alpha : float, default=0.05
        Family-wise significance level used to flag ``significant`` pairs.

    Returns
    -------
    dict
        ``{"n_comparisons": int, "alpha": float, "pairs": [...]}`` where each
        pair record holds the two encodings, raw and corrected p-values,
        Wilcoxon statistic, Cliff's delta with magnitude, the mean-score
        difference, and a ``significant`` boolean (corrected p < alpha).
    """
    names = list(results.keys())
    raw: dict[tuple[str, str], dict[str, Any]] = {}

    for i, name_a in enumerate(names):
        for name_b in names[i + 1 :]:
            a, b = results[name_a], results[name_b]
            stat, p_value = wilcoxon_test(a, b)
            delta, magnitude = cliffs_delta(a, b)
            mean_diff = (sum(a) / len(a) - sum(b) / len(b)) if a and b else 0.0
            raw[(name_a, name_b)] = {
                "encoding_a": name_a,
                "encoding_b": name_b,
                "statistic": stat,
                "p_value": p_value,
                "cliffs_delta": delta,
                "effect_magnitude": magnitude,
                "mean_difference": mean_diff,
            }

    corrected = holm_bonferroni({key: rec["p_value"] for key, rec in raw.items()})

    pairs = []
    for key, rec in raw.items():
        rec["p_value_corrected"] = corrected[key]
        rec["significant"] = corrected[key] < alpha
        pairs.append(rec)
    pairs.sort(key=lambda r: r["p_value_corrected"])

    return {"n_comparisons": len(pairs), "alpha": alpha, "pairs": pairs}

Atlas Module

The empirical benchmark results — measured circuit resources, simulability, expressibility, entanglement, trainability, noise resilience, and downstream VQC / quantum-kernel accuracy for all 16 encodings — bundled with the package as a queryable, read-only API.

get_encoding_profile

get_encoding_profile(name: str) -> EncodingProfile

Return the measured profile for a single encoding.

Parameters

name : str Encoding identifier. Canonical names ("qaoa"), dataset aliases ("qaoa_encoding"), and class display names ("QAOAEncoding") are all accepted, case-insensitively.

Returns

EncodingProfile The encoding's empirical profile.

Raises

KeyError If name does not correspond to any benchmarked encoding.

Source code in src/encoding_atlas/atlas/profiles.py
def get_encoding_profile(name: str) -> EncodingProfile:
    """Return the measured profile for a single encoding.

    Parameters
    ----------
    name : str
        Encoding identifier. Canonical names (``"qaoa"``), dataset aliases
        (``"qaoa_encoding"``), and class display names (``"QAOAEncoding"``) are
        all accepted, case-insensitively.

    Returns
    -------
    EncodingProfile
        The encoding's empirical profile.

    Raises
    ------
    KeyError
        If ``name`` does not correspond to any benchmarked encoding.
    """
    index = _profile_index()
    key = name.strip().lower()
    if key not in index:
        available = ", ".join(available_encodings())
        raise KeyError(f"Unknown encoding {name!r}. Available encodings: {available}")
    return index[key]

rank_encodings

rank_encodings(by: str = _SCORE_KEY, *, ascending: bool | None = None, limit: int | None = None) -> list[EncodingProfile]

Rank encodings by a measured metric or by the composite score.

Parameters

by : str Ranking key: "score", "rank", or any scalar metric from :func:list_metrics (e.g. "kernel_accuracy", "depth"). ascending : bool or None Sort direction. If None (default), a sensible direction is chosen per key: ascending for "rank" and "depth" (lower is better), descending otherwise (higher is better). limit : int or None If given, return at most this many profiles.

Returns

list[EncodingProfile] Profiles sorted as requested. Encodings whose value for by is undefined (None) are omitted from metric rankings.

Raises

ValueError If by is not a recognised ranking key, or limit is negative.

Source code in src/encoding_atlas/atlas/profiles.py
def rank_encodings(
    by: str = _SCORE_KEY,
    *,
    ascending: bool | None = None,
    limit: int | None = None,
) -> list[EncodingProfile]:
    """Rank encodings by a measured metric or by the composite score.

    Parameters
    ----------
    by : str
        Ranking key: ``"score"``, ``"rank"``, or any scalar metric from
        :func:`list_metrics` (e.g. ``"kernel_accuracy"``, ``"depth"``).
    ascending : bool or None
        Sort direction. If ``None`` (default), a sensible direction is chosen
        per key: ascending for ``"rank"`` and ``"depth"`` (lower is better),
        descending otherwise (higher is better).
    limit : int or None
        If given, return at most this many profiles.

    Returns
    -------
    list[EncodingProfile]
        Profiles sorted as requested. Encodings whose value for ``by`` is
        undefined (``None``) are omitted from metric rankings.

    Raises
    ------
    ValueError
        If ``by`` is not a recognised ranking key, or ``limit`` is negative.
    """
    valid_keys = {_SCORE_KEY, _RANK_KEY, *_SCALAR_METRICS}
    if by not in valid_keys:
        raise ValueError(f"Cannot rank by {by!r}. Valid keys: {sorted(valid_keys)}")
    if limit is not None and limit < 0:
        raise ValueError(f"limit must be non-negative, got {limit}")

    profiles = list(_profiles_by_rank())
    if by in _SCALAR_METRICS:
        profiles = [p for p in profiles if p.metrics.get(by) is not None]

    if ascending is None:
        ascending = by in _LOWER_IS_BETTER

    ranked = sorted(profiles, key=_sort_key(by), reverse=not ascending)
    if limit is not None:
        ranked = ranked[:limit]
    return ranked

pareto_front

pareto_front() -> list[EncodingProfile]

Return the Pareto-optimal encoding profiles, ordered by rank.

These are the encodings not dominated on the benchmark objectives (accuracy, inverse depth, trainability, noise resilience).

Source code in src/encoding_atlas/atlas/profiles.py
def pareto_front() -> list[EncodingProfile]:
    """Return the Pareto-optimal encoding profiles, ordered by rank.

    These are the encodings not dominated on the benchmark objectives
    (accuracy, inverse depth, trainability, noise resilience).
    """
    return [p for p in _profiles_by_rank() if p.is_pareto]

hypothesis_verdicts

hypothesis_verdicts() -> dict[str, Any]

Return the pre-registered hypothesis outcomes (H1H7).

Each verdict carries its verdict ("supported" / "refuted" / "inconclusive"), confidence, a plain-language evidence summary, and the supporting test_statistic. A deep copy is returned so callers cannot mutate the cached dataset.

Source code in src/encoding_atlas/atlas/profiles.py
def hypothesis_verdicts() -> dict[str, Any]:
    """Return the pre-registered hypothesis outcomes (``H1``–``H7``).

    Each verdict carries its ``verdict`` (``"supported"`` / ``"refuted"`` /
    ``"inconclusive"``), ``confidence``, a plain-language ``evidence`` summary,
    and the supporting ``test_statistic``. A deep copy is returned so callers
    cannot mutate the cached dataset.
    """
    return copy.deepcopy(load_raw()["hypothesis_verdicts"])

atlas_metadata

atlas_metadata() -> dict[str, Any]

Return provenance and summary metadata for the bundled atlas.

Returns

dict Schema version, encoding count, benchmark objective names, Pareto-front size, per-stage success counts, and a human-readable source string.

Source code in src/encoding_atlas/atlas/profiles.py
def atlas_metadata() -> dict[str, Any]:
    """Return provenance and summary metadata for the bundled atlas.

    Returns
    -------
    dict
        Schema version, encoding count, benchmark objective names, Pareto-front
        size, per-stage success counts, and a human-readable ``source`` string.
    """
    raw = load_raw()
    pareto = raw["pareto_front"]
    return {
        "schema_version": raw["schema_version"],
        "n_encodings": raw["n_encodings"],
        "objective_names": list(pareto["objective_names"]),
        "n_pareto_optimal": pareto["n_pareto_optimal"],
        "stage_counts": copy.deepcopy(raw["stage_counts"]),
        "source": ATLAS_SOURCE,
    }

EncodingProfile dataclass

EncodingProfile(name: str, display_name: str, family: str, rank: int, score: float, is_pareto: bool, is_simulable: bool, metrics: Mapping[str, Any])

Measured empirical profile of a single encoding.

Attributes

name : str Canonical (registry-primary) encoding name, e.g. "qaoa". display_name : str Human-friendly class name, e.g. "QAOAEncoding". family : str Taxonomic family the encoding belongs to. rank : int Overall benchmark rank (1 = best) by the composite trade-off score. score : float Composite trade-off score in [0, 1] (higher is better). is_pareto : bool Whether the encoding lies on the Pareto front across the benchmark objectives (accuracy, inverse depth, trainability, noise resilience). is_simulable : bool Whether the encoding is classically efficiently simulable. metrics : Mapping[str, Any] Read-only mapping of measured metrics. Scalar keys are listed by :func:list_metrics; vqc_ci and kernel_ci hold 95% confidence intervals as [low, high] pairs. Some entries may be None when a metric is not defined for that encoding (e.g. expressibility for basis/amplitude state-preparation circuits).

``kernel_target_alignment`` is the mean *centered* alignment over the
benchmark's (configuration, dataset) pairs — the training-free quantity
that predicts kernel accuracy. To measure it on your own data instead,
see :func:`encoding_atlas.guide.screen_encodings`.

metric

metric(key: str, default: Any = None) -> Any

Return a measured metric value, or default if absent/undefined.

Parameters

key : str Metric name (see :func:list_metrics). default : Any Value to return if the metric is missing or recorded as None.

Source code in src/encoding_atlas/atlas/profiles.py
def metric(self, key: str, default: Any = None) -> Any:
    """Return a measured metric value, or ``default`` if absent/undefined.

    Parameters
    ----------
    key : str
        Metric name (see :func:`list_metrics`).
    default : Any
        Value to return if the metric is missing or recorded as ``None``.
    """
    value = self.metrics.get(key, default)
    return default if value is None else value

Kernel-Concentration Scan

The companion dataset recording how each encoding's fidelity kernel approaches the Haar floor across 2–8 qubits. The accuracy numbers above were measured at 2–4 qubits, so this is the axis that says whether they transfer.

get_concentration_profile

get_concentration_profile(name: str) -> ConcentrationProfile

Return the measured concentration profile for a single encoding.

Parameters

name : str Encoding identifier. Canonical names ("qaoa"), dataset aliases ("qaoa_encoding"), and class display names ("QAOAEncoding") are all accepted, case-insensitively.

Returns

ConcentrationProfile The encoding's measured concentration scaling.

Raises

KeyError If name does not correspond to any scanned encoding.

Examples

profile = get_concentration_profile("iqp") profile.horizon 2

Source code in src/encoding_atlas/atlas/concentration.py
def get_concentration_profile(name: str) -> ConcentrationProfile:
    """Return the measured concentration profile for a single encoding.

    Parameters
    ----------
    name : str
        Encoding identifier. Canonical names (``"qaoa"``), dataset aliases
        (``"qaoa_encoding"``), and class display names (``"QAOAEncoding"``)
        are all accepted, case-insensitively.

    Returns
    -------
    ConcentrationProfile
        The encoding's measured concentration scaling.

    Raises
    ------
    KeyError
        If ``name`` does not correspond to any scanned encoding.

    Examples
    --------
    >>> profile = get_concentration_profile("iqp")
    >>> profile.horizon
    2
    """
    index = _concentration_index()
    key = name.strip().lower()
    if key not in index:
        available = ", ".join(sorted(p.name for p in _concentration_profiles()))
        raise KeyError(f"Unknown encoding {name!r}. Scanned encodings: {available}")
    return index[key]

list_concentration_profiles

list_concentration_profiles() -> list[ConcentrationProfile]

Return all concentration profiles, ordered by canonical name.

Source code in src/encoding_atlas/atlas/concentration.py
def list_concentration_profiles() -> list[ConcentrationProfile]:
    """Return all concentration profiles, ordered by canonical name."""
    return list(_concentration_profiles())

concentrated_encodings

concentrated_encodings(*, threshold: float = _DEFAULT_THRESHOLD) -> list[ConcentrationProfile]

Return the encodings whose kernel reaches the Haar floor.

These are the encodings for which a fidelity-kernel method stops working as the circuit widens, regardless of shot budget or noise level.

Parameters

threshold : float, default=2.0 concentration_ratio defining the floor. An encoding qualifies when its widest measured point falls below this. Must be positive.

Returns

list[ConcentrationProfile] Matching profiles, ordered by canonical name.

Raises

ValueError If threshold is not positive.

Examples

sorted(p.name for p in concentrated_encodings()) ['hamiltonian', 'iqp', 'pauli_feature_map', 'zz_feature_map']

Source code in src/encoding_atlas/atlas/concentration.py
def concentrated_encodings(
    *, threshold: float = _DEFAULT_THRESHOLD
) -> list[ConcentrationProfile]:
    """Return the encodings whose kernel reaches the Haar floor.

    These are the encodings for which a fidelity-kernel method stops working
    as the circuit widens, regardless of shot budget or noise level.

    Parameters
    ----------
    threshold : float, default=2.0
        ``concentration_ratio`` defining the floor. An encoding qualifies when
        its widest measured point falls below this. Must be positive.

    Returns
    -------
    list[ConcentrationProfile]
        Matching profiles, ordered by canonical name.

    Raises
    ------
    ValueError
        If ``threshold`` is not positive.

    Examples
    --------
    >>> sorted(p.name for p in concentrated_encodings())
    ['hamiltonian', 'iqp', 'pauli_feature_map', 'zz_feature_map']
    """
    if threshold <= 0.0:
        raise ValueError(f"threshold must be positive, got {threshold}")
    return [
        p
        for p in _concentration_profiles()
        if p.points and p.points[-1].concentration_ratio < threshold
    ]

concentration_metadata

concentration_metadata() -> dict[str, Any]

Return provenance and the measurement protocol for the scan.

Returns

dict Schema version, encoding count, the full measurement protocol (feature counts, sample count, input range, sampling strategy, threshold, seed, backend), and a human-readable source string.

Source code in src/encoding_atlas/atlas/concentration.py
def concentration_metadata() -> dict[str, Any]:
    """Return provenance and the measurement protocol for the scan.

    Returns
    -------
    dict
        Schema version, encoding count, the full measurement protocol
        (feature counts, sample count, input range, sampling strategy,
        threshold, seed, backend), and a human-readable ``source`` string.
    """
    raw = load_concentration_raw()
    return {
        "schema_version": raw["schema_version"],
        "n_encodings": raw["n_encodings"],
        "protocol": copy.deepcopy(raw["protocol"]),
        "generated_by": raw["generated_by"],
        "source": CONCENTRATION_SOURCE,
    }

ConcentrationProfile dataclass

ConcentrationProfile(name: str, display_name: str, params: Mapping[str, Any], points: tuple[ConcentrationPoint, ...], decay_rate: float | None, mean_decay_rate: float | None, haar_normalized_slope: float | None, r_squared: float | None, horizon: int | None, skipped: Mapping[str, str])

Measured kernel-concentration scaling for a single encoding.

Attributes

name : str Canonical (registry-primary) encoding name, e.g. "qaoa". display_name : str Human-friendly class name, e.g. "QAOAEncoding". params : Mapping[str, Any] Fixed circuit parameters the scan used (matching the benchmark's kernel stage), e.g. {"reps": 2}. points : tuple[ConcentrationPoint, ...] Per-width measurements, ordered by circuit width. decay_rate : float or None Per-qubit factor by which the off-diagonal variance shrinks. The Haar floor itself falls at 4.0, so a rate near 4 means the encoding scrambles as fast as it can. None if unfittable (too few widths). mean_decay_rate : float or None The same for the off-diagonal mean; the Haar floor falls at 2.0. haar_normalized_slope : float or None Slope of log(concentration_ratio) against qubit count. Negative means the kernel is collapsing towards the floor as the circuit widens; positive means it is pulling away. r_squared : float or None Quality of the log-linear variance fit, in [0, 1]. horizon : int or None Narrowest measured width from which the encoding stays at the Haar floor, or an extrapolation when the trend points there. None means the encoding shows no sign of reaching the floor over 2-8 qubits. skipped : Mapping[str, str] Feature counts the scan could not measure (an encoding requiring exactly two features, or an even count), mapped to the reason.

at_features

at_features(n_features: int) -> ConcentrationPoint

Return the measured point closest to n_features.

The scan measured a fixed grid of feature counts, so this snaps to the nearest measured configuration rather than interpolating. Ties break towards the wider circuit, which is the conservative direction for a concentration question.

Parameters

n_features : int Feature count of interest. Must be a positive integer.

Returns

ConcentrationPoint The nearest measured point.

Raises

ValueError If n_features is not a positive integer. RuntimeError If this profile has no measured points at all.

Source code in src/encoding_atlas/atlas/concentration.py
def at_features(self, n_features: int) -> ConcentrationPoint:
    """Return the measured point closest to ``n_features``.

    The scan measured a fixed grid of feature counts, so this snaps to the
    nearest measured configuration rather than interpolating. Ties break
    towards the wider circuit, which is the conservative direction for a
    concentration question.

    Parameters
    ----------
    n_features : int
        Feature count of interest. Must be a positive integer.

    Returns
    -------
    ConcentrationPoint
        The nearest measured point.

    Raises
    ------
    ValueError
        If ``n_features`` is not a positive integer.
    RuntimeError
        If this profile has no measured points at all.
    """
    if isinstance(n_features, bool) or not isinstance(n_features, int):
        raise ValueError(
            f"n_features must be a positive integer, got {n_features!r}"
        )
    if n_features < 1:
        raise ValueError(
            f"n_features must be a positive integer, got {n_features!r}"
        )
    if not self.points:
        raise RuntimeError(
            f"encoding {self.name!r} has no measured concentration points"
        )
    return min(
        self.points,
        key=lambda p: (abs(p.n_features - n_features), -p.n_features),
    )

is_concentrated_at

is_concentrated_at(n_features: int, *, threshold: float = _DEFAULT_THRESHOLD) -> bool

Whether the kernel is at the Haar floor at roughly n_features.

Uses the nearest measured point (see :meth:at_features).

Parameters

n_features : int Feature count of interest. threshold : float, default=2.0 concentration_ratio defining the floor. Must be positive.

Returns

bool True when the nearest measured concentration_ratio falls below threshold.

Raises

ValueError If threshold is not positive, or n_features is invalid.

Source code in src/encoding_atlas/atlas/concentration.py
def is_concentrated_at(
    self, n_features: int, *, threshold: float = _DEFAULT_THRESHOLD
) -> bool:
    """Whether the kernel is at the Haar floor at roughly ``n_features``.

    Uses the nearest measured point (see :meth:`at_features`).

    Parameters
    ----------
    n_features : int
        Feature count of interest.
    threshold : float, default=2.0
        ``concentration_ratio`` defining the floor. Must be positive.

    Returns
    -------
    bool
        ``True`` when the nearest measured ``concentration_ratio`` falls
        below ``threshold``.

    Raises
    ------
    ValueError
        If ``threshold`` is not positive, or ``n_features`` is invalid.
    """
    if threshold <= 0.0:
        raise ValueError(f"threshold must be positive, got {threshold}")
    return self.at_features(n_features).concentration_ratio < threshold

ConcentrationPoint dataclass

ConcentrationPoint(n_features: int, n_qubits: int, concentration_ratio: float, mean_ratio: float, offdiagonal_mean: float, offdiagonal_variance: float, shots_per_entry: float, is_concentrated: bool)

One measured (width, concentration) point for an encoding.

Attributes

n_features : int Feature count the encoding was built with. n_qubits : int Resulting circuit width. Equal to n_features for most encodings; ceil(log2(n_features)) for amplitude encoding. concentration_ratio : float Off-diagonal kernel variance divided by the Haar variance at this width. Near 1 means the kernel is at the floor. mean_ratio : float Off-diagonal kernel mean divided by the Haar mean. Reported for completeness only — it is 1 for any ensemble of independent random single-qubit states as well as for Haar, so it cannot discriminate. offdiagonal_mean, offdiagonal_variance : float The raw measured moments of the off-diagonal entries. shots_per_entry : float Shots per kernel entry needed for the compute-uncompute estimator's standard error to fall below half the measured spread. A noiseless lower bound on hardware cost. is_concentrated : bool Whether concentration_ratio was below the scan's threshold.

Feature-Scaling Sensitivity Scan

What the feature range costs each encoding, and how far the study's own expressibility-accuracy correlation moves with that choice.

get_scaling_profile

get_scaling_profile(name: str) -> ScalingProfile

Return the measured scaling sensitivity for a single encoding.

Parameters

name : str Encoding identifier. Canonical names, dataset aliases, and class display names are all accepted, case-insensitively.

Returns

ScalingProfile

Raises

KeyError If name does not correspond to any scanned encoding.

Examples

profile = get_scaling_profile("iqp") profile.accuracy_spread > 0.2 True

Source code in src/encoding_atlas/atlas/scaling.py
def get_scaling_profile(name: str) -> ScalingProfile:
    """Return the measured scaling sensitivity for a single encoding.

    Parameters
    ----------
    name : str
        Encoding identifier. Canonical names, dataset aliases, and class
        display names are all accepted, case-insensitively.

    Returns
    -------
    ScalingProfile

    Raises
    ------
    KeyError
        If ``name`` does not correspond to any scanned encoding.

    Examples
    --------
    >>> profile = get_scaling_profile("iqp")
    >>> profile.accuracy_spread > 0.2
    True
    """
    index = _scaling_index()
    key = name.strip().lower()
    if key not in index:
        available = ", ".join(sorted(p.name for p in _scaling_profiles()))
        raise KeyError(f"Unknown encoding {name!r}. Scanned encodings: {available}")
    return index[key]

list_scaling_profiles

list_scaling_profiles() -> list[ScalingProfile]

Return all scaling profiles, ordered by canonical name.

Source code in src/encoding_atlas/atlas/scaling.py
def list_scaling_profiles() -> list[ScalingProfile]:
    """Return all scaling profiles, ordered by canonical name."""
    return list(_scaling_profiles())

scaling_sensitive_encodings

scaling_sensitive_encodings(*, threshold: float = 0.15) -> list[ScalingProfile]

Return encodings whose accuracy moves more than threshold with scaling.

These are the encodings for which the feature range is not a detail: report it, and tune it before drawing conclusions.

Parameters

threshold : float, default=0.15 Minimum accuracy spread across the scanned ranges. Must be non-negative.

Returns

list[ScalingProfile] Matching profiles, most sensitive first.

Raises

ValueError If threshold is negative.

Source code in src/encoding_atlas/atlas/scaling.py
def scaling_sensitive_encodings(*, threshold: float = 0.15) -> list[ScalingProfile]:
    """Return encodings whose accuracy moves more than ``threshold`` with scaling.

    These are the encodings for which the feature range is not a detail: report
    it, and tune it before drawing conclusions.

    Parameters
    ----------
    threshold : float, default=0.15
        Minimum accuracy spread across the scanned ranges. Must be
        non-negative.

    Returns
    -------
    list[ScalingProfile]
        Matching profiles, most sensitive first.

    Raises
    ------
    ValueError
        If ``threshold`` is negative.
    """
    if threshold < 0.0:
        raise ValueError(f"threshold must be non-negative, got {threshold}")
    matches = [p for p in _scaling_profiles() if p.accuracy_spread > threshold]
    return sorted(matches, key=lambda p: (-p.accuracy_spread, p.name))

expressibility_accuracy_correlation

expressibility_accuracy_correlation() -> list[dict[str, Any]]

Return the expressibility-accuracy correlation at each scaling range.

This is the scan's scientific payload: hypothesis H1 evaluated as a function of the preprocessing choice it was originally measured under.

Returns

list[dict] One entry per range, ordered as scanned, each with low, high, spearman_rho / p_value / n_encodings over every measurable encoding, the same three over the _atlas_subset (the encodings the atlas reports an expressibility for — the set the published analysis used), and is_published_range. A deep copy is returned, so callers cannot mutate the cached dataset.

Source code in src/encoding_atlas/atlas/scaling.py
def expressibility_accuracy_correlation() -> list[dict[str, Any]]:
    """Return the expressibility-accuracy correlation at each scaling range.

    This is the scan's scientific payload: hypothesis H1 evaluated as a
    function of the preprocessing choice it was originally measured under.

    Returns
    -------
    list[dict]
        One entry per range, ordered as scanned, each with ``low``, ``high``,
        ``spearman_rho`` / ``p_value`` / ``n_encodings`` over every measurable
        encoding, the same three over the ``_atlas_subset`` (the encodings the
        atlas reports an expressibility for — the set the published analysis
        used), and ``is_published_range``. A deep copy is returned, so callers
        cannot mutate the cached dataset.
    """
    return copy.deepcopy(
        list(load_scaling_raw()["expressibility_accuracy_correlation"])
    )

scaling_metadata

scaling_metadata() -> dict[str, Any]

Return provenance and the measurement protocol for the scan.

Returns

dict Schema version, encoding count, the full protocol (ranges, datasets, sample and fold counts, seed, backend, and the pipeline's own published_range), and a human-readable source string.

Source code in src/encoding_atlas/atlas/scaling.py
def scaling_metadata() -> dict[str, Any]:
    """Return provenance and the measurement protocol for the scan.

    Returns
    -------
    dict
        Schema version, encoding count, the full protocol (ranges, datasets,
        sample and fold counts, seed, backend, and the pipeline's own
        ``published_range``), and a human-readable ``source`` string.
    """
    raw = load_scaling_raw()
    return {
        "schema_version": raw["schema_version"],
        "n_encodings": raw["n_encodings"],
        "protocol": copy.deepcopy(raw["protocol"]),
        "generated_by": raw["generated_by"],
        "source": SCALING_SOURCE,
    }

ScalingProfile dataclass

ScalingProfile(name: str, display_name: str, params: Mapping[str, Any], points: tuple[ScalingPoint, ...], best_range: tuple[float, float], accuracy_spread: float)

Measured feature-scaling sensitivity for a single encoding.

Attributes

name : str Canonical (registry-primary) encoding name. display_name : str Human-friendly class name. params : Mapping[str, Any] Fixed circuit parameters the scan used. points : tuple[ScalingPoint, ...] Per-range measurements, ordered by increasing range width. best_range : tuple[float, float] The range that maximised alignment. accuracy_spread : float Highest minus lowest mean accuracy across ranges. For several encodings this exceeds the spread between encodings, which is the point: scaling is not a minor knob.

published property

published: ScalingPoint | None

The measurement at the pipeline's own [0, 2*pi] range.

at_range

at_range(low: float, high: float) -> ScalingPoint | None

Return the measurement for an exact range, or None.

Source code in src/encoding_atlas/atlas/scaling.py
def at_range(self, low: float, high: float) -> ScalingPoint | None:
    """Return the measurement for an exact range, or ``None``."""
    for point in self.points:
        if point.low == low and point.high == high:
            return point
    return None

ScalingPoint dataclass

ScalingPoint(low: float, high: float, width: float, mean_alignment: float, mean_accuracy: float, mean_concentration_ratio: float, expressibility: float | None, n_datasets: int)

One encoding measured at one feature-scaling range.

Attributes

low, high : float The range the features were scaled into. width : float high - low; 2*pi is one full rotation period. mean_alignment : float Mean centred kernel-target alignment across the benchmark datasets. mean_accuracy : float Mean cross-validated quantum-kernel accuracy across those datasets. mean_concentration_ratio : float Mean kernel variance relative to the Haar floor. Falling towards 1 as the range widens is the mechanism behind the accuracy loss. expressibility : float or None Expressibility re-measured with inputs drawn from this same range, so both axes of the correlation describe one regime. n_datasets : int Datasets contributing to these means.


Exceptions

exceptions

Custom exceptions for encoding_atlas.

This module defines the exception hierarchy for the encoding_atlas package. All custom exceptions inherit from :class:EncodingError, which itself inherits from Python's built-in :class:Exception.

Exception Hierarchy

::

Exception
└── EncodingError (base for all encoding_atlas exceptions)
    ├── ValidationError (input validation failures)
    ├── BackendError (quantum backend issues)
    ├── RegistryError (encoding registry issues)
    └── AnalysisError (analysis operation failures)
        ├── SimulationError (circuit simulation failures)
        ├── ConvergenceError (iterative computation failures)
        ├── NumericalInstabilityError (numerical issues)
        └── InsufficientSamplesError (sampling issues)

Examples

Catching all encoding-related exceptions:

try: ... result = some_encoding_operation() ... except EncodingError as e: ... print(f"Encoding operation failed: {e}")

Catching specific analysis exceptions:

from encoding_atlas.core.exceptions import SimulationError try: ... statevector = simulate_encoding(encoding, x) ... except SimulationError as e: ... print(f"Simulation failed: {e}")

AnalysisError

AnalysisError(message: str, details: dict | None = None)

Bases: EncodingError

Base exception for analysis operations.

This exception class serves as the base for all analysis-related errors in the encoding_atlas.analysis module. It inherits from :class:EncodingError to maintain the package's exception hierarchy.

Use this exception for general analysis failures that don't fit into more specific categories. For specific failure modes, use the specialized subclasses.

Parameters

message : str Human-readable description of the error. details : dict, optional Additional context about the error (e.g., parameter values, intermediate results).

Attributes

details : dict Additional context about the error, if provided.

Examples

raise AnalysisError("Analysis failed for encoding") Traceback (most recent call last): ... AnalysisError: Analysis failed for encoding

With additional details:

raise AnalysisError( ... "Invalid encoding for analysis", ... details={"encoding_type": "CustomEncoding", "n_qubits": 0} ... )

BackendError

Bases: EncodingError

Raised when quantum backend operations fail.

This exception indicates a failure in the underlying quantum computing framework (PennyLane, Qiskit, or Cirq). Common causes include missing backend installations, invalid circuit operations, or backend-specific limitations.

Examples

raise BackendError("Qiskit backend not available") Traceback (most recent call last): ... BackendError: Qiskit backend not available

ConvergenceError

ConvergenceError(message: str, iterations: int | None = None, target_tolerance: float | None = None, achieved_tolerance: float | None = None, details: dict | None = None)

Bases: AnalysisError

Raised when iterative computation fails to converge.

This exception indicates that an iterative algorithm did not reach the desired convergence criteria within the allowed iterations or time. Common causes include:

  • Insufficient sample count
  • Poor initial conditions
  • Numerically unstable problem
  • Convergence criteria too strict
Parameters

message : str Human-readable description of the convergence failure. iterations : int, optional Number of iterations attempted before failure. target_tolerance : float, optional The convergence tolerance that was not achieved. achieved_tolerance : float, optional The best tolerance achieved before stopping. details : dict, optional Additional context about the failure.

Attributes

iterations : int or None Number of iterations attempted. target_tolerance : float or None The target convergence tolerance. achieved_tolerance : float or None The best tolerance achieved. details : dict Additional context about the failure.

Examples

raise ConvergenceError( ... "Expressibility estimation did not converge", ... iterations=10000, ... target_tolerance=1e-4, ... achieved_tolerance=1e-2 ... )

EncodingError

Bases: Exception

Base exception for all encoding_atlas errors.

This is the root exception class for the encoding_atlas package. All custom exceptions in this package inherit from this class, allowing users to catch all encoding-related errors with a single except clause.

Parameters

message : str Human-readable description of the error. *args : tuple Additional positional arguments passed to Exception.

Examples

raise EncodingError("Something went wrong") Traceback (most recent call last): ... EncodingError: Something went wrong

InsufficientSamplesError

InsufficientSamplesError(message: str, requested_samples: int | None = None, minimum_samples: int | None = None, metric: str | None = None, details: dict | None = None)

Bases: AnalysisError

Raised when sample count is too low for reliable results.

This exception indicates that a statistical analysis cannot provide reliable results with the given number of samples. This is distinct from a convergence failure — here, the algorithm recognizes upfront that the sample count is inadequate.

Parameters

message : str Human-readable description of the sampling issue. requested_samples : int, optional The number of samples requested by the user. minimum_samples : int, optional The minimum number of samples required for reliable results. metric : str, optional The metric being computed (e.g., "expressibility"). details : dict, optional Additional context about the sampling requirements.

Attributes

requested_samples : int or None The number of samples requested. minimum_samples : int or None The minimum required samples. metric : str or None The metric being computed. details : dict Additional context about the failure.

Examples

raise InsufficientSamplesError( ... "Too few samples for expressibility estimation", ... requested_samples=10, ... minimum_samples=100, ... metric="expressibility" ... )

Notes

When this exception is raised, the recommended action is to increase the sample count. The minimum_samples attribute provides guidance on the minimum viable sample count.

NumericalInstabilityError

NumericalInstabilityError(message: str, value: float | None = None, operation: str | None = None, details: dict | None = None)

Bases: AnalysisError

Raised when numerical instability is detected.

This exception indicates that a computation encountered numerical issues that compromise the accuracy or validity of results. Common causes include:

  • Division by near-zero values
  • Overflow or underflow in intermediate calculations
  • Loss of precision in floating-point operations
  • Ill-conditioned matrices
Parameters

message : str Human-readable description of the numerical issue. value : float, optional The problematic value that triggered the error. operation : str, optional The operation that encountered the issue. details : dict, optional Additional context about the numerical issue.

Attributes

value : float or None The problematic value, if applicable. operation : str or None The operation that failed, if specified. details : dict Additional context about the failure.

Examples

raise NumericalInstabilityError( ... "Division by near-zero in fidelity computation", ... value=1e-320, ... operation="compute_fidelity" ... )

Notes

This exception is distinct from Python's built-in arithmetic exceptions (ZeroDivisionError, OverflowError) because it catches cases where the computation technically succeeds but produces unreliable results due to floating-point limitations.

RegistryError

Bases: EncodingError

Raised when encoding registry operations fail.

This exception indicates a failure in the encoding registration or lookup system. Common causes include duplicate registrations or requests for non-existent encodings.

Examples

raise RegistryError("Encoding 'CustomEncoding' not found in registry") Traceback (most recent call last): ... RegistryError: Encoding 'CustomEncoding' not found in registry

SimulationError

SimulationError(message: str, backend: str | None = None, details: dict | None = None)

Bases: AnalysisError

Raised when quantum circuit simulation fails.

This exception indicates that a quantum circuit could not be successfully simulated. Common causes include:

  • Missing backend dependencies (PennyLane, Qiskit not installed)
  • Invalid circuit structure
  • Memory limitations for large qubit counts
  • Backend-specific errors
Parameters

message : str Human-readable description of the simulation failure. backend : str, optional The backend that failed (e.g., "pennylane", "qiskit"). details : dict, optional Additional context about the failure.

Attributes

backend : str or None The backend that failed, if specified. details : dict Additional context about the failure.

Examples

raise SimulationError("PennyLane simulation failed: device error") Traceback (most recent call last): ... SimulationError: PennyLane simulation failed: device error

With backend information:

raise SimulationError( ... "Simulation failed", ... backend="qiskit", ... details={"n_qubits": 20, "error": "memory exceeded"} ... )

ValidationError

Bases: EncodingError

Raised when input validation fails.

This exception indicates that input data does not meet the requirements of an encoding or analysis function. Common causes include incorrect shapes, invalid data types, or out-of-range values.

Examples

raise ValidationError("Expected 4 features, got 3") Traceback (most recent call last): ... ValidationError: Expected 4 features, got 3