Pauli Feature Map Encoding - Comprehensive Guide¶
This notebook provides a thorough walkthrough of the PauliFeatureMap encoding from the Quantum Encoding Atlas library. It covers every feature, configuration option, edge case, and analysis tool available.
Table of Contents¶
- Overview & Mathematical Background
- Installation & Imports
- Creating a PauliFeatureMap
- Configuration Parameters Deep Dive
- 4.1 Pauli Terms
- 4.2 Repetitions (reps)
- 4.3 Entanglement Topologies
- Core Properties & Attributes
- Entanglement Pair Inspection
- Circuit Generation Across Backends
- 7.1 PennyLane Backend
- 7.2 Qiskit Backend
- 7.3 Cirq Backend
- Batch Circuit Generation & Parallel Processing
- Gate Count Breakdown & Resource Summary
- EncodingProperties & Lazy Evaluation
- Capability Protocols (isinstance Checks)
- Analysis Tools
- 12.1 Resource Counting
- 12.2 Simulability Analysis
- 12.3 Expressibility
- 12.4 Entanglement Capability
- 12.5 Trainability & Barren Plateau Detection
- Statevector Simulation & Quantum State Inspection
- Equality, Hashing & Serialization
- Configuration Access & Read-Only Safety
- Input Validation & Edge Cases
- Logging & Warnings
- Comparison with Related Encodings
- Registry System
- Hardware Planning Guide
- Summary
1. Overview & Mathematical Background¶
The PauliFeatureMap encodes classical data into quantum states using configurable Pauli rotation gates. It generalizes simpler encodings like ZZFeatureMap by allowing users to specify which Pauli terms to include.
Circuit Structure (per repetition)¶
- Hadamard Layer: $H^{\otimes n}$ creates uniform superposition
- Single-Qubit Pauli Rotations: $R_P(2x_i)$ for each feature $x_i$ and Pauli $P \in \{X, Y, Z\}$
- Two-Qubit Pauli Interactions: $\exp\left(-i\frac{\phi}{2} P_a \otimes P_b\right)$ for qubit pairs
Feature Mapping Functions¶
- Single-qubit: $\phi(x_i) = 2 x_i$
- Two-qubit: $\phi(x_i, x_j) = 2(\pi - x_i)(\pi - x_j)$
Two-Qubit Gate Decomposition¶
Two-qubit Pauli rotations are decomposed as:
$$\exp\left(-i\frac{\phi}{2} P_a \otimes P_b\right) = \text{BasisChange} \cdot \text{CNOT} \cdot R_Z(\phi) \cdot \text{CNOT} \cdot \text{BasisChange}^\dagger$$
The basis change depends on the Pauli type:
- Z: No change (computational basis)
- X: Hadamard gate
- Y: $S^\dagger \cdot H$ (rotate to Y-basis)
References¶
- Havlíček, V., et al. (2019). "Supervised learning with quantum-enhanced feature spaces." Nature, 567(7747), 209-212.
- Schuld, M., & Killoran, N. (2019). "Quantum Machine Learning in Feature Hilbert Spaces." Physical Review Letters, 122(4), 040504.
2. Installation & Imports¶
# Install the library (uncomment if needed)
# pip install encoding-atlas[all] # All backends (PennyLane, Qiskit, Cirq)
# pip install encoding-atlas # Core only (PennyLane)
# pip install encoding-atlas[qiskit] # Core + Qiskit
# pip install encoding-atlas[cirq] # Core + Cirq
import numpy as np
import warnings
import pickle
import logging
from encoding_atlas import (
PauliFeatureMap,
ZZFeatureMap,
AngleEncoding,
IQPEncoding,
BaseEncoding,
EncodingProperties,
)
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
EntanglementQueryable,
DataTransformable,
)
from encoding_atlas import analysis
print("All imports successful!")
All imports successful!
3. Creating a PauliFeatureMap¶
The simplest way to create a PauliFeatureMap requires only the number of features. All other parameters have sensible defaults.
# Minimal creation - defaults: reps=2, paulis=["Z", "ZZ"], entanglement="full"
enc = PauliFeatureMap(n_features=4)
print(enc)
print(f" n_features: {enc.n_features}")
print(f" n_qubits: {enc.n_qubits}")
print(f" reps: {enc.reps}")
print(f" paulis: {enc.paulis}")
print(f" entanglement: {enc.entanglement}")
print(f" depth: {enc.depth}")
PauliFeatureMap(n_features=4, reps=2, paulis=['Z', 'ZZ'], entanglement='full') n_features: 4 n_qubits: 4 reps: 2 paulis: ['Z', 'ZZ'] entanglement: full depth: 10
# Fully specified creation
enc_custom = PauliFeatureMap(
n_features=6,
reps=3,
paulis=["X", "Y", "Z", "XX", "YY", "ZZ"],
entanglement="linear",
)
print(enc_custom)
PauliFeatureMap(n_features=6, reps=3, paulis=['X', 'Y', 'Z', 'XX', 'YY', 'ZZ'], entanglement='linear')
4. Configuration Parameters Deep Dive¶
4.1 Pauli Terms¶
The paulis parameter controls which rotation gates are applied.
Valid single-qubit terms: "X", "Y", "Z" (applied to every qubit)
Valid two-qubit terms: "XX", "YY", "ZZ", "XY", "XZ", "YZ", "YX", "ZX", "ZY" (applied to entangled pairs)
| 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 |
# Single-qubit Pauli terms only (no entanglement)
enc_single = PauliFeatureMap(n_features=4, paulis=["X", "Y", "Z"])
print(f"Single-qubit only: {enc_single.paulis}")
print(f" Is entangling: {enc_single.properties.is_entangling}")
# Two-qubit Pauli terms only
enc_two = PauliFeatureMap(n_features=4, paulis=["ZZ"])
print(f"\nTwo-qubit only: {enc_two.paulis}")
print(f" Is entangling: {enc_two.properties.is_entangling}")
# Mixed single and two-qubit terms
enc_mixed = PauliFeatureMap(n_features=4, paulis=["Z", "XX", "YZ"])
print(f"\nMixed terms: {enc_mixed.paulis}")
print(f" Is entangling: {enc_mixed.properties.is_entangling}")
Single-qubit only: ['X', 'Y', 'Z'] Is entangling: False Two-qubit only: ['ZZ'] Is entangling: True Mixed terms: ['Z', 'XX', 'YZ'] Is entangling: True
# Pauli terms are normalized to uppercase automatically
enc_lower = PauliFeatureMap(n_features=4, paulis=["z", "zz"])
print(f"Input: ['z', 'zz'] -> Stored: {enc_lower.paulis}")
# Duplicates are removed while preserving order
enc_dup = PauliFeatureMap(n_features=4, paulis=["Z", "ZZ", "Z", "ZZ", "X"])
print(f"Input: ['Z', 'ZZ', 'Z', 'ZZ', 'X'] -> Stored: {enc_dup.paulis}")
Input: ['z', 'zz'] -> Stored: ['Z', 'ZZ'] Input: ['Z', 'ZZ', 'Z', 'ZZ', 'X'] -> Stored: ['Z', 'ZZ', 'X']
# All 9 valid two-qubit Pauli combinations (including asymmetric ones)
all_two_qubit = ["XX", "YY", "ZZ", "XY", "XZ", "YZ", "YX", "ZX", "ZY"]
enc_all = PauliFeatureMap(n_features=3, paulis=all_two_qubit, reps=1)
print(f"All two-qubit Paulis: {enc_all.paulis}")
breakdown = enc_all.gate_count_breakdown()
print(f"Total gates with all 9 two-qubit terms: {breakdown['total']}")
print(f"CNOT gates: {breakdown['cnot']}")
All two-qubit Paulis: ['XX', 'YY', 'ZZ', 'XY', 'XZ', 'YZ', 'YX', 'ZX', 'ZY'] Total gates with all 9 two-qubit terms: 192 CNOT gates: 54
# Default paulis are ["Z", "ZZ"] - equivalent to ZZFeatureMap
enc_default = PauliFeatureMap(n_features=4)
print(f"Default paulis: {enc_default.paulis}")
print("This is equivalent to ZZFeatureMap with the same parameters.")
Default paulis: ['Z', 'ZZ'] This is equivalent to ZZFeatureMap with the same parameters.
4.2 Repetitions (reps)¶
The reps parameter controls the number of encoding layers. More repetitions increase expressibility but also circuit depth.
# Compare different repetition counts
for reps in [1, 2, 3, 5]:
enc = PauliFeatureMap(n_features=4, reps=reps, paulis=["Z", "ZZ"])
bd = enc.gate_count_breakdown()
print(f"reps={reps}: depth={enc.depth:3d}, total_gates={bd['total']:4d}, "
f"cnot={bd['cnot']:3d}, trainability~{enc.properties.trainability_estimate:.2f}")
reps=1: depth= 5, total_gates= 26, cnot= 12, trainability~0.65 reps=2: depth= 10, total_gates= 52, cnot= 24, trainability~0.40 reps=3: depth= 15, total_gates= 78, cnot= 36, trainability~0.15 reps=5: depth= 25, total_gates= 130, cnot= 60, trainability~0.10
4.3 Entanglement Topologies¶
Three entanglement patterns control how two-qubit gates are connected:
| Topology | Gate Scaling | Connectivity | Best For |
|---|---|---|---|
"full" |
$O(n^2)$ | All-to-all | Max expressivity |
"linear" |
$O(n)$ | Nearest-only | NISQ devices |
"circular" |
$O(n)$ | Ring | Periodic problems |
n = 6
for topology in ["full", "linear", "circular"]:
enc = PauliFeatureMap(n_features=n, paulis=["Z", "ZZ"], entanglement=topology)
pairs = enc.get_entanglement_pairs()
bd = enc.gate_count_breakdown()
print(f"{topology:8s}: {len(pairs):2d} pairs, {bd['cnot']:3d} CNOTs, "
f"depth={enc.depth}")
print(f" Pairs: {pairs}")
full : 15 pairs, 60 CNOTs, depth=10
Pairs: [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]
linear : 5 pairs, 20 CNOTs, depth=10
Pairs: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
circular: 6 pairs, 24 CNOTs, depth=10
Pairs: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "X", "ZZ"], entanglement="full")
# Direct attributes
print("=== Direct Attributes ===")
print(f"n_features: {enc.n_features}") # Number of classical features
print(f"n_qubits: {enc.n_qubits}") # Always == n_features (1:1 mapping)
print(f"reps: {enc.reps}") # Number of circuit layers
print(f"paulis: {enc.paulis}") # Stored Pauli terms
print(f"entanglement: {enc.entanglement}") # Topology name
print(f"depth: {enc.depth}") # Circuit depth
# Depth calculation:
# depth_per_rep = 1 (H layer) + len(single_paulis) + 3 * len(two_paulis)
# = 1 + 2 + 3*1 = 6
# total depth = reps * depth_per_rep = 2 * 6 = 12
print(f"\nDepth verification: reps * (1 + {2} + 3*{1}) = {enc.reps} * 6 = {enc.depth}")
=== Direct Attributes === n_features: 4 n_qubits: 4 reps: 2 paulis: ['Z', 'X', 'ZZ'] entanglement: full depth: 12 Depth verification: reps * (1 + 2 + 3*1) = 2 * 6 = 12
# The __repr__ provides a complete reconstruction string
print(repr(enc))
PauliFeatureMap(n_features=4, reps=2, paulis=['Z', 'X', 'ZZ'], entanglement='full')
6. Entanglement Pair Inspection¶
get_entanglement_pairs() returns the list of qubit pairs that will have two-qubit interactions. This is useful for understanding circuit structure and verifying hardware connectivity requirements.
# Full entanglement: all unique pairs (i, j) where i < j
enc_full = PauliFeatureMap(n_features=5, entanglement="full")
pairs_full = enc_full.get_entanglement_pairs()
print(f"Full (5 qubits): {len(pairs_full)} pairs = n(n-1)/2 = 5*4/2 = 10")
print(f" {pairs_full}")
# Linear entanglement: adjacent pairs (i, i+1)
enc_lin = PauliFeatureMap(n_features=5, entanglement="linear")
pairs_lin = enc_lin.get_entanglement_pairs()
print(f"\nLinear (5 qubits): {len(pairs_lin)} pairs = n-1 = 4")
print(f" {pairs_lin}")
# Circular entanglement: linear + wrap-around (n-1, 0)
enc_circ = PauliFeatureMap(n_features=5, entanglement="circular")
pairs_circ = enc_circ.get_entanglement_pairs()
print(f"\nCircular (5 qubits): {len(pairs_circ)} pairs = n = 5")
print(f" {pairs_circ}")
Full (5 qubits): 10 pairs = n(n-1)/2 = 5*4/2 = 10 [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] Linear (5 qubits): 4 pairs = n-1 = 4 [(0, 1), (1, 2), (2, 3), (3, 4)] Circular (5 qubits): 5 pairs = n = 5 [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]
# Returns a copy - modifying it doesn't affect the encoding
pairs = enc_full.get_entanglement_pairs()
pairs.clear() # Safe: this is a copy
print(f"Original still intact: {len(enc_full.get_entanglement_pairs())} pairs")
Original still intact: 10 pairs
# Edge case: single qubit - no pairs possible
enc_1q = PauliFeatureMap(n_features=1, paulis=["Z", "ZZ"])
print(f"1 qubit with ZZ Pauli: pairs = {enc_1q.get_entanglement_pairs()}")
print(f" Is entangling: {enc_1q.properties.is_entangling}")
# Edge case: 2 qubits with circular entanglement (no wrap-around for n<=2)
enc_2q = PauliFeatureMap(n_features=2, entanglement="circular", paulis=["ZZ"])
print(f"\n2 qubits, circular: pairs = {enc_2q.get_entanglement_pairs()}")
print(f" Same as linear for n=2 (no wrap-around needed)")
1 qubit with ZZ Pauli: pairs = [] Is entangling: False 2 qubits, circular: pairs = [(0, 1)] Same as linear for n=2 (no wrap-around needed)
import pennylane as qml
enc = PauliFeatureMap(n_features=3, reps=1, paulis=["Z", "ZZ"], entanglement="linear")
x = np.array([0.5, 1.0, 1.5])
# PennyLane returns a callable closure that applies gates when called
circuit_fn = enc.get_circuit(x, backend="pennylane")
print(f"Type: {type(circuit_fn)}")
# Use it inside a QNode
dev = qml.device("default.qubit", wires=enc.n_qubits)
@qml.qnode(dev)
def qnode():
circuit_fn() # Apply the encoding gates
return qml.state()
state = qnode()
print(f"Output state shape: {state.shape}")
print(f"State vector (first 4 amplitudes): {state[:4].round(4)}")
print(f"State norm: {np.linalg.norm(state):.6f}")
Type: <class 'function'> Output state shape: (8,) State vector (first 4 amplitudes): [ 0.3265+0.1356j -0.191 -0.2975j -0.1108+0.3357j -0.191 -0.2975j] State norm: 1.000000
# Visualize the PennyLane circuit using the draw utility
@qml.qnode(dev)
def draw_circuit():
circuit_fn()
return qml.state()
print(qml.draw(draw_circuit)())
0: ──H──RZ(1.00)─╭●────────────╭●─────────────────┤ State 1: ──H──RZ(2.00)─╰X──RZ(11.31)─╰X─╭●───────────╭●─┤ State 2: ──H──RZ(3.00)──────────────────╰X──RZ(7.03)─╰X─┤ State
7.2 Qiskit Backend¶
# Qiskit returns a QuantumCircuit object
qc = enc.get_circuit(x, backend="qiskit")
print(f"Type: {type(qc)}")
print(f"Circuit name: {qc.name}")
print(f"Qubits: {qc.num_qubits}")
print()
print(qc.draw(output='text'))
Type: <class 'qiskit.circuit.quantumcircuit.QuantumCircuit'>
Circuit name: PauliFeatureMap
Qubits: 3
┌───┐┌───────┐
q_0: ┤ H ├┤ Rz(1) ├──■──────────────────■──────────────────────────
├───┤├───────┤┌─┴─┐┌────────────┐┌─┴─┐
q_1: ┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(11.314) ├┤ X ├──■──────────────────■──
├───┤├───────┤└───┘└────────────┘└───┘┌─┴─┐┌────────────┐┌─┴─┐
q_2: ┤ H ├┤ Rz(3) ├────────────────────────┤ X ├┤ Rz(7.0312) ├┤ X ├
└───┘└───────┘ └───┘└────────────┘└───┘
7.3 Cirq Backend¶
import cirq
# Cirq returns a cirq.Circuit object
cirq_circuit = enc.get_circuit(x, backend="cirq")
print(f"Type: {type(cirq_circuit)}")
print(cirq_circuit)
Type: <class 'cirq.circuits.circuit.Circuit'>
0: ───H───Rz(0.318π)───@─────────────────@────────────────────────
│ │
1: ───H───Rz(0.637π)───X───Rz(-0.399π)───X───@────────────────@───
│ │
2: ───H───Rz(0.955π)─────────────────────────X───Rz(-1.76π)───X───
# Cross-backend equivalence: all three backends produce the same quantum state
from encoding_atlas.analysis import simulate_encoding_statevector
state_pl = simulate_encoding_statevector(enc, x, backend="pennylane")
state_qk = simulate_encoding_statevector(enc, x, backend="qiskit")
# Compare PennyLane vs Qiskit fidelity
fidelity = abs(np.vdot(state_pl, state_qk)) ** 2
print(f"PennyLane vs Qiskit fidelity: {fidelity:.10f}")
print(f"States are equivalent: {np.allclose(state_pl, state_qk, atol=1e-10)}")
PennyLane vs Qiskit fidelity: 1.0000000000 States are equivalent: True
8. Batch Circuit Generation & Parallel Processing¶
get_circuits() generates circuits for multiple data samples at once. It supports parallel processing for large batches.
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"])
X_batch = np.random.default_rng(42).uniform(0, 2 * np.pi, size=(50, 4))
# Sequential batch generation (default)
circuits_seq = enc.get_circuits(X_batch, backend="pennylane")
print(f"Sequential: {len(circuits_seq)} circuits generated")
# Parallel batch generation (beneficial for large batches)
circuits_par = enc.get_circuits(X_batch, backend="pennylane", parallel=True)
print(f"Parallel: {len(circuits_par)} circuits generated")
# Custom worker count
import os
circuits_custom = enc.get_circuits(
X_batch, backend="pennylane", parallel=True, max_workers=os.cpu_count()
)
print(f"Custom workers ({os.cpu_count()}): {len(circuits_custom)} circuits generated")
Sequential: 50 circuits generated Parallel: 50 circuits generated Custom workers (12): 50 circuits generated
# Order preservation: parallel results match sequential results
dev = qml.device("default.qubit", wires=4)
def get_state(circuit_fn):
@qml.qnode(dev)
def qn():
circuit_fn()
return qml.state()
return qn()
# Compare first 5 circuits from sequential vs parallel
for i in range(5):
s_seq = get_state(circuits_seq[i])
s_par = get_state(circuits_par[i])
match = np.allclose(s_seq, s_par, atol=1e-10)
print(f" Sample {i}: sequential == parallel? {match}")
Sample 0: sequential == parallel? True Sample 1: sequential == parallel? True Sample 2: sequential == parallel? True Sample 3: sequential == parallel? True Sample 4: sequential == parallel? True
# Single sample input is also accepted (1D array)
x_single = np.array([0.1, 0.2, 0.3, 0.4])
circuits_single = enc.get_circuits(x_single, backend="pennylane")
print(f"Single sample input -> {len(circuits_single)} circuit(s)")
Single sample input -> 1 circuit(s)
# Timing comparison: sequential vs parallel for a larger batch (Qiskit)
import time
X_large = np.random.default_rng(0).uniform(0, 2 * np.pi, size=(200, 4))
start = time.perf_counter()
_ = enc.get_circuits(X_large, backend="qiskit", parallel=False)
seq_time = time.perf_counter() - start
start = time.perf_counter()
_ = enc.get_circuits(X_large, backend="qiskit", parallel=True)
par_time = time.perf_counter() - start
print(f"200 Qiskit circuits:")
print(f" Sequential: {seq_time:.3f}s")
print(f" Parallel: {par_time:.3f}s")
print(f" Speedup: {seq_time / par_time:.2f}x")
200 Qiskit circuits: Sequential: 0.235s Parallel: 0.195s Speedup: 1.21x
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
breakdown = enc.gate_count_breakdown()
print("Gate Count Breakdown:")
print(f" Hadamard (H): {breakdown['hadamard']}")
print(f" RX: {breakdown['rx']}")
print(f" RY: {breakdown['ry']}")
print(f" RZ (single-qubit): {breakdown['rz']}")
print(f" RZ (two-qubit): {breakdown['rz_two_qubit']}")
print(f" CNOT: {breakdown['cnot']}")
print(f" Basis change: {breakdown['basis_change']}")
print(f" ---")
print(f" Total single-qubit: {breakdown['total_single_qubit']}")
print(f" Total two-qubit: {breakdown['total_two_qubit']}")
print(f" Total: {breakdown['total']}")
Gate Count Breakdown: Hadamard (H): 8 RX: 0 RY: 0 RZ (single-qubit): 8 RZ (two-qubit): 12 CNOT: 24 Basis change: 0 --- Total single-qubit: 28 Total two-qubit: 24 Total: 52
# Compare gate overhead across different Pauli configurations
configs = [
(["Z"], "Z only"),
(["Z", "ZZ"], "Z + ZZ"),
(["X", "XX"], "X + XX"),
(["Y", "YY"], "Y + YY"),
(["Z", "XY"], "Z + XY"),
(["X", "Y", "Z", "ZZ"], "X+Y+Z+ZZ"),
]
print(f"{'Config':<14s} {'Total':>6s} {'1-qubit':>8s} {'2-qubit':>8s} {'CNOT':>5s} {'Basis':>6s}")
print("-" * 52)
for paulis, label in configs:
e = PauliFeatureMap(n_features=4, reps=1, paulis=paulis)
b = e.gate_count_breakdown()
print(f"{label:<14s} {b['total']:>6d} {b['total_single_qubit']:>8d} "
f"{b['total_two_qubit']:>8d} {b['cnot']:>5d} {b['basis_change']:>6d}")
Config Total 1-qubit 2-qubit CNOT Basis ---------------------------------------------------- Z only 8 8 0 0 0 Z + ZZ 26 14 12 12 0 X + XX 50 38 12 12 24 Y + YY 74 62 12 12 48 Z + XY 62 50 12 12 36 X+Y+Z+ZZ 34 22 12 12 0
Resource Summary¶
resource_summary() provides a comprehensive report including circuit structure, gate counts, encoding characteristics, and hardware requirements.
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
summary = enc.resource_summary()
print("=== Resource Summary ===")
print(f"\nCircuit Structure:")
print(f" n_qubits: {summary['n_qubits']}")
print(f" n_features: {summary['n_features']}")
print(f" depth: {summary['depth']}")
print(f" reps: {summary['reps']}")
print(f"\nPauli Configuration:")
print(f" paulis: {summary['paulis']}")
print(f" single_paulis: {summary['single_paulis']}")
print(f" two_paulis: {summary['two_paulis']}")
print(f"\nEntanglement:")
print(f" topology: {summary['entanglement']}")
print(f" n_pairs: {summary['n_entanglement_pairs']}")
print(f" pairs: {summary['entanglement_pairs']}")
print(f"\nEncoding Characteristics:")
print(f" is_entangling: {summary['is_entangling']}")
print(f" simulability: {summary['simulability']}")
print(f" trainability_estimate: {summary['trainability_estimate']:.2f}")
print(f"\nHardware Requirements:")
hw = summary['hardware_requirements']
print(f" connectivity: {hw['connectivity']}")
print(f" native_gates: {hw['native_gates']}")
print(f" min_2q_gate_fidelity: {hw['min_two_qubit_gate_fidelity']}")
=== Resource Summary === Circuit Structure: n_qubits: 4 n_features: 4 depth: 10 reps: 2 Pauli Configuration: paulis: ['Z', 'ZZ'] single_paulis: ['Z'] two_paulis: ['ZZ'] Entanglement: topology: full n_pairs: 6 pairs: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] Encoding Characteristics: is_entangling: True simulability: not_simulable trainability_estimate: 0.40 Hardware Requirements: connectivity: all-to-all native_gates: ['CNOT', 'H', 'RZ'] min_2q_gate_fidelity: 0.99
# Hardware requirements change based on Pauli terms and topology
enc_y = PauliFeatureMap(n_features=4, paulis=["Y", "YY"], entanglement="linear")
summary_y = enc_y.resource_summary()
print(f"Y+YY with linear entanglement:")
print(f" connectivity: {summary_y['hardware_requirements']['connectivity']}")
print(f" native_gates: {summary_y['hardware_requirements']['native_gates']}")
enc_ring = PauliFeatureMap(n_features=4, paulis=["X", "ZZ"], entanglement="circular")
summary_ring = enc_ring.resource_summary()
print(f"\nX+ZZ with circular entanglement:")
print(f" connectivity: {summary_ring['hardware_requirements']['connectivity']}")
print(f" native_gates: {summary_ring['hardware_requirements']['native_gates']}")
Y+YY with linear entanglement: connectivity: linear native_gates: ['CNOT', 'H', 'RY', 'S', 'Sdg'] X+ZZ with circular entanglement: connectivity: ring native_gates: ['CNOT', 'H', 'RX', 'RZ']
10. EncodingProperties & Lazy Evaluation¶
The properties attribute returns a frozen EncodingProperties dataclass. It is lazily computed on first access and then cached (thread-safe via double-checked locking).
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
props = enc.properties
print(f"Type: {type(props)}")
print(f"\n--- EncodingProperties Fields ---")
print(f"n_qubits: {props.n_qubits}")
print(f"depth: {props.depth}")
print(f"gate_count: {props.gate_count}")
print(f"single_qubit_gates: {props.single_qubit_gates}")
print(f"two_qubit_gates: {props.two_qubit_gates}")
print(f"parameter_count: {props.parameter_count}") # Always 0 (data-dependent only)
print(f"is_entangling: {props.is_entangling}")
print(f"simulability: {props.simulability}")
print(f"trainability_estimate: {props.trainability_estimate}")
print(f"notes: {props.notes}")
Type: <class 'encoding_atlas.core.properties.EncodingProperties'> --- EncodingProperties Fields --- n_qubits: 4 depth: 10 gate_count: 52 single_qubit_gates: 28 two_qubit_gates: 24 parameter_count: 0 is_entangling: True simulability: not_simulable trainability_estimate: 0.4 notes: Pauli terms: ['Z', 'ZZ'], Entanglement: full, Pairs: 6, Reps: 2
# Properties are immutable (frozen dataclass)
try:
props.depth = 999
except Exception as e:
print(f"Cannot modify frozen properties: {type(e).__name__}: {e}")
# Convert to dictionary for serialization
props_dict = props.to_dict()
print(f"\nAs dictionary keys: {list(props_dict.keys())}")
Cannot modify frozen properties: FrozenInstanceError: cannot assign to field 'depth' As dictionary keys: ['n_qubits', 'depth', 'gate_count', 'single_qubit_gates', 'two_qubit_gates', 'parameter_count', 'is_entangling', 'simulability', 'expressibility', 'entanglement_capability', 'trainability_estimate', 'noise_resilience_estimate', 'notes']
# Lazy evaluation: properties are computed only on first access
enc_new = PauliFeatureMap(n_features=8, reps=3, paulis=["Z", "ZZ"])
# At this point, _properties is None (not yet computed)
# Accessing .properties triggers computation and caching
p = enc_new.properties
print(f"Properties computed: gate_count={p.gate_count}, depth={p.depth}")
# Second access returns cached value (no recomputation)
p2 = enc_new.properties
print(f"Same object? {p is p2}") # True - cached
Properties computed: gate_count=300, depth=15 Same object? True
11. Capability Protocols (isinstance Checks)¶
The library uses Python's structural subtyping (PEP 544 Protocols) to declare optional capabilities. PauliFeatureMap implements ResourceAnalyzable and EntanglementQueryable.
enc = PauliFeatureMap(n_features=4)
# PauliFeatureMap implements ResourceAnalyzable
print(f"ResourceAnalyzable? {isinstance(enc, ResourceAnalyzable)}")
# PauliFeatureMap implements EntanglementQueryable
print(f"EntanglementQueryable? {isinstance(enc, EntanglementQueryable)}")
# PauliFeatureMap does NOT implement DataTransformable (no preprocessing needed)
print(f"DataTransformable? {isinstance(enc, DataTransformable)}")
# It IS a BaseEncoding
print(f"BaseEncoding? {isinstance(enc, BaseEncoding)}")
ResourceAnalyzable? True EntanglementQueryable? True DataTransformable? False BaseEncoding? True
# Type guards are also available
from encoding_atlas.core.protocols import (
is_resource_analyzable,
is_entanglement_queryable,
is_data_transformable,
)
print(f"is_resource_analyzable(enc): {is_resource_analyzable(enc)}")
print(f"is_entanglement_queryable(enc): {is_entanglement_queryable(enc)}")
print(f"is_data_transformable(enc): {is_data_transformable(enc)}")
is_resource_analyzable(enc): True is_entanglement_queryable(enc): True is_data_transformable(enc): False
# Writing generic code using protocols
def analyze_encoding(enc: BaseEncoding) -> dict:
"""Analyze any encoding using its declared capabilities."""
result = {"name": enc.__class__.__name__, "n_qubits": enc.n_qubits}
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
result["total_gates"] = summary["gate_counts"]["total"]
result["is_entangling"] = summary["is_entangling"]
if isinstance(enc, EntanglementQueryable):
result["n_pairs"] = len(enc.get_entanglement_pairs())
return result
# Works with PauliFeatureMap
info = analyze_encoding(PauliFeatureMap(n_features=4, paulis=["Z", "ZZ"]))
print(f"PauliFeatureMap: {info}")
# Works with any encoding
info_angle = analyze_encoding(AngleEncoding(n_features=4))
print(f"AngleEncoding: {info_angle}")
PauliFeatureMap: {'name': 'PauliFeatureMap', 'n_qubits': 4, 'total_gates': 52, 'is_entangling': True, 'n_pairs': 6}
AngleEncoding: {'name': 'AngleEncoding', 'n_qubits': 4, 'total_gates': 4, 'is_entangling': False}
from encoding_atlas.analysis import (
count_resources,
get_resource_summary,
get_gate_breakdown,
compare_resources,
estimate_execution_time,
)
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"])
# count_resources: quick summary
res = count_resources(enc)
print("count_resources():")
for k, v in res.items():
print(f" {k}: {v}")
count_resources(): n_qubits: 4 depth: 10 gate_count: 52 single_qubit_gates: 28 two_qubit_gates: 24 parameter_count: 0 cnot_count: 24 cz_count: 0 t_gate_count: 0 hadamard_count: 8 rotation_gates: 8 two_qubit_ratio: 0.46153846153846156 gates_per_qubit: 13.0 encoding_name: PauliFeatureMap is_data_dependent: False
# Detailed breakdown
detailed = count_resources(enc, detailed=True)
print("count_resources(detailed=True):")
for k, v in detailed.items():
print(f" {k}: {v}")
count_resources(detailed=True): rx: 0 ry: 0 rz: 8 h: 8 x: 0 y: 0 z: 0 s: 0 t: 0 cnot: 24 cx: 24 cz: 0 swap: 0 total_single_qubit: 28 total_two_qubit: 24 total: 52 encoding_name: PauliFeatureMap
# Compare resources across multiple encodings
encodings = [
PauliFeatureMap(n_features=4, paulis=["Z"], reps=2),
PauliFeatureMap(n_features=4, paulis=["Z", "ZZ"], reps=2),
PauliFeatureMap(n_features=4, paulis=["X", "Y", "Z", "ZZ"], reps=2),
IQPEncoding(n_features=4, reps=2),
]
# compare_resources returns dict[str, list] (column-oriented)
comparison = compare_resources(encodings)
print("Resource Comparison:")
names = comparison["encoding_name"]
for i, name in enumerate(names):
gates = comparison['gate_count'][i]
depth = comparison['depth'][i]
twoq = comparison['two_qubit_gates'][i]
print(f" {name:<45s} gates={gates:>4d} depth={depth:>3d} 2q={twoq:>3d}")
Resource Comparison: PauliFeatureMap gates= 16 depth= 4 2q= 0 PauliFeatureMap gates= 52 depth= 10 2q= 24 PauliFeatureMap gates= 68 depth= 14 2q= 24 IQPEncoding gates= 52 depth= 6 2q= 24
# Estimate execution time on hardware
time_est = estimate_execution_time(enc)
print("Estimated execution time:")
for k, v in time_est.items():
print(f" {k}: {v:.4f}")
Estimated execution time: serial_time_us: 6.3600 estimated_time_us: 3.6800 single_qubit_time_us: 0.5600 two_qubit_time_us: 4.8000 measurement_time_us: 1.0000 parallelization_factor: 0.5000
12.2 Simulability Analysis¶
Determines whether the encoding can be efficiently simulated classically.
from encoding_atlas.analysis import (
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
)
# Entangling PauliFeatureMap: NOT simulable (non-Clifford + entanglement)
enc_ent = PauliFeatureMap(n_features=4, paulis=["Z", "ZZ"], entanglement="full")
sim_result = check_simulability(enc_ent, detailed=True)
print("Entangling PauliFeatureMap (Z+ZZ):")
print(f" simulability_class: {sim_result['simulability_class']}")
print(f" is_simulable: {sim_result['is_simulable']}")
print(f" reason: {sim_result['reason']}")
if 'recommendations' in sim_result:
print(f" recommendations: {sim_result['recommendations']}")
Entangling PauliFeatureMap (Z+ZZ): simulability_class: not_simulable is_simulable: False reason: High entanglement circuit with 24 two-qubit gates and non-Clifford operations recommendations: ['Statevector simulation feasible (4 qubits, ~256 bytes memory)', 'Brute-force statevector simulation is feasible at this circuit size (4 qubits, ~256 bytes memory)', 'Use statevector simulation for instances with < 20 qubits', 'Consider tensor network methods for structured entanglement', 'May require quantum hardware for large instances']
# Non-entangling PauliFeatureMap: simulable
enc_sim = PauliFeatureMap(n_features=4, paulis=["Z"])
sim_simple = check_simulability(enc_sim, detailed=True)
print("Non-entangling PauliFeatureMap (Z only):")
print(f" simulability_class: {sim_simple['simulability_class']}")
print(f" is_simulable: {sim_simple['is_simulable']}")
print(f" reason: {sim_simple['reason']}")
Non-entangling PauliFeatureMap (Z only): simulability_class: simulable is_simulable: True reason: Encoding produces only product states (no entanglement)
# Quick simulability explanation
print(f"Entangling: {get_simulability_reason(enc_ent)}")
print(f"Non-entangling: {get_simulability_reason(enc_sim)}")
# Clifford and matchgate checks
print(f"\nis_clifford_circuit(Z+ZZ): {is_clifford_circuit(enc_ent)}")
print(f"is_matchgate_circuit(Z+ZZ): {is_matchgate_circuit(enc_ent)}")
Entangling: Not simulable: High entanglement circuit with 24 two-qubit gates and non-Clifford operations Non-entangling: Simulable: Encoding produces only product states (no entanglement) is_clifford_circuit(Z+ZZ): False is_matchgate_circuit(Z+ZZ): False
12.3 Expressibility¶
Measures how well the encoding explores the Hilbert space. Higher values (closer to 1.0) indicate the encoding produces states closer to the Haar-random distribution.
from encoding_atlas.analysis import compute_expressibility
# Compare expressibility of different configurations
configs = [
("Z only", {"paulis": ["Z"]}),
("Z+ZZ (full)", {"paulis": ["Z", "ZZ"], "entanglement": "full"}),
("Z+ZZ (linear)", {"paulis": ["Z", "ZZ"], "entanglement": "linear"}),
("X+Y+Z+ZZ", {"paulis": ["X", "Y", "Z", "ZZ"]}),
]
print(f"{'Config':<18s} {'Expressibility':>15s}")
print("-" * 35)
for label, kwargs in configs:
enc = PauliFeatureMap(n_features=3, reps=2, **kwargs)
expr = compute_expressibility(enc, n_samples=1000, seed=42)
print(f"{label:<18s} {expr:>15.6f}")
Config Expressibility ----------------------------------- Z only 0.937796 Z+ZZ (full) 0.994536 Z+ZZ (linear) 0.989470 X+Y+Z+ZZ 0.922957
12.4 Entanglement Capability¶
Measures how much entanglement the encoding generates using the Meyer-Wallach measure. Value ranges from 0 (product states) to 1 (maximally entangled).
from encoding_atlas.analysis import compute_entanglement_capability
# Non-entangling encoding should have ~0 entanglement
enc_no_ent = PauliFeatureMap(n_features=3, paulis=["Z"], reps=2)
ent_none = compute_entanglement_capability(enc_no_ent, n_samples=500, seed=42)
print(f"Z only (non-entangling): entanglement = {ent_none:.6f}")
# Entangling encodings produce non-zero entanglement
enc_ent = PauliFeatureMap(n_features=3, paulis=["Z", "ZZ"], reps=2, entanglement="full")
ent_full = compute_entanglement_capability(enc_ent, n_samples=500, seed=42)
print(f"Z+ZZ (full): entanglement = {ent_full:.6f}")
enc_lin = PauliFeatureMap(n_features=3, paulis=["Z", "ZZ"], reps=2, entanglement="linear")
ent_lin = compute_entanglement_capability(enc_lin, n_samples=500, seed=42)
print(f"Z+ZZ (linear): entanglement = {ent_lin:.6f}")
Z only (non-entangling): entanglement = 0.000000 Z+ZZ (full): entanglement = 0.583768 Z+ZZ (linear): entanglement = 0.433392
12.5 Trainability & Barren Plateau Detection¶
Estimates how easy it is to optimize parameters through the encoding (relevant for variational quantum algorithms).
from encoding_atlas.analysis import estimate_trainability, detect_barren_plateau
# Shallow circuit: good trainability
enc_shallow = PauliFeatureMap(n_features=3, reps=1, paulis=["Z", "ZZ"])
train_shallow = estimate_trainability(enc_shallow, n_samples=200, seed=42)
print(f"Shallow (reps=1): trainability = {train_shallow:.4f}")
# Deep circuit: potentially worse trainability
enc_deep = PauliFeatureMap(n_features=3, reps=5, paulis=["Z", "ZZ"])
train_deep = estimate_trainability(enc_deep, n_samples=200, seed=42)
print(f"Deep (reps=5): trainability = {train_deep:.4f}")
Shallow (reps=1): trainability = 0.0000 Deep (reps=5): trainability = 0.1525
# Barren plateau detection via detailed trainability analysis
details_shallow = estimate_trainability(enc_shallow, n_samples=200, seed=42, return_details=True)
details_deep = estimate_trainability(enc_deep, n_samples=200, seed=42, return_details=True)
print(f"Shallow (reps=1): barren_plateau_risk = {details_shallow['barren_plateau_risk']}")
print(f" gradient_variance = {details_shallow['gradient_variance']:.2e}")
print(f"Deep (reps=5): barren_plateau_risk = {details_deep['barren_plateau_risk']}")
print(f" gradient_variance = {details_deep['gradient_variance']:.2e}")
# detect_barren_plateau is also available as a low-level utility:
# it takes (gradient_variance, n_qubits, n_params) -> 'low' | 'medium' | 'high'
bp_risk = detect_barren_plateau(
gradient_variance=details_shallow['gradient_variance'],
n_qubits=enc_shallow.n_qubits,
n_params=max(1, enc_shallow.properties.parameter_count or enc_shallow.n_qubits),
)
print(f"Low-level detect_barren_plateau: {bp_risk}")
Shallow (reps=1): barren_plateau_risk = high gradient_variance = 6.81e-34 Deep (reps=5): barren_plateau_risk = low gradient_variance = 1.39e-02 Low-level detect_barren_plateau: high
13. Statevector Simulation & Quantum State Inspection¶
The analysis utilities let you simulate the encoding to get the output quantum state.
from encoding_atlas.analysis import (
simulate_encoding_statevector,
simulate_encoding_statevectors_batch,
compute_fidelity,
compute_purity,
compute_linear_entropy,
compute_von_neumann_entropy,
partial_trace_single_qubit,
partial_trace_subsystem,
)
enc = PauliFeatureMap(n_features=3, reps=2, paulis=["Z", "ZZ"], entanglement="full")
x = np.array([0.5, 1.0, 1.5])
# Simulate to get statevector
state = simulate_encoding_statevector(enc, x, backend="pennylane")
print(f"Statevector shape: {state.shape}")
print(f"Statevector (rounded):")
for i, amp in enumerate(state):
if abs(amp) > 1e-6:
print(f" |{i:03b}> : {amp:.6f}")
print(f"\nNorm: {np.linalg.norm(state):.10f}")
Statevector shape: (8,) Statevector (rounded): |000> : 0.083348-0.443904j |001> : 0.331873+0.000769j |010> : -0.311629+0.023697j |011> : -0.021322-0.160874j |100> : 0.236785+0.232683j |101> : 0.057408-0.156670j |110> : -0.331873-0.492934j |111> : -0.265504+0.013690j Norm: 1.0000000000
# Batch simulation
X_batch = np.array([
[0.0, 0.0, 0.0],
[np.pi, np.pi, np.pi],
[0.5, 1.0, 1.5],
])
states = simulate_encoding_statevectors_batch(enc, X_batch, backend="pennylane")
print(f"Batch of {len(states)} statevectors:")
for i, s in enumerate(states):
print(f" State {i}: norm={np.linalg.norm(s):.6f}, "
f"max_amplitude={np.max(np.abs(s)):.4f}")
Batch of 3 statevectors: State 0: norm=1.000000, max_amplitude=0.7398 State 1: norm=1.000000, max_amplitude=1.0000 State 2: norm=1.000000, max_amplitude=0.5942
# Fidelity between two encoded states
x1 = np.array([0.5, 1.0, 1.5])
x2 = np.array([0.5, 1.0, 1.6]) # Slightly different
x3 = np.array([3.0, 0.1, 2.5]) # Very different
s1 = simulate_encoding_statevector(enc, x1)
s2 = simulate_encoding_statevector(enc, x2)
s3 = simulate_encoding_statevector(enc, x3)
print(f"Fidelity(x1, x1): {compute_fidelity(s1, s1):.10f}") # Self-fidelity = 1
print(f"Fidelity(x1, x2): {compute_fidelity(s1, s2):.10f}") # Close inputs -> high
print(f"Fidelity(x1, x3): {compute_fidelity(s1, s3):.10f}") # Distant inputs -> low
Fidelity(x1, x1): 1.0000000000 Fidelity(x1, x2): 0.7420358012 Fidelity(x1, x3): 0.0427982066
# Reduced density matrix analysis: examine entanglement at qubit level
state = simulate_encoding_statevector(enc, x1)
n_qubits = enc.n_qubits
print("Per-qubit reduced density matrix analysis:")
for qubit in range(n_qubits):
rho = partial_trace_single_qubit(state, n_qubits, qubit)
purity = compute_purity(rho)
entropy = compute_linear_entropy(rho)
print(f" Qubit {qubit}: purity={purity:.4f}, linear_entropy={entropy:.4f}")
# Purity < 1 indicates the qubit is entangled with others
print("\n(Purity < 1 means the qubit is entangled with others)")
Per-qubit reduced density matrix analysis: Qubit 0: purity=0.5824, linear_entropy=0.4176 Qubit 1: purity=0.7798, linear_entropy=0.2202 Qubit 2: purity=0.6565, linear_entropy=0.3435 (Purity < 1 means the qubit is entangled with others)
# Subsystem analysis: trace out a subset of qubits
rho_01 = partial_trace_subsystem(state, n_qubits, keep_qubits=[0, 1])
print(f"Subsystem [0,1] density matrix shape: {rho_01.shape}")
print(f"Subsystem [0,1] purity: {compute_purity(rho_01):.6f}")
print(f"Subsystem [0,1] von Neumann entropy: {compute_von_neumann_entropy(rho_01):.6f}")
Subsystem [0,1] density matrix shape: (4, 4) Subsystem [0,1] purity: 0.656469 Subsystem [0,1] von Neumann entropy: 0.760707
14. Equality, Hashing & Serialization¶
PauliFeatureMap supports equality comparison, hashing, and pickle serialization.
# Equality: based on class, n_features, and config (reps, paulis, entanglement)
enc_a = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
enc_b = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
enc_c = PauliFeatureMap(n_features=4, reps=3, paulis=["Z", "ZZ"], entanglement="full")
print(f"Same config: enc_a == enc_b? {enc_a == enc_b}") # True
print(f"Different reps: enc_a == enc_c? {enc_a == enc_c}") # False
print(f"Different type: enc_a == 42? {enc_a == 42}") # False
# Different encoding class with same params
enc_iqp = IQPEncoding(n_features=4, reps=2)
print(f"Different class: enc_a == IQP? {enc_a == enc_iqp}") # False
Same config: enc_a == enc_b? True Different reps: enc_a == enc_c? False Different type: enc_a == 42? False Different class: enc_a == IQP? False
# Hashing: allows use in sets and as dict keys
enc_set = {enc_a, enc_b, enc_c}
print(f"Set of 3 encodings (2 equal): {len(enc_set)} unique")
# As dictionary keys
results = {
enc_a: "result_a",
enc_c: "result_c",
}
print(f"Lookup enc_b (same as enc_a): {results[enc_b]}")
Set of 3 encodings (2 equal): 2 unique Lookup enc_b (same as enc_a): result_a
# Pickle serialization: full round-trip support
enc_orig = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
# Access properties before pickling to test cached value preservation
_ = enc_orig.properties
# Serialize and deserialize
data = pickle.dumps(enc_orig)
enc_loaded = pickle.loads(data)
print(f"Original: {enc_orig}")
print(f"Deserialized: {enc_loaded}")
print(f"Equal? {enc_orig == enc_loaded}")
print(f"Properties preserved? {enc_loaded.properties.gate_count == enc_orig.properties.gate_count}")
print(f"Pickle size: {len(data)} bytes")
Original: PauliFeatureMap(n_features=4, reps=2, paulis=['Z', 'ZZ'], entanglement='full') Deserialized: PauliFeatureMap(n_features=4, reps=2, paulis=['Z', 'ZZ'], entanglement='full') Equal? True Properties preserved? True Pickle size: 711 bytes
15. Configuration Access & Read-Only Safety¶
The config property returns a read-only copy of the configuration dictionary.
enc = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
# Access configuration
config = enc.config
print(f"Config: {config}")
# Config is a COPY - modifying it doesn't affect the encoding
config['reps'] = 999
config['paulis'].append('XX')
print(f"Modified copy: {config}")
print(f"Original still intact: {enc.config}")
print(f"enc.reps still: {enc.reps}")
Config: {'reps': 2, 'paulis': ['Z', 'ZZ'], 'entanglement': 'full'}
Modified copy: {'reps': 999, 'paulis': ['Z', 'ZZ', 'XX'], 'entanglement': 'full'}
Original still intact: {'reps': 2, 'paulis': ['Z', 'ZZ', 'XX'], 'entanglement': 'full'}
enc.reps still: 2
16. Input Validation & Edge Cases¶
PauliFeatureMap includes comprehensive input validation at both construction time and circuit generation time.
# === CONSTRUCTION-TIME VALIDATION ===
# Invalid n_features
for bad_n in [0, -1, 1.5]:
try:
PauliFeatureMap(n_features=bad_n)
except (ValueError, TypeError) as e:
print(f"n_features={bad_n!r}: {type(e).__name__}: {e}")
print()
n_features=0: ValueError: n_features must be a positive integer, got 0 n_features=-1: ValueError: n_features must be a positive integer, got -1 n_features=1.5: ValueError: n_features must be a positive integer, got 1.5
# Invalid reps
for bad_reps in [0, -1, True, False, 1.5]:
try:
PauliFeatureMap(n_features=4, reps=bad_reps)
except (ValueError, TypeError) as e:
print(f"reps={bad_reps!r}: {type(e).__name__}: {e}")
print()
# Note: bool is explicitly rejected even though True==1 in Python
reps=0: ValueError: reps must be a positive integer, got 0 reps=-1: ValueError: reps must be a positive integer, got -1 reps=True: ValueError: reps must be a positive integer, got bool (boolean values are not accepted) reps=False: ValueError: reps must be a positive integer, got bool (boolean values are not accepted) reps=1.5: ValueError: reps must be a positive integer, got 1.5
# Invalid paulis
invalid_paulis_cases = [
("not a list", "ZZ"), # String instead of list
("invalid term", ["ZZZ"]), # Three-character Pauli
("unknown Pauli", ["A"]), # Not a valid Pauli
("empty list", []), # Empty
]
for label, bad_paulis in invalid_paulis_cases:
try:
PauliFeatureMap(n_features=4, paulis=bad_paulis)
except (ValueError, TypeError) as e:
print(f"{label}: {type(e).__name__}: {e}")
not a list: TypeError: paulis must be a list of strings or None, got str invalid term: ValueError: Invalid Pauli term 'ZZZ'. Valid terms are: ['X', 'XX', 'XY', 'XZ', 'Y', 'YX', 'YY', 'YZ', 'Z', 'ZX', 'ZY', 'ZZ'] unknown Pauli: ValueError: Invalid Pauli term 'A'. Valid terms are: ['X', 'XX', 'XY', 'XZ', 'Y', 'YX', 'YY', 'YZ', 'Z', 'ZX', 'ZY', 'ZZ'] empty list: ValueError: paulis list cannot be empty
# Invalid entanglement
try:
PauliFeatureMap(n_features=4, entanglement="star")
except ValueError as e:
print(f"Invalid entanglement: {e}")
Invalid entanglement: entanglement must be one of ['circular', 'full', 'linear'], got 'star'
# === CIRCUIT GENERATION VALIDATION ===
enc = PauliFeatureMap(n_features=4)
# Wrong number of features
try:
enc.get_circuit(np.array([1.0, 2.0, 3.0])) # 3 instead of 4
except ValueError as e:
print(f"Wrong shape: {e}")
# NaN values
try:
enc.get_circuit(np.array([1.0, float('nan'), 3.0, 4.0]))
except ValueError as e:
print(f"NaN: {e}")
# Infinite values
try:
enc.get_circuit(np.array([1.0, float('inf'), 3.0, 4.0]))
except ValueError as e:
print(f"Inf: {e}")
# Complex numbers
try:
enc.get_circuit(np.array([1+2j, 3+0j, 0+1j, 2+0j]))
except TypeError as e:
print(f"Complex: {e}")
# String inputs
try:
enc.get_circuit(["0.5", "1.0", "1.5", "2.0"])
except TypeError as e:
print(f"String: {e}")
# Invalid backend
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="tensorflow")
except ValueError as e:
print(f"Bad backend: {e}")
Wrong shape: Expected 4 features, got 3 NaN: Input contains NaN or infinite values Inf: Input contains NaN or infinite values Complex: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only. String: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding. Bad backend: Unknown backend 'tensorflow'. Supported backends: 'pennylane', 'qiskit', 'cirq'
# === VALID EDGE CASES ===
enc = PauliFeatureMap(n_features=4)
# List input (auto-converted to numpy array)
circuit = enc.get_circuit([0.1, 0.2, 0.3, 0.4])
print(f"List input: OK (type: {type(circuit)})")
# Tuple input
circuit = enc.get_circuit((0.1, 0.2, 0.3, 0.4))
print(f"Tuple input: OK")
# Integer input (auto-cast to float64)
circuit = enc.get_circuit(np.array([1, 2, 3, 4]))
print(f"Integer input: OK (auto-cast to float64)")
# Zero input
circuit = enc.get_circuit(np.zeros(4))
print(f"All-zeros input: OK")
# Large values (valid but may get debug log)
circuit = enc.get_circuit(np.array([100.0, 200.0, 300.0, 400.0]))
print(f"Large values: OK (accepted, debug log if > 4pi)")
# Negative values
circuit = enc.get_circuit(np.array([-1.0, -2.0, -3.0, -4.0]))
print(f"Negative values: OK")
List input: OK (type: <class 'function'>) Tuple input: OK Integer input: OK (auto-cast to float64) All-zeros input: OK Large values: OK (accepted, debug log if > 4pi) Negative values: OK
# Thread safety: input validation creates defensive copies
x_original = np.array([0.1, 0.2, 0.3, 0.4])
circuit = enc.get_circuit(x_original)
# Modifying the original after circuit generation is safe
x_original[0] = 999.0
# The circuit still uses the original values (defensive copy was made)
dev = qml.device("default.qubit", wires=4)
@qml.qnode(dev)
def verify():
circuit()
return qml.state()
state = verify()
print(f"Circuit unaffected by post-generation input mutation: norm={np.linalg.norm(state):.6f}")
Circuit unaffected by post-generation input mutation: norm=1.000000
17. Logging & Warnings¶
PauliFeatureMap uses Python's logging module for debugging and warnings for user-facing alerts.
# Enable debug logging for PauliFeatureMap
logger = logging.getLogger('encoding_atlas.encodings.pauli_feature_map')
logger.setLevel(logging.DEBUG)
# Add a handler to see the output
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
# Now creation and circuit generation will produce debug output
enc_debug = PauliFeatureMap(n_features=3, reps=1, paulis=["Z", "ZZ"])
_ = enc_debug.get_circuit(np.array([0.5, 1.0, 1.5]), backend="pennylane")
# Clean up
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
encoding_atlas.encodings.pauli_feature_map - DEBUG - PauliFeatureMap initialized: n_features=3, reps=1, paulis=['Z', 'ZZ'], entanglement='full', n_pairs=3 encoding_atlas.encodings.pauli_feature_map - DEBUG - Generating circuit: backend='pennylane', input_shape=(3,) encoding_atlas.encodings.pauli_feature_map - DEBUG - Circuit generated successfully for backend='pennylane'
# UserWarning for full entanglement with many features (>12)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_large = PauliFeatureMap(
n_features=15, paulis=["Z", "ZZ"], entanglement="full"
)
if w:
print(f"Warning issued: {w[0].category.__name__}")
print(f"Message: {w[0].message}")
else:
print("No warning (expected for n_features <= 12)")
Large feature count with full entanglement: 15 features, 1 two-qubit Paulis, 420 CNOT gates total
Warning issued: UserWarning Message: Full entanglement with 15 features and 1 two-qubit Pauli term(s) creates 105 interaction pairs per layer (420 total CNOT gates for 2 reps). This may exceed practical limits for NISQ devices. Consider using entanglement='linear' or 'circular' for better hardware compatibility.
# No warning if no two-qubit Paulis (even with many features)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_no_warn = PauliFeatureMap(
n_features=20, paulis=["Z"], entanglement="full"
)
print(f"20 features, Z only: warnings issued = {len(w)}")
# No warning with linear/circular entanglement
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_linear_large = PauliFeatureMap(
n_features=20, paulis=["Z", "ZZ"], entanglement="linear"
)
print(f"20 features, linear: warnings issued = {len(w)}")
20 features, Z only: warnings issued = 0 20 features, linear: warnings issued = 0
18. Comparison with Related Encodings¶
PauliFeatureMap generalizes several other encodings in the library.
# PauliFeatureMap with default ["Z", "ZZ"] is equivalent to ZZFeatureMap
pfm = PauliFeatureMap(n_features=4, reps=2, paulis=["Z", "ZZ"], entanglement="full")
zzfm = ZZFeatureMap(n_features=4, reps=2, entanglement="full")
# Compare properties
print("PauliFeatureMap [Z, ZZ] vs ZZFeatureMap:")
print(f" Gate count: {pfm.properties.gate_count} vs {zzfm.properties.gate_count}")
print(f" Depth: {pfm.depth} vs {zzfm.depth}")
print(f" Is entangling: {pfm.properties.is_entangling} vs {zzfm.properties.is_entangling}")
# Same quantum state output
x = np.array([0.5, 1.0, 1.5, 2.0])
state_pfm = simulate_encoding_statevector(pfm, x)
state_zzfm = simulate_encoding_statevector(zzfm, x)
fidelity = compute_fidelity(state_pfm, state_zzfm)
print(f" Fidelity: {fidelity:.10f}")
PauliFeatureMap [Z, ZZ] vs ZZFeatureMap: Gate count: 52 vs 52 Depth: 10 vs 22 Is entangling: True vs True Fidelity: 1.0000000000
# Side-by-side comparison with other encoding types
encodings = [
("PauliFeatureMap Z", PauliFeatureMap(n_features=4, paulis=["Z"], reps=2)),
("PauliFeatureMap Z+ZZ", PauliFeatureMap(n_features=4, paulis=["Z", "ZZ"], reps=2)),
("PauliFeatureMap All", PauliFeatureMap(n_features=4, paulis=["X","Y","Z","ZZ"], reps=2)),
("ZZFeatureMap", ZZFeatureMap(n_features=4, reps=2)),
("IQPEncoding", IQPEncoding(n_features=4, reps=2)),
("AngleEncoding", AngleEncoding(n_features=4)),
]
print(f"{'Name':<25s} {'Qubits':>6s} {'Depth':>6s} {'Gates':>6s} {'2Q':>4s} "
f"{'Entangling':>11s} {'Simulability':>15s}")
print("-" * 80)
for name, enc in encodings:
p = enc.properties
print(f"{name:<25s} {p.n_qubits:>6d} {p.depth:>6d} {p.gate_count:>6d} "
f"{p.two_qubit_gates:>4d} {str(p.is_entangling):>11s} {p.simulability:>15s}")
Name Qubits Depth Gates 2Q Entangling Simulability -------------------------------------------------------------------------------- PauliFeatureMap Z 4 4 16 0 False simulable PauliFeatureMap Z+ZZ 4 10 52 24 True not_simulable PauliFeatureMap All 4 14 68 24 True not_simulable ZZFeatureMap 4 22 52 24 True not_simulable IQPEncoding 4 6 52 24 True not_simulable AngleEncoding 4 1 4 0 False simulable
19. Registry System¶
The library includes a registry system to discover and instantiate encodings by name.
from encoding_atlas import list_encodings, get_encoding
# List all registered encodings
available = list_encodings()
print(f"Registered encodings ({len(available)}):")
for name in available:
print(f" - {name}")
Registered encodings (26): - amplitude - angle - angle_ry - basis - covariant - covariant_feature_map - cyclic_equivariant - cyclic_equivariant_feature_map - data_reuploading - hamiltonian - hamiltonian_encoding - hardware_efficient - higher_order_angle - iqp - pauli_feature_map - qaoa - qaoa_encoding - so2_equivariant - so2_equivariant_feature_map - swap_equivariant - swap_equivariant_feature_map - symmetry_inspired - symmetry_inspired_feature_map - trainable - trainable_encoding - zz_feature_map
# Instantiate by name (if PauliFeatureMap is registered)
if 'pauli_feature_map' in available:
enc_reg = get_encoding('pauli_feature_map', n_features=4, reps=2)
print(f"From registry: {enc_reg}")
else:
print("PauliFeatureMap registry name not found. Available names:")
print(f" {available}")
print("\nDirect instantiation always works:")
enc_reg = PauliFeatureMap(n_features=4, reps=2)
print(f" {enc_reg}")
From registry: PauliFeatureMap(n_features=4, reps=2, paulis=['Z', 'ZZ'], entanglement='full')
20. Hardware Planning Guide¶
PauliFeatureMap provides all the information needed for hardware-aware circuit design.
def hardware_report(enc: PauliFeatureMap) -> None:
"""Generate a hardware planning report for a PauliFeatureMap."""
summary = enc.resource_summary()
bd = enc.gate_count_breakdown()
hw = summary['hardware_requirements']
print(f"=== Hardware Planning Report ===")
print(f"Encoding: {enc}")
print(f"")
print(f"Qubit Requirements:")
print(f" Qubits needed: {summary['n_qubits']}")
print(f" Connectivity: {hw['connectivity']}")
print(f"")
print(f"Gate Requirements:")
print(f" Native gates: {', '.join(hw['native_gates'])}")
print(f" Total gates: {bd['total']}")
print(f" Two-qubit gates: {bd['cnot']} CNOTs")
print(f" Circuit depth: {summary['depth']}")
print(f"")
print(f"Fidelity Requirements:")
print(f" Min 2Q fidelity: {hw['min_two_qubit_gate_fidelity']}")
two_q_ratio = bd['total_two_qubit'] / bd['total'] if bd['total'] > 0 else 0
print(f" 2Q gate ratio: {two_q_ratio:.1%}")
print(f"")
print(f"Recommendations:")
if hw['connectivity'] == 'all-to-all':
print(f" - Requires all-to-all connectivity (e.g., trapped ion)")
print(f" - For superconducting devices, consider 'linear' or 'circular'")
elif hw['connectivity'] == 'linear':
print(f" - Compatible with nearest-neighbor architectures")
print(f" - Minimal SWAP overhead on linear qubit layouts")
else:
print(f" - Requires ring connectivity")
print(f" - Good for circular/ring qubit layouts")
# Report for full entanglement
hardware_report(PauliFeatureMap(n_features=6, reps=2, paulis=["Z","ZZ"], entanglement="full"))
print()
# Report for linear entanglement
hardware_report(PauliFeatureMap(n_features=6, reps=2, paulis=["Z","ZZ"], entanglement="linear"))
=== Hardware Planning Report === Encoding: PauliFeatureMap(n_features=6, reps=2, paulis=['Z', 'ZZ'], entanglement='full') Qubit Requirements: Qubits needed: 6 Connectivity: all-to-all Gate Requirements: Native gates: CNOT, H, RZ Total gates: 114 Two-qubit gates: 60 CNOTs Circuit depth: 10 Fidelity Requirements: Min 2Q fidelity: 0.99 2Q gate ratio: 52.6% Recommendations: - Requires all-to-all connectivity (e.g., trapped ion) - For superconducting devices, consider 'linear' or 'circular' === Hardware Planning Report === Encoding: PauliFeatureMap(n_features=6, reps=2, paulis=['Z', 'ZZ'], entanglement='linear') Qubit Requirements: Qubits needed: 6 Connectivity: linear Gate Requirements: Native gates: CNOT, H, RZ Total gates: 54 Two-qubit gates: 20 CNOTs Circuit depth: 10 Fidelity Requirements: Min 2Q fidelity: 0.99 2Q gate ratio: 37.0% Recommendations: - Compatible with nearest-neighbor architectures - Minimal SWAP overhead on linear qubit layouts
# Scaling analysis: how resource requirements grow with n_features
print(f"{'n_features':>10s} {'full_pairs':>11s} {'full_cnots':>11s} "
f"{'linear_pairs':>13s} {'linear_cnots':>13s}")
print("-" * 65)
for n in [2, 4, 6, 8, 10, 12, 16, 20]:
enc_full = PauliFeatureMap(n_features=n, reps=2, paulis=["Z", "ZZ"], entanglement="full")
enc_lin = PauliFeatureMap(n_features=n, reps=2, paulis=["Z", "ZZ"], entanglement="linear")
with warnings.catch_warnings():
warnings.simplefilter("ignore") # Suppress large-feature warnings
bd_full = enc_full.gate_count_breakdown()
bd_lin = enc_lin.gate_count_breakdown()
print(f"{n:>10d} {len(enc_full.get_entanglement_pairs()):>11d} "
f"{bd_full['cnot']:>11d} "
f"{len(enc_lin.get_entanglement_pairs()):>13d} "
f"{bd_lin['cnot']:>13d}")
print("\nFull entanglement scales as O(n^2), linear as O(n)")
C:\Users\ashut\AppData\Local\Temp\ipykernel_54068\2645321524.py:7: UserWarning: Full entanglement with 16 features and 1 two-qubit Pauli term(s) creates 120 interaction pairs per layer (480 total CNOT gates for 2 reps). This may exceed practical limits for NISQ devices. Consider using entanglement='linear' or 'circular' for better hardware compatibility. enc_full = PauliFeatureMap(n_features=n, reps=2, paulis=["Z", "ZZ"], entanglement="full") Large feature count with full entanglement: 16 features, 1 two-qubit Paulis, 480 CNOT gates total C:\Users\ashut\AppData\Local\Temp\ipykernel_54068\2645321524.py:7: UserWarning: Full entanglement with 20 features and 1 two-qubit Pauli term(s) creates 190 interaction pairs per layer (760 total CNOT gates for 2 reps). This may exceed practical limits for NISQ devices. Consider using entanglement='linear' or 'circular' for better hardware compatibility. enc_full = PauliFeatureMap(n_features=n, reps=2, paulis=["Z", "ZZ"], entanglement="full") Large feature count with full entanglement: 20 features, 1 two-qubit Paulis, 760 CNOT gates total
n_features full_pairs full_cnots linear_pairs linear_cnots
-----------------------------------------------------------------
2 1 4 1 4
4 6 24 3 12
6 15 60 5 20
8 28 112 7 28
10 45 180 9 36
12 66 264 11 44
16 120 480 15 60
20 190 760 19 76
Full entanglement scales as O(n^2), linear as O(n)
21. Summary¶
What We Covered¶
| Feature | Section |
|---|---|
| Construction (defaults + custom) | 3 |
| All Pauli terms (X, Y, Z, XX, YY, ZZ, XY, XZ, YZ, YX, ZX, ZY) | 4.1 |
| Auto-uppercase & deduplication | 4.1 |
| Repetitions and depth scaling | 4.2 |
| Entanglement topologies (full, linear, circular) | 4.3 |
| Properties (n_qubits, depth, n_features) | 5 |
| Entanglement pair inspection | 6 |
| PennyLane circuit generation | 7.1 |
| Qiskit circuit generation | 7.2 |
| Cirq circuit generation | 7.3 |
| Cross-backend equivalence verification | 7.3 |
| Batch & parallel circuit generation | 8 |
| Gate count breakdown | 9 |
| Resource summary & hardware requirements | 9 |
| EncodingProperties & lazy evaluation | 10 |
| Capability protocols (isinstance checks) | 11 |
| Resource counting (analysis module) | 12.1 |
| Simulability analysis | 12.2 |
| Expressibility measurement | 12.3 |
| Entanglement capability | 12.4 |
| Trainability & barren plateaus | 12.5 |
| Statevector simulation & quantum state inspection | 13 |
| Fidelity, purity, entropy analysis | 13 |
| Partial trace & subsystem analysis | 13 |
| Equality, hashing & sets/dict keys | 14 |
| Pickle serialization | 14 |
| Read-only config safety | 15 |
| Input validation (shape, NaN, inf, complex, string) | 16 |
| Thread-safe defensive copies | 16 |
| Logging (debug, info, warning) | 17 |
| UserWarning for large full entanglement | 17 |
| Comparison with ZZFeatureMap, IQP, Angle | 18 |
| Registry system | 19 |
| Hardware planning & scaling analysis | 20 |
Key Takeaways¶
- PauliFeatureMap is the most configurable encoding in the library, allowing arbitrary combinations of Pauli rotation terms.
- With default settings
["Z", "ZZ"], it behaves identically toZZFeatureMap. - Three entanglement topologies trade off between expressivity (
full) and hardware compatibility (linear,circular). - The library provides comprehensive analysis tools for expressibility, entanglement, trainability, simulability, and resources.
- All backends (PennyLane, Qiskit, Cirq) produce equivalent quantum states.
- Thread-safe design enables parallel batch processing.
- Extensive input validation catches errors early with clear messages.