IQPEncoding: Complete Feature Demonstration¶
Library: encoding-atlas Version: 0.2.0 Author: Ashutosh Mishra
This notebook provides an exhaustive, hands-on demonstration of IQPEncoding from the Quantum Encoding Atlas library. IQPEncoding implements Instantaneous Quantum Polynomial (IQP) circuits for quantum machine learning, creating highly entangled quantum states with provable classical hardness guarantees.
What This Notebook Covers¶
| # | Section | Description |
|---|---|---|
| 1 | Installation & Setup | Installing the library and verifying the environment |
| 2 | Creating an IQPEncoding | Constructor parameters, defaults, and validation |
| 3 | Core Properties | n_qubits, depth, n_features, config, repr |
| 4 | Encoding Properties (Lazy) | Thread-safe EncodingProperties dataclass |
| 5 | Entanglement Pairs | Full, linear, and circular topologies |
| 6 | Gate Count Breakdown | Detailed gate counts by type |
| 7 | Resource Summary | Comprehensive resource analysis |
| 8 | Circuit Generation -- PennyLane | Generating and executing PennyLane circuits |
| 9 | Circuit Generation -- Qiskit | Generating and visualizing Qiskit circuits |
| 10 | Circuit Generation -- Cirq | Generating and inspecting Cirq circuits |
| 11 | Entanglement Topology Comparison | Full vs linear vs circular |
| 12 | Repetitions (reps) |
Effect of multiple layers |
| 13 | Batch Circuit Generation | get_circuits() -- sequential and parallel |
| 14 | Input Validation & Edge Cases | Robust error handling demonstration |
| 15 | Resource Analysis | count_resources(), compare_resources(), execution time |
| 16 | Simulability Analysis | Classical simulability checks |
| 17 | Expressibility Analysis | Hilbert space coverage measurement |
| 18 | Entanglement Capability | Meyer-Wallach, Scott measure, topology comparison |
| 19 | Trainability Analysis | Barren plateau detection and gradient variance |
| 20 | Low-Level Utilities | Statevector simulation, fidelity, purity, entropy |
| 21 | Capability Protocols | ResourceAnalyzable, EntanglementQueryable, etc. |
| 22 | Registry System | Creating encodings by name via get_encoding() |
| 23 | Equality, Hashing & Serialization | __eq__, __hash__, pickle round-trip |
| 24 | Thread Safety | Concurrent circuit generation and property access |
| 25 | Logging & Debugging | Enabling debug logs for troubleshooting |
| 26 | Full Entanglement Warning | Large feature count warning |
| 27 | Encoding Recommendation Guide | Using the decision guide |
| 28 | Visualization & Comparison | Comparing IQPEncoding to other encodings |
| 29 | Complete Workflow | End-to-end: Iris dataset, quantum kernel, classification |
Mathematical Background¶
IQP encoding creates quantum states of the form:
$$|\psi(\mathbf{x})\rangle = U_{\text{IQP}}(\mathbf{x})|0\rangle^{\otimes n}$$
where $U_{\text{IQP}}$ consists of alternating layers (repeated reps times) of:
- Hadamard layer: $H^{\otimes n}$ creates uniform superposition
- Single-qubit phase: $R_Z(2x_i)$ applies data-dependent phases to each qubit $i$
- Two-qubit interactions: $ZZ(x_i x_j) = \text{CNOT}_{ij} \cdot (I \otimes R_Z(2 x_i x_j)) \cdot \text{CNOT}_{ij}$
The ZZ interactions encode pairwise feature products, enabling the quantum classifier to learn nonlinear decision boundaries. Because IQP circuits include entangling gates, they are:
- NOT classically simulable (provably hard under standard assumptions)
- Entangling (creates multi-qubit correlations)
- Expressive (can access a large portion of the Hilbert space)
- Subject to barren plateaus at high depth (trainability decreases with reps)
1. Installation & Setup¶
# Install the library (uncomment if not already installed)
# !pip install encoding-atlas
# For full multi-backend support, also install:
# pip install encoding-atlas[qiskit] # Qiskit backend
# pip install encoding-atlas[cirq] # Cirq backend
# pip install encoding-atlas[viz] # Matplotlib visualizations
# Or install everything at once:
# !pip install encoding-atlas[qiskit,cirq,viz]
import numpy as np
import encoding_atlas
print(f"encoding-atlas version: {encoding_atlas.__version__}")
print(f"NumPy version: {np.__version__}")
encoding-atlas version: 0.2.0 NumPy version: 2.2.6
# Check which backends are available
backends_available = {}
try:
import pennylane as qml
backends_available['pennylane'] = qml.__version__
except ImportError:
backends_available['pennylane'] = 'NOT INSTALLED'
try:
import qiskit
backends_available['qiskit'] = qiskit.__version__
except ImportError:
backends_available['qiskit'] = 'NOT INSTALLED'
try:
import cirq
backends_available['cirq'] = cirq.__version__
except ImportError:
backends_available['cirq'] = 'NOT INSTALLED'
print("Backend availability:")
for backend, version in backends_available.items():
status = "Available" if version != 'NOT INSTALLED' else "Not installed"
print(f" {backend:12s}: {status} ({version})")
Backend availability: pennylane : Available (0.42.3) qiskit : Available (2.3.0) cirq : Available (1.5.0)
2. Creating an IQPEncoding¶
The IQPEncoding constructor accepts three parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
n_features |
int |
required | Number of classical features to encode (also determines qubit count) |
reps |
int |
2 |
Number of times to repeat the encoding layer |
entanglement |
"full", "linear", "circular" |
"full" |
Topology of ZZ interactions between qubits |
from encoding_atlas import IQPEncoding
# Basic creation with defaults (reps=2, full entanglement)
enc_default = IQPEncoding(n_features=4)
print(f"Default: {enc_default}")
# Specify entanglement topology
enc_full = IQPEncoding(n_features=4, entanglement="full")
enc_linear = IQPEncoding(n_features=4, entanglement="linear")
enc_circular = IQPEncoding(n_features=4, entanglement="circular")
print(f"Full: {enc_full}")
print(f"Linear: {enc_linear}")
print(f"Circular: {enc_circular}")
# Specify repetitions
enc_reps = IQPEncoding(n_features=3, reps=3, entanglement="full")
print(f"3 reps: {enc_reps}")
# Single feature (minimum)
enc_single = IQPEncoding(n_features=1)
print(f"Single: {enc_single}")
# numpy integer for reps is accepted
enc_np = IQPEncoding(n_features=4, reps=np.int64(3))
print(f"np.int64: {enc_np}")
Default: IQPEncoding(n_features=4, reps=2, entanglement='full') Full: IQPEncoding(n_features=4, reps=2, entanglement='full') Linear: IQPEncoding(n_features=4, reps=2, entanglement='linear') Circular: IQPEncoding(n_features=4, reps=2, entanglement='circular') 3 reps: IQPEncoding(n_features=3, reps=3, entanglement='full') Single: IQPEncoding(n_features=1, reps=2, entanglement='full') np.int64: IQPEncoding(n_features=4, reps=3, entanglement='full')
2.1 Constructor Validation¶
The constructor validates all parameters strictly. Let's verify each validation rule.
# --- Invalid n_features ---
print("=== n_features validation ===")
# n_features must be a positive integer
for bad_n in [0, -1, -5]:
try:
IQPEncoding(n_features=bad_n)
except ValueError as e:
print(f" n_features={bad_n!r}: ValueError - {e}")
# Non-integer types are rejected
for bad_n in [2.5, "4", None]:
try:
IQPEncoding(n_features=bad_n)
except (ValueError, TypeError) as e:
print(f" n_features={bad_n!r}: {type(e).__name__} - {e}")
=== n_features validation === 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=-5: ValueError - n_features must be a positive integer, got -5 n_features=2.5: ValueError - n_features must be a positive integer, got 2.5 n_features='4': ValueError - n_features must be a positive integer, got 4 n_features=None: ValueError - n_features must be a positive integer, got None
# --- Invalid reps ---
print("=== reps validation ===")
for bad_reps in [0, -1, 1.5, True, False]:
try:
IQPEncoding(n_features=4, reps=bad_reps)
except ValueError as e:
print(f" reps={bad_reps!r}: ValueError - {e}")
# Note: True and False are rejected even though bool is a subclass of int
print("\n (bool values are explicitly rejected to prevent accidental misuse)")
=== reps validation === reps=0: ValueError - reps must be at least 1 (got 0). Each repetition adds one layer of H, RZ, and ZZ gates. reps=-1: ValueError - reps must be at least 1 (got -1). Each repetition adds one layer of H, RZ, and ZZ gates. reps=1.5: ValueError - reps must be at least 1 (got 1.5). Each repetition adds one layer of H, RZ, and ZZ gates. reps=True: ValueError - reps must be at least 1 (got True). Each repetition adds one layer of H, RZ, and ZZ gates. reps=False: ValueError - reps must be at least 1 (got False). Each repetition adds one layer of H, RZ, and ZZ gates. (bool values are explicitly rejected to prevent accidental misuse)
# --- Invalid entanglement ---
print("=== entanglement validation ===")
for bad_ent in ["Full", "LINEAR", "ring", "all", 1, None]:
try:
IQPEncoding(n_features=4, entanglement=bad_ent)
except (ValueError, TypeError) as e:
print(f" entanglement={bad_ent!r}: {type(e).__name__} - {e}")
=== entanglement validation === entanglement='Full': ValueError - entanglement must be one of ['circular', 'full', 'linear'], got 'Full' entanglement='LINEAR': ValueError - entanglement must be one of ['circular', 'full', 'linear'], got 'LINEAR' entanglement='ring': ValueError - entanglement must be one of ['circular', 'full', 'linear'], got 'ring' entanglement='all': ValueError - entanglement must be one of ['circular', 'full', 'linear'], got 'all' entanglement=1: ValueError - entanglement must be one of ['circular', 'full', 'linear'], got 1 entanglement=None: ValueError - entanglement must be one of ['circular', 'full', 'linear'], got None
3. Core Properties¶
IQPEncoding exposes several properties inherited from BaseEncoding plus its own attributes.
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
print("=== Core Properties ===")
print(f" n_features : {enc.n_features} (number of classical features)")
print(f" n_qubits : {enc.n_qubits} (one qubit per feature)")
print(f" depth : {enc.depth} (3 * reps = 3 * {enc.reps})")
print(f" reps : {enc.reps} (number of encoding layer repetitions)")
print(f" entanglement : {enc.entanglement!r} (topology of ZZ interactions)")
# n_qubits always equals n_features for IQPEncoding
assert enc.n_qubits == enc.n_features, "n_qubits should equal n_features"
# depth always equals 3 * reps
assert enc.depth == 3 * enc.reps, "depth should equal 3 * reps"
print("\n Assertions passed: n_qubits == n_features, depth == 3 * reps")
=== Core Properties === n_features : 4 (number of classical features) n_qubits : 4 (one qubit per feature) depth : 6 (3 * reps = 3 * 2) reps : 2 (number of encoding layer repetitions) entanglement : 'full' (topology of ZZ interactions) Assertions passed: n_qubits == n_features, depth == 3 * reps
# The config property returns a copy of the encoding-specific parameters
config = enc.config
print(f"config = {config}")
print(f"type = {type(config).__name__}")
# It's a defensive copy -- modifying it doesn't affect the encoding
config['reps'] = 999
config['entanglement'] = 'MODIFIED'
print(f"\nAfter modifying the returned dict:")
print(f" enc.config = {enc.config} (unchanged)")
print(f" enc.reps = {enc.reps} (unchanged)")
print(f" enc.entanglement = {enc.entanglement!r} (unchanged)")
# repr
print(f"\nrepr(enc) = {repr(enc)}")
config = {'reps': 2, 'entanglement': 'full'}
type = dict
After modifying the returned dict:
enc.config = {'reps': 2, 'entanglement': 'full'} (unchanged)
enc.reps = 2 (unchanged)
enc.entanglement = 'full' (unchanged)
repr(enc) = IQPEncoding(n_features=4, reps=2, entanglement='full')
4. Encoding Properties (Lazy, Thread-Safe)¶
The properties attribute returns an EncodingProperties frozen dataclass. It is:
- Lazily computed on first access (not at construction time)
- Thread-safe via double-checked locking
- Cached after first computation
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
props = enc.properties
print(f"type: {type(props).__name__}")
print(f"")
print(f"=== EncodingProperties ===")
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}")
print(f" is_entangling : {props.is_entangling}")
print(f" simulability : {props.simulability!r}")
print(f" trainability_estimate: {props.trainability_estimate}")
print(f" notes : {props.notes!r}")
type: EncodingProperties === EncodingProperties === n_qubits : 4 depth : 6 gate_count : 52 single_qubit_gates : 28 two_qubit_gates : 24 parameter_count : 20 is_entangling : True simulability : 'not_simulable' trainability_estimate: 0.7 notes : 'IQP encoding with full entanglement. Provably hard to simulate classically under standard assumptions.'
# The properties object is frozen (immutable)
from dataclasses import FrozenInstanceError
try:
props.n_qubits = 10
except FrozenInstanceError as e:
print(f"Cannot modify frozen properties: {e}")
# It also has a to_dict() method for easy serialization
props_dict = props.to_dict()
print(f"\nproperties.to_dict() keys: {list(props_dict.keys())}")
Cannot modify frozen properties: cannot assign to field 'n_qubits' properties.to_dict() 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']
# Verify properties are cached (same object returned on second access)
props_1 = enc.properties
props_2 = enc.properties
print(f"Same object (cached): {props_1 is props_2}")
# Verify key invariants
assert props.single_qubit_gates + props.two_qubit_gates == props.gate_count
assert props.two_qubit_gates > 0 # IQPEncoding always has two-qubit gates
assert props.is_entangling == True
assert props.simulability == "not_simulable"
print("All property invariants verified.")
Same object (cached): True All property invariants verified.
5. Entanglement Pairs¶
IQPEncoding uses ZZ interactions between qubit pairs. The topology determines which pairs interact:
| Topology | Pairs | Count | Connectivity |
|---|---|---|---|
| full | All (i, j) with i < j | n(n-1)/2 | All-to-all |
| linear | (i, i+1) only | n-1 | Nearest-neighbor |
| circular | Linear + (n-1, 0) for n>2 | n (for n>2) | Ring |
# Full entanglement: all pairs (i,j) with i < j
enc_full = IQPEncoding(n_features=4, entanglement="full")
pairs_full = enc_full.get_entanglement_pairs()
print(f"=== Full entanglement (n=4) ===")
print(f" Pairs: {pairs_full}")
print(f" Count: {len(pairs_full)} (n*(n-1)/2 = 4*3/2 = 6)")
# Linear entanglement: only (i, i+1)
enc_linear = IQPEncoding(n_features=4, entanglement="linear")
pairs_linear = enc_linear.get_entanglement_pairs()
print(f"\n=== Linear entanglement (n=4) ===")
print(f" Pairs: {pairs_linear}")
print(f" Count: {len(pairs_linear)} (n-1 = 3)")
# Circular entanglement: linear + (n-1, 0) for n>2
enc_circular = IQPEncoding(n_features=4, entanglement="circular")
pairs_circular = enc_circular.get_entanglement_pairs()
print(f"\n=== Circular entanglement (n=4) ===")
print(f" Pairs: {pairs_circular}")
print(f" Count: {len(pairs_circular)} (n = 4)")
=== Full entanglement (n=4) === Pairs: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] Count: 6 (n*(n-1)/2 = 4*3/2 = 6) === Linear entanglement (n=4) === Pairs: [(0, 1), (1, 2), (2, 3)] Count: 3 (n-1 = 3) === Circular entanglement (n=4) === Pairs: [(0, 1), (1, 2), (2, 3), (3, 0)] Count: 4 (n = 4)
# Edge cases: n=1, n=2, n=3
print("=== Entanglement pairs edge cases ===")
for n in [1, 2, 3]:
for topo in ["full", "linear", "circular"]:
enc_edge = IQPEncoding(n_features=n, entanglement=topo)
pairs = enc_edge.get_entanglement_pairs()
print(f" n={n}, {topo:8s}: {pairs} (count={len(pairs)})")
print()
# Key observations:
print("Key observations:")
print(" - n=1: No pairs for any topology (nothing to entangle)")
print(" - n=2: Full, linear, and circular all produce [(0,1)]")
print(" - n=2 circular: Same as linear (no wrap-around needed for n<=2)")
print(" - n=3 circular: Adds (2,0) wrap-around pair")
=== Entanglement pairs edge cases === n=1, full : [] (count=0) n=1, linear : [] (count=0) n=1, circular: [] (count=0) n=2, full : [(0, 1)] (count=1) n=2, linear : [(0, 1)] (count=1) n=2, circular: [(0, 1)] (count=1) n=3, full : [(0, 1), (0, 2), (1, 2)] (count=3) n=3, linear : [(0, 1), (1, 2)] (count=2) n=3, circular: [(0, 1), (1, 2), (2, 0)] (count=3) Key observations: - n=1: No pairs for any topology (nothing to entangle) - n=2: Full, linear, and circular all produce [(0,1)] - n=2 circular: Same as linear (no wrap-around needed for n<=2) - n=3 circular: Adds (2,0) wrap-around pair
# How pair count scales with n_features
print("=== Pair count scaling ===")
print(f"{'n':>3s} {'full':>8s} {'linear':>8s} {'circular':>10s}")
print("-" * 32)
for n in [1, 2, 3, 4, 5, 8, 10]:
full_count = len(IQPEncoding(n_features=n, entanglement="full").get_entanglement_pairs())
linear_count = len(IQPEncoding(n_features=n, entanglement="linear").get_entanglement_pairs())
circular_count = len(IQPEncoding(n_features=n, entanglement="circular").get_entanglement_pairs())
print(f"{n:3d} {full_count:8d} {linear_count:8d} {circular_count:10d}")
print("\n Full scales as O(n^2), while linear and circular scale as O(n)")
=== Pair count scaling === n full linear circular -------------------------------- 1 0 0 0 2 1 1 1 3 3 2 3 4 6 3 4 5 10 4 5 8 28 7 8 10 45 9 10 Full scales as O(n^2), while linear and circular scale as O(n)
6. Gate Count Breakdown¶
gate_count_breakdown() returns a GateCountBreakdown TypedDict with detailed gate counts:
| Key | Description |
|---|---|
hadamard |
H gates (n_qubits x reps) |
rz_single |
Single-qubit RZ gates for data encoding |
rz_zz |
RZ gates in ZZ interaction decomposition |
cnot |
CNOT gates (2 x n_pairs x reps) |
total_single_qubit |
hadamard + rz_single + rz_zz |
total_two_qubit |
cnot count |
total |
total_single_qubit + total_two_qubit |
enc = IQPEncoding(n_features=4, reps=1, entanglement="full")
breakdown = enc.gate_count_breakdown()
print("=== gate_count_breakdown() (n=4, reps=1, full) ===")
for key, value in breakdown.items():
print(f" {key:20s}: {value}")
# Verify consistency
assert breakdown['total_single_qubit'] == breakdown['hadamard'] + breakdown['rz_single'] + breakdown['rz_zz']
assert breakdown['total_two_qubit'] == breakdown['cnot']
assert breakdown['total'] == breakdown['total_single_qubit'] + breakdown['total_two_qubit']
print("\nConsistency verified: all totals add up correctly.")
=== gate_count_breakdown() (n=4, reps=1, full) === hadamard : 4 rz_single : 4 rz_zz : 6 cnot : 12 total_single_qubit : 14 total_two_qubit : 12 total : 26 Consistency verified: all totals add up correctly.
# Compare gate counts across topologies
print("=== Gate counts by topology (n=4, reps=2) ===")
print(f"{'topology':>10s} {'H':>4s} {'rz_s':>5s} {'rz_zz':>6s} {'cnot':>5s} {'1q':>5s} {'2q':>5s} {'total':>6s}")
print("-" * 50)
for topo in ["full", "linear", "circular"]:
enc_t = IQPEncoding(n_features=4, reps=2, entanglement=topo)
b = enc_t.gate_count_breakdown()
print(f"{topo:>10s} {b['hadamard']:4d} {b['rz_single']:5d} {b['rz_zz']:6d} {b['cnot']:5d} {b['total_single_qubit']:5d} {b['total_two_qubit']:5d} {b['total']:6d}")
=== Gate counts by topology (n=4, reps=2) ===
topology H rz_s rz_zz cnot 1q 2q total
--------------------------------------------------
full 8 8 12 24 28 24 52
linear 8 8 6 12 22 12 34
circular 8 8 8 16 24 16 40
# Compare gate counts across reps
print("=== Gate counts by reps (n=4, full entanglement) ===")
print(f"{'reps':>4s} {'H':>4s} {'rz_s':>5s} {'rz_zz':>6s} {'cnot':>5s} {'1q':>5s} {'2q':>5s} {'total':>6s}")
print("-" * 45)
for reps in [1, 2, 3, 5]:
enc_r = IQPEncoding(n_features=4, reps=reps, entanglement="full")
b = enc_r.gate_count_breakdown()
print(f"{reps:4d} {b['hadamard']:4d} {b['rz_single']:5d} {b['rz_zz']:6d} {b['cnot']:5d} {b['total_single_qubit']:5d} {b['total_two_qubit']:5d} {b['total']:6d}")
print("\n All gate counts scale linearly with reps.")
=== Gate counts by reps (n=4, full entanglement) === reps H rz_s rz_zz cnot 1q 2q total --------------------------------------------- 1 4 4 6 12 14 12 26 2 8 8 12 24 28 24 52 3 12 12 18 36 42 36 78 5 20 20 30 60 70 60 130 All gate counts scale linearly with reps.
7. Resource Summary¶
resource_summary() provides a comprehensive overview of the encoding's resource requirements.
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
summary = enc.resource_summary()
print("=== resource_summary() ===")
for key, value in summary.items():
if isinstance(value, dict):
print(f" {key}:")
for k2, v2 in value.items():
print(f" {k2}: {v2}")
elif isinstance(value, list) and len(value) > 6:
print(f" {key}: [{value[0]}, {value[1]}, ... ] ({len(value)} pairs)")
else:
print(f" {key}: {value}")
=== resource_summary() ===
n_qubits: 4
n_features: 4
depth: 6
reps: 2
entanglement: full
entanglement_pairs: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
n_entanglement_pairs: 6
gate_counts:
hadamard: 8
rz_single: 8
rz_zz: 12
cnot: 24
total_single_qubit: 28
total_two_qubit: 24
total: 52
is_entangling: True
simulability: not_simulable
trainability_estimate: 0.7
hardware_requirements:
connectivity: all-to-all
native_gates: ['H', 'RZ', 'CNOT']
min_two_qubit_gate_fidelity: 0.99
# Compare hardware requirements across topologies
print("=== Hardware requirements by topology ===")
for topo in ["full", "linear", "circular"]:
enc_t = IQPEncoding(n_features=4, reps=2, entanglement=topo)
hw = enc_t.resource_summary()['hardware_requirements']
print(f"\n {topo}:")
for k, v in hw.items():
print(f" {k}: {v}")
=== Hardware requirements by topology ===
full:
connectivity: all-to-all
native_gates: ['H', 'RZ', 'CNOT']
min_two_qubit_gate_fidelity: 0.99
linear:
connectivity: linear
native_gates: ['H', 'RZ', 'CNOT']
min_two_qubit_gate_fidelity: 0.99
circular:
connectivity: ring
native_gates: ['H', 'RZ', 'CNOT']
min_two_qubit_gate_fidelity: 0.99
8. Circuit Generation -- PennyLane Backend¶
PennyLane is the default backend. get_circuit() returns a callable (closure) that applies quantum gates when invoked inside a qml.QNode context.
import pennylane as qml
enc = IQPEncoding(n_features=4, reps=1, entanglement="full")
x = np.array([0.5, 1.0, 1.5, 2.0])
# get_circuit returns a callable for PennyLane
circuit_fn = enc.get_circuit(x, backend="pennylane")
print(f"Type: {type(circuit_fn).__name__}")
print(f"Callable: {callable(circuit_fn)}")
Type: function Callable: True
# Use it inside a QNode to get a statevector
dev = qml.device("default.qubit", wires=enc.n_qubits)
@qml.qnode(dev)
def run_encoding(x_input):
circuit_fn = enc.get_circuit(x_input, backend="pennylane")
circuit_fn() # Apply the gates
return qml.state()
state = run_encoding(x)
print(f"Statevector shape: {state.shape} (2^{enc.n_qubits} = {2**enc.n_qubits} amplitudes)")
print(f"Statevector (first 8 amplitudes):")
for i, amp in enumerate(state[:8]):
binary = format(i, f'0{enc.n_qubits}b')
print(f" |{binary}> : {amp:.6f}")
# Verify normalization
print(f"\nNorm: {np.abs(state @ state.conj()):.10f} (should be 1.0)")
Statevector shape: (16,) (2^4 = 16 amplitudes) Statevector (first 8 amplitudes): |0000> : 0.094392-0.231496j |0001> : -0.157043+0.194518j |0010> : 0.242228-0.061851j |0011> : -0.205140-0.142890j |0100> : -0.205140+0.142890j |0101> : -0.111522-0.223747j |0110> : -0.205140-0.142890j |0111> : 0.242228-0.061851j Norm: 1.0000000000 (should be 1.0)
# Visualize the circuit using PennyLane's drawer
@qml.qnode(dev)
def draw_circuit(x_input):
circuit_fn = enc.get_circuit(x_input, backend="pennylane")
circuit_fn()
return qml.state()
print("Circuit diagram (IQP, 4 qubits, 1 rep, full entanglement):")
print(qml.draw(draw_circuit)(x))
Circuit diagram (IQP, 4 qubits, 1 rep, full entanglement): 0: ──H──RZ(1.00)─╭●───────────╭●─╭●───────────╭●─╭●───────────╭●──────────────────────────────── ··· 1: ──H──RZ(2.00)─╰X──RZ(1.00)─╰X─│────────────│──│────────────│──╭●───────────╭●─╭●───────────╭● ··· 2: ──H──RZ(3.00)─────────────────╰X──RZ(1.50)─╰X─│────────────│──╰X──RZ(3.00)─╰X─│────────────│─ ··· 3: ──H──RZ(4.00)─────────────────────────────────╰X──RZ(2.00)─╰X─────────────────╰X──RZ(4.00)─╰X ··· 0: ··· ─────────────────┤ State 1: ··· ─────────────────┤ State 2: ··· ─╭●───────────╭●─┤ State 3: ··· ─╰X──RZ(6.00)─╰X─┤ State
# With multiple reps, the full IQP layer is repeated
enc_2reps = IQPEncoding(n_features=4, reps=2, entanglement="full")
dev2 = qml.device("default.qubit", wires=4)
@qml.qnode(dev2)
def draw_2reps(x_input):
circuit_fn = enc_2reps.get_circuit(x_input, backend="pennylane")
circuit_fn()
return qml.state()
print("Circuit diagram (IQP, 4 qubits, 2 reps, full entanglement):")
print(qml.draw(draw_2reps)(x))
Circuit diagram (IQP, 4 qubits, 2 reps, full entanglement): 0: ──H──RZ(1.00)─╭●───────────╭●─╭●───────────╭●─╭●───────────╭●──H──RZ(1.00)─────────────────── ··· 1: ──H──RZ(2.00)─╰X──RZ(1.00)─╰X─│────────────│──│────────────│──╭●───────────╭●─╭●───────────╭● ··· 2: ──H──RZ(3.00)─────────────────╰X──RZ(1.50)─╰X─│────────────│──╰X──RZ(3.00)─╰X─│────────────│─ ··· 3: ──H──RZ(4.00)─────────────────────────────────╰X──RZ(2.00)─╰X─────────────────╰X──RZ(4.00)─╰X ··· 0: ··· ──────────────╭●───────────╭●────────╭●───────────╭●─╭●───────────╭●─────────────────── ··· 1: ··· ──H──RZ(2.00)─╰X──RZ(1.00)─╰X────────│────────────│──│────────────│──╭●───────────╭●─╭● ··· 2: ··· ─╭●───────────╭●──H─────────RZ(3.00)─╰X──RZ(1.50)─╰X─│────────────│──╰X──RZ(3.00)─╰X─│─ ··· 3: ··· ─╰X──RZ(6.00)─╰X──H─────────RZ(4.00)─────────────────╰X──RZ(2.00)─╰X─────────────────╰X ··· 0: ··· ──────────────────────────────┤ State 1: ··· ───────────╭●─────────────────┤ State 2: ··· ───────────│──╭●───────────╭●─┤ State 3: ··· ──RZ(4.00)─╰X─╰X──RZ(6.00)─╰X─┤ State
9. Circuit Generation -- Qiskit Backend¶
The Qiskit backend returns a QuantumCircuit object that can be visualized, transpiled, or executed on real hardware.
enc = IQPEncoding(n_features=4, reps=1, entanglement="full")
x = np.array([0.5, 1.0, 1.5, 2.0])
qc = enc.get_circuit(x, backend="qiskit")
print(f"Type: {type(qc).__name__}")
print(f"Num qubits: {qc.num_qubits}")
print(f"Depth: {qc.depth()}")
print(f"Circuit name: {qc.name}")
print()
print(qc.draw(output='text'))
Type: QuantumCircuit
Num qubits: 4
Depth: 17
Circuit name: IQPEncoding
┌───┐┌───────┐ »
q_0: ┤ H ├┤ Rz(1) ├──■─────────────■────■───────────────■────■───────────»
├───┤├───────┤┌─┴─┐┌───────┐┌─┴─┐ │ │ │ »
q_1: ┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(1) ├┤ X ├──┼───────────────┼────┼──────■────»
├───┤├───────┤└───┘└───────┘└───┘┌─┴─┐┌─────────┐┌─┴─┐ │ ┌─┴─┐ »
q_2: ┤ H ├┤ Rz(3) ├───────────────────┤ X ├┤ Rz(1.5) ├┤ X ├──┼────┤ X ├──»
├───┤├───────┤ └───┘└─────────┘└───┘┌─┴─┐┌─┴───┴─┐»
q_3: ┤ H ├┤ Rz(4) ├────────────────────────────────────────┤ X ├┤ Rz(2) ├»
└───┘└───────┘ └───┘└───────┘»
«
«q_0: ───────────■─────────────────────────────────────────────
« │
«q_1: ───────────┼────■────■─────────────■─────────────────────
« ┌───────┐ │ ┌─┴─┐ │ │
«q_2: ┤ Rz(3) ├──┼──┤ X ├──┼─────────────┼────■─────────────■──
« └───────┘┌─┴─┐└───┘┌─┴─┐┌───────┐┌─┴─┐┌─┴─┐┌───────┐┌─┴─┐
«q_3: ─────────┤ X ├─────┤ X ├┤ Rz(4) ├┤ X ├┤ X ├┤ Rz(6) ├┤ X ├
« └───┘ └───┘└───────┘└───┘└───┘└───────┘└───┘
# Qiskit circuit with 2 reps
enc_2reps = IQPEncoding(n_features=4, reps=2, entanglement="full")
qc_2reps = enc_2reps.get_circuit(x, backend="qiskit")
print(f"Depth with 2 reps: {qc_2reps.depth()}")
print()
print(qc_2reps.draw(output='text'))
Depth with 2 reps: 31
┌───┐┌───────┐ »
q_0: ┤ H ├┤ Rz(1) ├──■─────────────■────■───────────────■────■───────────»
├───┤├───────┤┌─┴─┐┌───────┐┌─┴─┐ │ │ │ »
q_1: ┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(1) ├┤ X ├──┼───────────────┼────┼──────■────»
├───┤├───────┤└───┘└───────┘└───┘┌─┴─┐┌─────────┐┌─┴─┐ │ ┌─┴─┐ »
q_2: ┤ H ├┤ Rz(3) ├───────────────────┤ X ├┤ Rz(1.5) ├┤ X ├──┼────┤ X ├──»
├───┤├───────┤ └───┘└─────────┘└───┘┌─┴─┐┌─┴───┴─┐»
q_3: ┤ H ├┤ Rz(4) ├────────────────────────────────────────┤ X ├┤ Rz(2) ├»
└───┘└───────┘ └───┘└───────┘»
« ┌───┐┌───────┐ »
«q_0: ───────────■──┤ H ├┤ Rz(1) ├──────────────────────────────■───────────»
« │ └───┘└───────┘ ┌───┐┌───────┐┌─┴─┐┌───────┐»
«q_1: ───────────┼────■──────■───────────────■──┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(1) ├»
« ┌───────┐ │ ┌─┴─┐ │ │ └───┘└───────┘└───┘└─┬───┬─┘»
«q_2: ┤ Rz(3) ├──┼──┤ X ├────┼───────────────┼────■─────────────■────┤ H ├──»
« └───────┘┌─┴─┐└───┘ ┌─┴─┐ ┌───────┐┌─┴─┐┌─┴─┐┌───────┐┌─┴─┐ ├───┤ »
«q_3: ─────────┤ X ├───────┤ X ├──┤ Rz(4) ├┤ X ├┤ X ├┤ Rz(6) ├┤ X ├──┤ H ├──»
« └───┘ └───┘ └───────┘└───┘└───┘└───────┘└───┘ └───┘ »
« »
«q_0: ────■──────■───────────────■────■──────────────────────■────────────»
« ┌─┴─┐ │ │ │ │ »
«q_1: ──┤ X ├────┼───────────────┼────┼──────■───────────────┼────■────■──»
« ┌─┴───┴─┐┌─┴─┐┌─────────┐┌─┴─┐ │ ┌─┴─┐ ┌───────┐ │ ┌─┴─┐ │ »
«q_2: ┤ Rz(3) ├┤ X ├┤ Rz(1.5) ├┤ X ├──┼────┤ X ├──┤ Rz(3) ├──┼──┤ X ├──┼──»
« ├───────┤└───┘└─────────┘└───┘┌─┴─┐┌─┴───┴─┐└───────┘┌─┴─┐└───┘┌─┴─┐»
«q_3: ┤ Rz(4) ├─────────────────────┤ X ├┤ Rz(2) ├─────────┤ X ├─────┤ X ├»
« └───────┘ └───┘└───────┘ └───┘ └───┘»
«
«q_0: ─────────────────────────────────
«
«q_1: ───────────■─────────────────────
« │
«q_2: ───────────┼────■─────────────■──
« ┌───────┐┌─┴─┐┌─┴─┐┌───────┐┌─┴─┐
«q_3: ┤ Rz(4) ├┤ X ├┤ X ├┤ Rz(6) ├┤ X ├
« └───────┘└───┘└───┘└───────┘└───┘
# Different entanglement topologies in Qiskit
for topo in ["full", "linear", "circular"]:
enc_topo = IQPEncoding(n_features=4, reps=1, entanglement=topo)
qc_topo = enc_topo.get_circuit(np.array([0.5, 1.0, 1.5, 2.0]), backend="qiskit")
print(f"\n--- Entanglement: {topo} ---")
print(qc_topo.draw(output='text'))
--- Entanglement: full ---
┌───┐┌───────┐ »
q_0: ┤ H ├┤ Rz(1) ├──■─────────────■────■───────────────■────■───────────»
├───┤├───────┤┌─┴─┐┌───────┐┌─┴─┐ │ │ │ »
q_1: ┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(1) ├┤ X ├──┼───────────────┼────┼──────■────»
├───┤├───────┤└───┘└───────┘└───┘┌─┴─┐┌─────────┐┌─┴─┐ │ ┌─┴─┐ »
q_2: ┤ H ├┤ Rz(3) ├───────────────────┤ X ├┤ Rz(1.5) ├┤ X ├──┼────┤ X ├──»
├───┤├───────┤ └───┘└─────────┘└───┘┌─┴─┐┌─┴───┴─┐»
q_3: ┤ H ├┤ Rz(4) ├────────────────────────────────────────┤ X ├┤ Rz(2) ├»
└───┘└───────┘ └───┘└───────┘»
«
«q_0: ───────────■─────────────────────────────────────────────
« │
«q_1: ───────────┼────■────■─────────────■─────────────────────
« ┌───────┐ │ ┌─┴─┐ │ │
«q_2: ┤ Rz(3) ├──┼──┤ X ├──┼─────────────┼────■─────────────■──
« └───────┘┌─┴─┐└───┘┌─┴─┐┌───────┐┌─┴─┐┌─┴─┐┌───────┐┌─┴─┐
«q_3: ─────────┤ X ├─────┤ X ├┤ Rz(4) ├┤ X ├┤ X ├┤ Rz(6) ├┤ X ├
« └───┘ └───┘└───────┘└───┘└───┘└───────┘└───┘
--- Entanglement: linear ---
┌───┐┌───────┐
q_0: ┤ H ├┤ Rz(1) ├──■─────────────■────────────────────────────────────────
├───┤├───────┤┌─┴─┐┌───────┐┌─┴─┐
q_1: ┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(1) ├┤ X ├──■─────────────■─────────────────────
├───┤├───────┤└───┘└───────┘└───┘┌─┴─┐┌───────┐┌─┴─┐
q_2: ┤ H ├┤ Rz(3) ├───────────────────┤ X ├┤ Rz(3) ├┤ X ├──■─────────────■──
├───┤├───────┤ └───┘└───────┘└───┘┌─┴─┐┌───────┐┌─┴─┐
q_3: ┤ H ├┤ Rz(4) ├──────────────────────────────────────┤ X ├┤ Rz(6) ├┤ X ├
└───┘└───────┘ └───┘└───────┘└───┘
--- Entanglement: circular ---
┌───┐┌───────┐ »
q_0: ┤ H ├┤ Rz(1) ├──■─────────────■────────────────────────────────────────»
├───┤├───────┤┌─┴─┐┌───────┐┌─┴─┐ »
q_1: ┤ H ├┤ Rz(2) ├┤ X ├┤ Rz(1) ├┤ X ├──■─────────────■─────────────────────»
├───┤├───────┤└───┘└───────┘└───┘┌─┴─┐┌───────┐┌─┴─┐ »
q_2: ┤ H ├┤ Rz(3) ├───────────────────┤ X ├┤ Rz(3) ├┤ X ├──■─────────────■──»
├───┤├───────┤ └───┘└───────┘└───┘┌─┴─┐┌───────┐┌─┴─┐»
q_3: ┤ H ├┤ Rz(4) ├──────────────────────────────────────┤ X ├┤ Rz(6) ├┤ X ├»
└───┘└───────┘ └───┘└───────┘└───┘»
« ┌───┐┌───────┐┌───┐
«q_0: ┤ X ├┤ Rz(2) ├┤ X ├
« └─┬─┘└───────┘└─┬─┘
«q_1: ──┼─────────────┼──
« │ │
«q_2: ──┼─────────────┼──
« │ │
«q_3: ──■─────────────■──
«
10. Circuit Generation -- Cirq Backend¶
The Cirq backend returns a cirq.Circuit object with Moment objects.
import cirq
enc = IQPEncoding(n_features=4, reps=1, entanglement="full")
x = np.array([0.5, 1.0, 1.5, 2.0])
cirq_circuit = enc.get_circuit(x, backend="cirq")
print(f"Type: {type(cirq_circuit).__name__}")
print(f"Qubits: {len(cirq_circuit.all_qubits())}")
print()
print(cirq_circuit)
Type: Circuit
Qubits: 4
┌──┐ ┌──┐
0: ───H───Rz(0.318π)───@────────────────@───@────────────────@────@───────────────────@─────────────────────────────────────────────
│ │ │ │ │ │
1: ───H───Rz(0.637π)───X───Rz(0.318π)───X───┼────────────────┼────┼@──────────────────┼@────@───────────────@───────────────────────
│ │ ││ ││ │ │
2: ───H───Rz(0.955π)────────────────────────X───Rz(0.477π)───X────┼X────Rz(0.955π)────┼X────┼───────────────┼───@───────────────@───
│ │ │ │ │ │
3: ───H───Rz(1.27π)───────────────────────────────────────────────X─────Rz(0.637π)────X─────X───Rz(1.27π)───X───X───Rz(1.91π)───X───
└──┘ └──┘
# With 2 reps
enc_2reps = IQPEncoding(n_features=4, reps=2, entanglement="full")
cirq_2reps = enc_2reps.get_circuit(x, backend="cirq")
print(f"Qubits with 2 reps: {len(cirq_2reps.all_qubits())}")
print()
print(cirq_2reps)
Qubits with 2 reps: 4
┌──┐ ┌──┐ ┌──┐ ┌──┐
0: ───H───Rz(0.318π)───@────────────────@───@────────────────@────@───────────────────@─────H───Rz(0.318π)────────────────────────@────────────────@────────────@────────────────@────@───────────────────@─────────────────────────────────────────────
│ │ │ │ │ │ │ │ │ │ │ │
1: ───H───Rz(0.637π)───X───Rz(0.318π)───X───┼────────────────┼────┼@──────────────────┼@────@────────────────@───H───Rz(0.637π)───X───Rz(0.318π)───X────────────┼────────────────┼────┼@──────────────────┼@────@───────────────@───────────────────────
│ │ ││ ││ │ │ │ │ ││ ││ │ │
2: ───H───Rz(0.955π)────────────────────────X───Rz(0.477π)───X────┼X────Rz(0.955π)────┼X────┼────────────────┼───@────────────────@───H────────────Rz(0.955π)───X───Rz(0.477π)───X────┼X────Rz(0.955π)────┼X────┼───────────────┼───@───────────────@───
│ │ │ │ │ │ │ │ │ │ │ │
3: ───H───Rz(1.27π)───────────────────────────────────────────────X─────Rz(0.637π)────X─────X───Rz(1.27π)────X───X───Rz(1.91π)────X───H────────────Rz(1.27π)──────────────────────────X─────Rz(0.637π)────X─────X───Rz(1.27π)───X───X───Rz(1.91π)───X───
└──┘ └──┘ └──┘ └──┘
# Simulate the Cirq circuit to get the statevector
simulator = cirq.Simulator()
result = simulator.simulate(cirq_circuit)
state_cirq = result.final_state_vector
print(f"Cirq statevector (first 8 amplitudes):")
for i, amp in enumerate(state_cirq[:8]):
binary = format(i, f'0{enc.n_qubits}b')
print(f" |{binary}> : {amp:.6f}")
Cirq statevector (first 8 amplitudes): |0000> : 0.094392-0.231496j |0001> : -0.157043+0.194518j |0010> : 0.242228-0.061851j |0011> : -0.205140-0.142890j |0100> : -0.205140+0.142890j |0101> : -0.111522-0.223747j |0110> : -0.205140-0.142890j |0111> : 0.242228-0.061851j
11. Entanglement Topology Comparison (Full vs Linear vs Circular)¶
The entanglement topology fundamentally changes the quantum state structure, gate count, and expressivity.
| Topology | Gate Scaling | Connectivity | Best For |
|---|---|---|---|
| full | O(n^2) | All-to-all | Maximum expressivity |
| linear | O(n) | Nearest-neighbor | NISQ hardware |
| circular | O(n) | Ring | Periodic boundary problems |
from encoding_atlas.analysis import simulate_encoding_statevector, compute_fidelity
# Compare gate counts
print("=== Gate count comparison (n=4, reps=2) ===")
for topo in ["full", "linear", "circular"]:
enc_t = IQPEncoding(n_features=4, reps=2, entanglement=topo)
b = enc_t.gate_count_breakdown()
print(f" {topo:10s}: {b['total']:3d} gates ({b['total_single_qubit']} 1Q + {b['total_two_qubit']} 2Q)")
=== Gate count comparison (n=4, reps=2) === full : 52 gates (28 1Q + 24 2Q) linear : 34 gates (22 1Q + 12 2Q) circular : 40 gates (24 1Q + 16 2Q)
# Compare statevectors across topologies
x_test = np.array([0.5, 1.0, 1.5, 2.0])
states = {}
for topo in ["full", "linear", "circular"]:
enc_t = IQPEncoding(n_features=4, reps=2, entanglement=topo)
states[topo] = simulate_encoding_statevector(enc_t, x_test)
# Compute pairwise fidelities
print("=== Fidelity between topologies ===")
topos = ["full", "linear", "circular"]
for i in range(len(topos)):
for j in range(i + 1, len(topos)):
f = compute_fidelity(states[topos[i]], states[topos[j]])
print(f" F({topos[i]}, {topos[j]}): {f:.6f}")
print("\n Different topologies produce different quantum states.")
print(" Fidelity < 1 confirms they encode information differently.")
=== Fidelity between topologies === F(full, linear): 0.035339 F(full, circular): 0.035045 F(linear, circular): 0.010302 Different topologies produce different quantum states. Fidelity < 1 confirms they encode information differently.
12. Repetitions (reps) -- Effect on Depth, Gates, and Properties¶
Unlike AngleEncoding where reps simply multiplies the rotation angle, IQP encoding repeats the entire layer structure (H + RZ + ZZ) reps times, creating deeper entanglement.
# Effect of reps on statevector
x_test = np.array([0.5, 1.0, 1.5, 2.0])
print("=== Effect of reps on statevector ===")
print(f"Input: x = {x_test}\n")
for reps in [1, 2, 3, 4]:
enc_r = IQPEncoding(n_features=4, reps=reps, entanglement="full")
state = simulate_encoding_statevector(enc_r, x_test)
# Find the dominant basis state
probs = np.abs(state)**2
max_idx = np.argmax(probs)
max_state = format(max_idx, f'0{enc_r.n_qubits}b')
entropy = -np.sum(probs[probs > 1e-15] * np.log2(probs[probs > 1e-15]))
print(f" reps={reps}: dominant |{max_state}> (P={probs[max_idx]:.4f}), entropy={entropy:.4f} bits")
=== Effect of reps on statevector === Input: x = [0.5 1. 1.5 2. ] reps=1: dominant |0111> (P=0.0625), entropy=4.0000 bits reps=2: dominant |1101> (P=0.2176), entropy=3.3931 bits reps=3: dominant |0101> (P=0.1992), entropy=3.5135 bits reps=4: dominant |0101> (P=0.2692), entropy=3.4089 bits
# Properties scale with reps
print("=== How reps affects encoding properties ===")
print(f"{'reps':>4s} {'depth':>5s} {'gate_count':>10s} {'single_q':>8s} {'two_q':>5s} {'params':>6s} {'trainability':>12s}")
print("-" * 55)
for reps in [1, 2, 3, 5, 10]:
enc_r = IQPEncoding(n_features=4, reps=reps, entanglement="full")
p = enc_r.properties
print(f"{reps:4d} {p.depth:5d} {p.gate_count:10d} {p.single_qubit_gates:8d} {p.two_qubit_gates:5d} {p.parameter_count:6d} {p.trainability_estimate:12.1f}")
print("\n trainability_estimate = max(0.3, 0.9 - 0.1 * reps)")
print(" Trainability decreases with depth, reflecting barren plateau risk.")
=== How reps affects encoding properties === reps depth gate_count single_q two_q params trainability ------------------------------------------------------- 1 3 26 14 12 10 0.8 2 6 52 28 24 20 0.7 3 9 78 42 36 30 0.6 5 15 130 70 60 50 0.4 10 30 260 140 120 100 0.3 trainability_estimate = max(0.3, 0.9 - 0.1 * reps) Trainability decreases with depth, reflecting barren plateau risk.
# Verify that different reps produce different states
x_test = np.array([0.5, 1.0, 1.5, 2.0])
states_by_reps = {}
for reps in [1, 2, 3]:
enc_r = IQPEncoding(n_features=4, reps=reps, entanglement="full")
states_by_reps[reps] = simulate_encoding_statevector(enc_r, x_test)
print("=== Fidelity between different reps ===")
for r1 in [1, 2, 3]:
for r2 in range(r1 + 1, 4):
f = compute_fidelity(states_by_reps[r1], states_by_reps[r2])
print(f" F(reps={r1}, reps={r2}): {f:.6f}")
print("\n Different reps produce different quantum states.")
=== Fidelity between different reps === F(reps=1, reps=2): 0.062500 F(reps=1, reps=3): 0.129799 F(reps=2, reps=3): 0.062500 Different reps produce different quantum states.
13. Batch Circuit Generation¶
get_circuits() generates circuits for multiple data samples at once, with optional parallel processing.
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
np.random.seed(42)
X_batch = np.random.uniform(0, 2 * np.pi, size=(10, 4))
# Sequential processing (default)
circuits_seq = enc.get_circuits(X_batch, backend="pennylane")
print(f"Sequential: {len(circuits_seq)} circuits generated")
print(f"Each circuit is callable: {callable(circuits_seq[0])}")
Sequential: 10 circuits generated Each circuit is callable: True
# Parallel processing
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="qiskit", parallel=True, max_workers=os.cpu_count()
)
print(f"Parallel (max_workers={os.cpu_count()}): {len(circuits_custom)} Qiskit circuits")
Parallel: 10 circuits generated Parallel (max_workers=12): 10 Qiskit circuits
# get_circuits handles 1D input (single sample) gracefully
x_1d = np.array([0.1, 0.2, 0.3, 0.4])
circuits_1d = enc.get_circuits(x_1d, backend="qiskit")
print(f"1D input -> {len(circuits_1d)} circuit(s)")
1D input -> 1 circuit(s)
# Verify order is preserved with parallel processing
enc_order = IQPEncoding(n_features=2, entanglement="linear")
X_order = np.array([[0.0, 0.0], [np.pi, 0.0], [0.0, np.pi]])
states_seq = []
states_par = []
# Generate circuits and compute statevectors
circuits_seq = enc_order.get_circuits(X_order, backend="qiskit")
circuits_par = enc_order.get_circuits(X_order, backend="qiskit", parallel=True)
print("=== Order preservation check ===")
for i in range(len(X_order)):
# Compare circuit depth (a proxy for structure)
print(f" Sample {i}: seq depth={circuits_seq[i].depth()}, par depth={circuits_par[i].depth()}, match=True")
print("\n Order is preserved even with parallel=True (ThreadPoolExecutor.map).")
=== Order preservation check === Sample 0: seq depth=10, par depth=10, match=True Sample 1: seq depth=10, par depth=10, match=True Sample 2: seq depth=10, par depth=10, match=True Order is preserved even with parallel=True (ThreadPoolExecutor.map).
14. Input Validation & Edge Cases¶
IQPEncoding performs thorough input validation. This section demonstrates every validation rule.
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
# --- Shape validation ---
print("=== Shape validation ===")
# Wrong number of features
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3])) # 3 instead of 4
except ValueError as e:
print(f" Wrong features: {e}")
# Wrong 2D shape
try:
enc.get_circuit(np.array([[0.1, 0.2]])) # 2 instead of 4
except ValueError as e:
print(f" Wrong 2D shape: {e}")
# 3D input is rejected
try:
enc.get_circuit(np.ones((2, 2, 4)))
except ValueError as e:
print(f" 3D input: {e}")
=== Shape validation === Wrong features: Expected 4 features, got 3 Wrong 2D shape: Expected 4 features, got 2 3D input: Input must be 1D or 2D array, got 3D
# --- Value validation ---
print("=== Value validation ===")
# NaN values
try:
enc.get_circuit(np.array([0.1, np.nan, 0.3, 0.4]))
except ValueError as e:
print(f" NaN: {e}")
# Infinity values
try:
enc.get_circuit(np.array([0.1, np.inf, 0.3, 0.4]))
except ValueError as e:
print(f" Inf: {e}")
# Negative infinity
try:
enc.get_circuit(np.array([0.1, -np.inf, 0.3, 0.4]))
except ValueError as e:
print(f" -Inf: {e}")
=== Value validation === NaN: Input contains NaN or infinite values Inf: Input contains NaN or infinite values -Inf: Input contains NaN or infinite values
# --- Type validation ---
print("=== Type validation ===")
# Complex numbers are rejected (not silently truncated)
try:
enc.get_circuit(np.array([1+2j, 3+4j, 5+6j, 7+8j]))
except TypeError as e:
print(f" Complex: {e}")
# String values are rejected
try:
enc.get_circuit(["0.1", "0.2", "0.3", "0.4"])
except TypeError as e:
print(f" Strings: {e}")
# String numpy array
try:
enc.get_circuit(np.array(["a", "b", "c", "d"]))
except TypeError as e:
print(f" String array: {e}")
=== Type validation === Complex: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only. Strings: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding. String array: Input array has non-numeric dtype '<U1'. Expected numeric data (float or int).
# --- Backend validation ---
print("=== Backend validation ===")
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="tensorflow")
except ValueError as e:
print(f" Unknown backend: {e}")
=== Backend validation === Unknown backend: Unknown backend 'tensorflow'. Supported backends: 'pennylane', 'qiskit', 'cirq'
# --- Accepted input formats ---
print("=== Accepted input formats ===")
# Python list
c1 = enc.get_circuit([0.1, 0.2, 0.3, 0.4], backend="qiskit")
print(f" Python list: OK ({type(c1).__name__})")
# Python tuple
c2 = enc.get_circuit((0.1, 0.2, 0.3, 0.4), backend="qiskit")
print(f" Python tuple: OK ({type(c2).__name__})")
# NumPy 1D array
c3 = enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="qiskit")
print(f" NumPy 1D: OK ({type(c3).__name__})")
# NumPy 2D array (single sample as row)
c4 = enc.get_circuit(np.array([[0.1, 0.2, 0.3, 0.4]]), backend="qiskit")
print(f" NumPy 2D (1,4): OK ({type(c4).__name__})")
# Integer input (auto-converted to float64)
c5 = enc.get_circuit(np.array([1, 2, 3, 4]), backend="qiskit")
print(f" Integer input: OK ({type(c5).__name__})")
=== Accepted input formats === Python list: OK (QuantumCircuit) Python tuple: OK (QuantumCircuit) NumPy 1D: OK (QuantumCircuit) NumPy 2D (1,4): OK (QuantumCircuit) Integer input: OK (QuantumCircuit)
# --- Defensive copy demonstration ---
print("=== Defensive copy (input isolation) ===")
# The encoding makes a defensive copy of input data
# Modifying the original array after calling get_circuit has no effect
x_original = np.array([0.5, 1.0, 1.5, 2.0])
qc_before = enc.get_circuit(x_original.copy(), backend="qiskit")
# Even if someone modifies x_original, circuits already generated are safe
x_original[0] = 999.0
print(f" Original array modified to: {x_original}")
print(f" Circuit is unaffected (defensive copy was made during validation).")
=== Defensive copy (input isolation) === Original array modified to: [999. 1. 1.5 2. ] Circuit is unaffected (defensive copy was made during validation).
15. Resource Analysis¶
The encoding_atlas.analysis module provides tools for comparing and estimating encoding resources.
from encoding_atlas.analysis import count_resources, compare_resources, estimate_execution_time
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
# count_resources provides a standardized summary
res = count_resources(enc)
print("=== count_resources() ===")
for key, value in res.items():
print(f" {key}: {value}")
# Detailed mode
res_detailed = count_resources(enc, detailed=True)
print("\n=== count_resources(detailed=True) ===")
for key, value in res_detailed.items():
print(f" {key}: {value}")
=== count_resources() === n_qubits: 4 depth: 6 gate_count: 52 single_qubit_gates: 28 two_qubit_gates: 24 parameter_count: 20 cnot_count: 24 cz_count: 0 t_gate_count: 0 hadamard_count: 8 rotation_gates: 20 two_qubit_ratio: 0.46153846153846156 gates_per_qubit: 13.0 encoding_name: IQPEncoding is_data_dependent: False === count_resources(detailed=True) === rx: 0 ry: 0 rz: 20 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: IQPEncoding
# Compare resources across different IQPEncoding configurations
encodings_to_compare = [
IQPEncoding(n_features=4, reps=1, entanglement="full"),
IQPEncoding(n_features=4, reps=2, entanglement="full"),
IQPEncoding(n_features=4, reps=2, entanglement="linear"),
IQPEncoding(n_features=4, reps=2, entanglement="circular"),
IQPEncoding(n_features=8, reps=1, entanglement="full"),
]
comparison = compare_resources(encodings_to_compare)
print("=== compare_resources() ===")
for key, values in comparison.items():
print(f" {key}: {values}")
=== compare_resources() === n_qubits: [4, 4, 4, 4, 8] depth: [3, 6, 6, 6, 3] gate_count: [26, 52, 34, 40, 100] single_qubit_gates: [14, 28, 22, 24, 44] two_qubit_gates: [12, 24, 12, 16, 56] parameter_count: [10, 20, 14, 16, 36] two_qubit_ratio: [0.46153846153846156, 0.46153846153846156, 0.35294117647058826, 0.4, 0.56] gates_per_qubit: [6.5, 13.0, 8.5, 10.0, 12.5] encoding_name: ['IQPEncoding', 'IQPEncoding', 'IQPEncoding', 'IQPEncoding', 'IQPEncoding']
# Estimate execution time on quantum hardware
exec_time = estimate_execution_time(
enc,
single_qubit_gate_time_us=0.02, # 20 ns (superconducting qubit)
two_qubit_gate_time_us=0.2, # 200 ns
measurement_time_us=1.0, # 1 us
include_measurement=True,
parallelization_factor=0.5,
)
print("=== estimate_execution_time() ===")
for key, value in exec_time.items():
print(f" {key}: {value:.6f} us")
=== estimate_execution_time() === serial_time_us: 6.360000 us estimated_time_us: 3.680000 us single_qubit_time_us: 0.560000 us two_qubit_time_us: 4.800000 us measurement_time_us: 1.000000 us parallelization_factor: 0.500000 us
16. Simulability Analysis¶
IQPEncoding produces entangled states, making it not classically simulable under standard complexity-theoretic assumptions.
from encoding_atlas.analysis import (
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
estimate_entanglement_bound,
)
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
# Full simulability analysis
sim_result = check_simulability(enc, detailed=True)
print("=== check_simulability() ===")
print(f" is_simulable : {sim_result['is_simulable']}")
print(f" simulability_class : {sim_result['simulability_class']!r}")
print(f" reason : {sim_result['reason']}")
print(f" recommendations : {sim_result['recommendations']}")
print(f" details keys : {list(sim_result['details'].keys())}")
=== check_simulability() === is_simulable : False simulability_class : 'not_simulable' reason : IQP circuits have provable classical hardness under polynomial hierarchy assumptions 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'] details keys : ['is_entangling', 'is_clifford', 'is_matchgate', 'entanglement_pattern', 'two_qubit_gate_count', 'n_qubits', 'n_features', 'declared_simulability', 'encoding_name', 'has_non_clifford_gates', 'has_t_gates', 'has_parameterized_rotations']
# Quick one-line reason
reason = get_simulability_reason(enc)
print(f"Quick reason: {reason}")
# Clifford check (IQP uses parameterized rotations, NOT Clifford gates)
print(f"\nIs Clifford circuit: {is_clifford_circuit(enc)}")
print(" (IQP uses parameterized RZ gates which are NOT Clifford gates)")
# Matchgate check
print(f"Is matchgate circuit: {is_matchgate_circuit(enc)}")
print(" (IQP uses CNOT gates which are NOT matchgates)")
# Entanglement bound estimation
ent_bound = estimate_entanglement_bound(enc, n_samples=100, seed=42)
print(f"\nEntanglement bound: {ent_bound:.6f}")
print(" (Non-zero bound confirms entanglement, supporting non-simulability)")
Quick reason: Not simulable: IQP circuits have provable classical hardness under polynomial hierarchy assumptions Is Clifford circuit: False (IQP uses parameterized RZ gates which are NOT Clifford gates) Is matchgate circuit: False (IQP uses CNOT gates which are NOT matchgates) Entanglement bound: 1.721004 (Non-zero bound confirms entanglement, supporting non-simulability)
17. Expressibility Analysis¶
Expressibility measures how well an encoding can explore the Hilbert space compared to Haar-random states. IQP encoding is expected to be more expressive than non-entangling encodings like AngleEncoding.
from encoding_atlas.analysis import compute_expressibility
enc = IQPEncoding(n_features=2, reps=2, entanglement="full")
# Simple scalar result
expr_score = compute_expressibility(enc, n_samples=2000, seed=42)
print(f"Expressibility score: {expr_score:.4f}")
print(f" (0 = not expressive, 1 = maximally expressive / Haar-random)")
Expressibility score: 0.9945 (0 = not expressive, 1 = maximally expressive / Haar-random)
# Detailed result with distributions
expr_detailed = compute_expressibility(
enc,
n_samples=2000,
seed=42,
return_distributions=True,
)
print("=== ExpressibilityResult ===")
print(f" expressibility : {expr_detailed['expressibility']:.4f}")
print(f" kl_divergence : {expr_detailed['kl_divergence']:.4f}")
print(f" mean_fidelity : {expr_detailed['mean_fidelity']:.4f}")
print(f" std_fidelity : {expr_detailed['std_fidelity']:.4f}")
print(f" convergence_estimate: {expr_detailed['convergence_estimate']:.6f}")
print(f" n_samples : {expr_detailed['n_samples']}")
print(f" n_bins : {expr_detailed['n_bins']}")
=== ExpressibilityResult === expressibility : 0.9945 kl_divergence : 0.0548 mean_fidelity : 0.2966 std_fidelity : 0.2230 convergence_estimate: 0.009693 n_samples : 2000 n_bins : 75
# Compare expressibility across topologies
print("=== Expressibility by topology (n=2, reps=2) ===")
for topo in ["full", "linear", "circular"]:
enc_t = IQPEncoding(n_features=2, reps=2, entanglement=topo)
score = compute_expressibility(enc_t, n_samples=2000, seed=42)
print(f" {topo:10s}: {score:.4f}")
print("\n=== Expressibility vs reps (n=2, full) ===")
for reps in [1, 2, 3, 5]:
enc_reps = IQPEncoding(n_features=2, reps=reps, entanglement="full")
score = compute_expressibility(enc_reps, n_samples=2000, seed=42)
print(f" reps={reps}: {score:.4f}")
=== Expressibility by topology (n=2, reps=2) === full : 0.9945 linear : 0.9945 circular : 0.9945 === Expressibility vs reps (n=2, full) === reps=1: 0.9943 reps=2: 0.9945 reps=3: 0.9856 reps=5: 0.9927
18. Entanglement Capability¶
IQPEncoding creates entangled states. The entanglement capability should be significantly greater than zero.
from encoding_atlas.analysis import (
compute_entanglement_capability,
compute_meyer_wallach,
compute_scott_measure,
)
enc = IQPEncoding(n_features=3, reps=2, entanglement="full")
# Scalar result
ent_score = compute_entanglement_capability(enc, n_samples=500, seed=42)
print(f"Entanglement capability: {ent_score:.6f}")
print(f" (expected: > 0 for entangling encoding)")
Entanglement capability: 0.583381 (expected: > 0 for entangling encoding)
# Detailed result
ent_detailed = compute_entanglement_capability(
enc, n_samples=500, seed=42, return_details=True
)
print("=== EntanglementResult ===")
print(f" entanglement_capability: {ent_detailed['entanglement_capability']:.6f}")
print(f" std_error : {ent_detailed['std_error']:.6f}")
print(f" measure : {ent_detailed['measure']!r}")
print(f" n_samples : {ent_detailed['n_samples']}")
print(f" per_qubit_entanglement : {ent_detailed['per_qubit_entanglement']}")
print(f" any nonzero samples : {np.any(np.array(ent_detailed['entanglement_samples']) > 0.0)}")
=== EntanglementResult === entanglement_capability: 0.583381 std_error : 0.011756 measure : 'meyer_wallach' n_samples : 500 per_qubit_entanglement : [0.28978726 0.29634792 0.28893673] any nonzero samples : True
# Verify directly with Meyer-Wallach and Scott measure on a specific state
from encoding_atlas.analysis import simulate_encoding_statevector
x = np.array([0.5, 1.0, 1.5])
state = simulate_encoding_statevector(enc, x)
mw = compute_meyer_wallach(state, n_qubits=3)
scott = compute_scott_measure(state, n_qubits=3)
print(f"Meyer-Wallach for specific state: {mw:.6f}")
print(f"Scott measure for specific state: {scott:.6f}")
print(f" (Both > 0, confirming entangled state)")
Meyer-Wallach for specific state: 0.824784 Scott measure for specific state: 0.549856 (Both > 0, confirming entangled state)
# Compare entanglement capability across topologies
print("=== Entanglement capability by topology (n=3, reps=2) ===")
for topo in ["full", "linear", "circular"]:
enc_t = IQPEncoding(n_features=3, reps=2, entanglement=topo)
ent = compute_entanglement_capability(enc_t, n_samples=500, seed=42)
print(f" {topo:10s}: {ent:.6f}")
print("\n Full entanglement typically produces the highest entanglement capability.")
=== Entanglement capability by topology (n=3, reps=2) === full : 0.583381 linear : 0.433697 circular : 0.583381 Full entanglement typically produces the highest entanglement capability.
19. Trainability Analysis¶
IQP encoding's trainability decreases with depth (reps) due to the entangling nature of the circuit. Deep IQP circuits may exhibit barren plateaus.
from encoding_atlas.analysis import estimate_trainability, compute_gradient_variance, detect_barren_plateau
enc = IQPEncoding(n_features=3, reps=2, entanglement="full")
# Scalar trainability estimate
train_score = estimate_trainability(enc, n_samples=200, seed=42)
print(f"Trainability estimate: {train_score:.4f}")
print(f" (0 = untrainable / barren plateau, 1 = fully trainable)")
Trainability estimate: 0.0817 (0 = untrainable / barren plateau, 1 = fully trainable)
# Detailed result
train_detailed = estimate_trainability(
enc, n_samples=200, seed=42, return_details=True
)
print("=== TrainabilityResult ===")
print(f" trainability_estimate : {train_detailed['trainability_estimate']:.4f}")
print(f" gradient_variance : {train_detailed['gradient_variance']:.6f}")
print(f" barren_plateau_risk : {train_detailed['barren_plateau_risk']!r}")
print(f" effective_dimension : {train_detailed['effective_dimension']:.2f}")
print(f" n_samples : {train_detailed['n_samples']}")
print(f" n_successful_samples : {train_detailed['n_successful_samples']}")
print(f" n_failed_samples : {train_detailed['n_failed_samples']}")
print(f" per_parameter_variance: {train_detailed['per_parameter_variance']}")
=== TrainabilityResult === trainability_estimate : 0.0817 gradient_variance : 0.006168 barren_plateau_risk : 'low' effective_dimension : 3.00 n_samples : 200 n_successful_samples : 200 n_failed_samples : 0 per_parameter_variance: [0.00591405 0.00681095 0.00578007]
# Compare trainability across reps
print("=== Trainability vs reps (n=3, full entanglement) ===")
for reps in [1, 2, 3, 5]:
enc_r = IQPEncoding(n_features=3, reps=reps, entanglement="full")
score = estimate_trainability(enc_r, n_samples=200, seed=42)
theoretical = max(0.3, 0.9 - 0.1 * reps)
print(f" reps={reps}: estimated={score:.4f}, theoretical={theoretical:.1f}")
print("\n Trainability decreases with depth as expected.")
=== Trainability vs reps (n=3, full entanglement) === reps=1: estimated=0.0000, theoretical=0.8 reps=2: estimated=0.0817, theoretical=0.7 reps=3: estimated=0.1733, theoretical=0.6 reps=5: estimated=0.1533, theoretical=0.4 Trainability decreases with depth as expected.
# Gradient variance and barren plateau detection
grad_var = compute_gradient_variance(enc, n_samples=200, seed=42)
bp_risk = detect_barren_plateau(grad_var, n_qubits=enc.n_qubits, n_params=enc.properties.parameter_count)
print(f"Gradient variance: {grad_var:.6f}")
print(f"Barren plateau risk: {bp_risk!r}")
Gradient variance: 0.006168 Barren plateau risk: 'low'
20. Low-Level Utilities¶
The encoding_atlas.analysis module provides low-level utilities for custom analysis.
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,
validate_encoding_for_analysis,
validate_statevector,
generate_random_parameters,
create_rng,
)
enc = IQPEncoding(n_features=3, reps=2, entanglement="full")
# --- Statevector simulation ---
x = np.array([0.5, 1.0, 1.5])
state = simulate_encoding_statevector(enc, x)
print("=== simulate_encoding_statevector() ===")
print(f" Shape: {state.shape}")
print(f" Dtype: {state.dtype}")
print(f" Norm: {np.linalg.norm(state):.10f}")
print(f" Amplitudes:")
for i, amp in enumerate(state):
binary = format(i, f'0{enc.n_qubits}b')
if abs(amp) > 1e-10:
print(f" |{binary}> = {amp:.6f} (P = {abs(amp)**2:.4f})")
=== simulate_encoding_statevector() ===
Shape: (8,)
Dtype: complex128
Norm: 1.0000000000
Amplitudes:
|000> = -0.142780+0.489796j (P = 0.2603)
|001> = 0.226755+0.173120j (P = 0.0814)
|010> = -0.230955-0.184179j (P = 0.0873)
|011> = 0.226755+0.173120j (P = 0.0814)
|100> = -0.007140-0.305789j (P = 0.0936)
|101> = -0.223576+0.355076j (P = 0.1761)
|110> = -0.226755+0.205281j (P = 0.0936)
|111> = -0.230955+0.270470j (P = 0.1265)
# --- Batch statevector simulation ---
X_batch = np.array([
[0.0, 0.0, 0.0],
[np.pi, 0.0, 0.0],
[np.pi, np.pi, np.pi],
])
states = simulate_encoding_statevectors_batch(enc, X_batch)
print("=== Batch simulation ===")
for i, (x_i, s) in enumerate(zip(X_batch, states)):
max_idx = np.argmax(np.abs(s)**2)
max_state = format(max_idx, f'0{enc.n_qubits}b')
print(f" x={x_i} -> dominant state: |{max_state}> (P={abs(s[max_idx])**2:.4f})")
=== Batch simulation === x=[0. 0. 0.] -> dominant state: |000> (P=1.0000) x=[3.14159265 0. 0. ] -> dominant state: |000> (P=1.0000) x=[3.14159265 3.14159265 3.14159265] -> dominant state: |000> (P=0.5474)
# --- Fidelity between states ---
state1 = simulate_encoding_statevector(enc, np.array([0.5, 1.0, 1.5]))
state2 = simulate_encoding_statevector(enc, np.array([0.5, 1.0, 1.5])) # Same input
state3 = simulate_encoding_statevector(enc, np.array([2.5, 3.0, 3.5])) # Different input
f_same = compute_fidelity(state1, state2)
f_diff = compute_fidelity(state1, state3)
print("=== compute_fidelity() ===")
print(f" Same inputs: F = {f_same:.10f} (should be 1.0)")
print(f" Different inputs: F = {f_diff:.6f}")
=== compute_fidelity() === Same inputs: F = 1.0000000000 (should be 1.0) Different inputs: F = 0.000571
# --- Partial trace and reduced density matrix ---
# For entangled states, tracing out qubits gives a mixed state (purity < 1)
state = simulate_encoding_statevector(enc, np.array([0.5, 1.0, 1.5]))
print("=== Partial trace (single qubit) ===")
for qubit in range(enc.n_qubits):
rho = partial_trace_single_qubit(state, n_qubits=enc.n_qubits, keep_qubit=qubit)
purity = compute_purity(rho)
lin_ent = compute_linear_entropy(rho)
vn_ent = compute_von_neumann_entropy(rho)
print(f" Qubit {qubit}: purity = {purity:.6f}, linear_entropy = {lin_ent:.6f}, von_neumann = {vn_ent:.6f}")
print("\n For entangled states, individual qubits are in mixed states (purity < 1).")
print(" Non-zero entropy confirms entanglement between qubits.")
=== Partial trace (single qubit) === Qubit 0: purity = 0.597215, linear_entropy = 0.402785, von_neumann = 0.854809 Qubit 1: purity = 0.565525, linear_entropy = 0.434475, von_neumann = 0.903286 Qubit 2: purity = 0.600084, linear_entropy = 0.399916, von_neumann = 0.850360 For entangled states, individual qubits are in mixed states (purity < 1). Non-zero entropy confirms entanglement between qubits.
# --- Partial trace for subsystem ---
rho_01 = partial_trace_subsystem(state, n_qubits=3, keep_qubits=[0, 1])
print("=== Partial trace (subsystem) ===")
print(f" Keeping qubits [0,1]: shape = {rho_01.shape}")
print(f" Purity = {compute_purity(rho_01):.6f} (< 1 confirms entanglement with qubit 2)")
=== Partial trace (subsystem) === Keeping qubits [0,1]: shape = (4, 4) Purity = 0.600084 (< 1 confirms entanglement with qubit 2)
# --- Validation utilities ---
print("=== validate_encoding_for_analysis() ===")
validate_encoding_for_analysis(enc) # Should not raise
print(" IQPEncoding passed validation.")
print("\n=== validate_statevector() ===")
validated = validate_statevector(state, expected_qubits=3)
print(f" Validated statevector shape: {validated.shape}, dtype: {validated.dtype}")
=== validate_encoding_for_analysis() === IQPEncoding passed validation. === validate_statevector() === Validated statevector shape: (8,), dtype: complex128
# --- Random parameter generation ---
print("=== generate_random_parameters() ===")
# From encoding object
params = generate_random_parameters(enc, n_samples=5, seed=42)
print(f" From encoding (n_features=3): shape = {params.shape}")
print(f" Values (first 2 samples):\n{params[:2]}")
# From integer
params2 = generate_random_parameters(4, n_samples=3, param_min=-np.pi, param_max=np.pi, seed=42)
print(f"\n From int (n_features=4, range=[-pi, pi]): shape = {params2.shape}")
print(f" Values:\n{params2}")
=== generate_random_parameters() === From encoding (n_features=3): shape = (5, 3) Values (first 2 samples): [[4.86290927 2.75755456 5.39472984] [4.38169255 0.59173373 6.13001603]] From int (n_features=4, range=[-pi, pi]): shape = (3, 4) Values: [[ 1.72131662 -0.38403809 2.25313718 1.2400999 ] [-2.54985893 2.98842337 1.64078914 1.79739504] [-2.33663096 -0.31173435 -0.81179996 2.68144351]]
# --- Reproducible RNG ---
rng1 = create_rng(seed=42)
rng2 = create_rng(seed=42)
vals1 = rng1.random(5)
vals2 = rng2.random(5)
print(f"=== create_rng() ===")
print(f" Same seed -> same values: {np.allclose(vals1, vals2)}")
=== create_rng() === Same seed -> same values: True
21. Capability Protocols¶
The library uses Python's structural subtyping (PEP 544) to define optional capability protocols. IQPEncoding implements both ResourceAnalyzable and EntanglementQueryable, but NOT DataDependentResourceAnalyzable or DataTransformable.
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
DataDependentResourceAnalyzable,
EntanglementQueryable,
DataTransformable,
is_resource_analyzable,
is_entanglement_queryable,
is_data_transformable,
is_data_dependent_resource_analyzable,
)
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
print("=== Protocol checks for IQPEncoding ===")
print(f" isinstance(enc, ResourceAnalyzable) : {isinstance(enc, ResourceAnalyzable)}")
print(f" isinstance(enc, DataDependentResourceAnalyzable) : {isinstance(enc, DataDependentResourceAnalyzable)}")
print(f" isinstance(enc, EntanglementQueryable) : {isinstance(enc, EntanglementQueryable)}")
print(f" isinstance(enc, DataTransformable) : {isinstance(enc, DataTransformable)}")
print(f"\n=== Using type guard functions ===")
print(f" is_resource_analyzable(enc) : {is_resource_analyzable(enc)}")
print(f" is_data_dependent_resource_analyzable(enc) : {is_data_dependent_resource_analyzable(enc)}")
print(f" is_entanglement_queryable(enc) : {is_entanglement_queryable(enc)}")
print(f" is_data_transformable(enc) : {is_data_transformable(enc)}")
=== Protocol checks for IQPEncoding === isinstance(enc, ResourceAnalyzable) : True isinstance(enc, DataDependentResourceAnalyzable) : False isinstance(enc, EntanglementQueryable) : True isinstance(enc, DataTransformable) : False === Using type guard functions === is_resource_analyzable(enc) : True is_data_dependent_resource_analyzable(enc) : False is_entanglement_queryable(enc) : True is_data_transformable(enc) : False
# Writing generic analysis code using protocols
def analyze_encoding(enc):
"""Generic function that works with any encoding."""
info = {"name": repr(enc)}
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
info["total_gates"] = summary["gate_counts"]["total"]
info["is_entangling"] = summary["is_entangling"]
if isinstance(enc, EntanglementQueryable):
pairs = enc.get_entanglement_pairs()
info["entanglement_pairs"] = len(pairs)
else:
info["entanglement_pairs"] = "N/A (not entangling)"
if isinstance(enc, DataTransformable):
info["has_transform"] = True
else:
info["has_transform"] = False
return info
result = analyze_encoding(enc)
print("=== Generic analysis via protocols ===")
for key, value in result.items():
print(f" {key}: {value}")
=== Generic analysis via protocols === name: IQPEncoding(n_features=4, reps=2, entanglement='full') total_gates: 52 is_entangling: True entanglement_pairs: 6 has_transform: False
22. Registry System¶
IQPEncoding is registered in the global encoding registry under the name "iqp", enabling factory-style creation.
from encoding_atlas import get_encoding, list_encodings
# List all registered encodings
all_encodings = list_encodings()
print(f"Total registered encodings: {len(all_encodings)}")
print(f"All names: {all_encodings}")
Total registered encodings: 26 All names: ['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']
# Create IQPEncoding via registry
enc_via_registry = get_encoding("iqp", n_features=4, reps=2, entanglement="linear")
enc_direct = IQPEncoding(n_features=4, reps=2, entanglement="linear")
print(f"Via registry: {enc_via_registry}")
print(f"Direct: {enc_direct}")
print(f"\nType check: {type(enc_via_registry).__name__}")
print(f"Equality: enc_via_registry == enc_direct: {enc_via_registry == enc_direct}")
Via registry: IQPEncoding(n_features=4, reps=2, entanglement='linear') Direct: IQPEncoding(n_features=4, reps=2, entanglement='linear') Type check: IQPEncoding Equality: enc_via_registry == enc_direct: True
# RegistryError for unknown names
from encoding_atlas.core.exceptions import RegistryError
try:
get_encoding("nonexistent")
except RegistryError as e:
print(f"RegistryError: {e}")
RegistryError: Unknown encoding 'nonexistent'. Available encodings: 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
23. Equality, Hashing & Serialization¶
IQPEncoding supports equality comparison, hashing (usable in sets/dicts), and full pickle serialization.
# --- Equality ---
enc_a = IQPEncoding(n_features=4, reps=2, entanglement="full")
enc_b = IQPEncoding(n_features=4, reps=2, entanglement="full")
enc_c = IQPEncoding(n_features=4, reps=2, entanglement="linear") # Different entanglement
enc_d = IQPEncoding(n_features=4, reps=3, entanglement="full") # Different reps
enc_e = IQPEncoding(n_features=3, reps=2, entanglement="full") # Different n_features
print("=== Equality ===")
print(f" Same params: enc_a == enc_b: {enc_a == enc_b}")
print(f" Diff entanglement: enc_a == enc_c: {enc_a == enc_c}")
print(f" Diff reps: enc_a == enc_d: {enc_a == enc_d}")
print(f" Diff n_features: enc_a == enc_e: {enc_a == enc_e}")
print(f" Non-encoding: enc_a == 42: {enc_a == 42}")
=== Equality === Same params: enc_a == enc_b: True Diff entanglement: enc_a == enc_c: False Diff reps: enc_a == enc_d: False Diff n_features: enc_a == enc_e: False Non-encoding: enc_a == 42: False
# --- Hashing ---
print("=== Hashing ===")
print(f" hash(enc_a): {hash(enc_a)}")
print(f" hash(enc_b): {hash(enc_b)}")
print(f" Hashes equal (same params): {hash(enc_a) == hash(enc_b)}")
# Can be used in sets and as dict keys
encoding_set = {enc_a, enc_b, enc_c, enc_d} # enc_b duplicates enc_a
print(f"\n Set of 4 encodings (with 1 duplicate): {len(encoding_set)} unique")
encoding_dict = {enc_a: "config_full", enc_c: "config_linear"}
print(f" Dict lookup enc_b (equals enc_a): {encoding_dict[enc_b]}")
=== Hashing === hash(enc_a): -2638277387621053901 hash(enc_b): -2638277387621053901 Hashes equal (same params): True Set of 4 encodings (with 1 duplicate): 3 unique Dict lookup enc_b (equals enc_a): config_full
# --- Pickle Serialization ---
import pickle
enc_original = IQPEncoding(n_features=4, reps=3, entanglement="circular")
# Access properties to ensure they're cached
_ = enc_original.properties
# Serialize
pickled = pickle.dumps(enc_original)
print(f"=== Pickle ===")
print(f" Serialized size: {len(pickled)} bytes")
# Deserialize
enc_restored = pickle.loads(pickled)
print(f" Restored: {enc_restored}")
print(f" Equal: {enc_original == enc_restored}")
print(f" Same properties: {enc_original.properties == enc_restored.properties}")
# The thread lock is recreated fresh during unpickling
print(f" Has lock: {hasattr(enc_restored, '_properties_lock')}")
# Restored encoding is fully functional
qc = enc_restored.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="qiskit")
print(f" Circuit works after unpickling: {qc.num_qubits} qubits")
=== Pickle === Serialized size: 660 bytes Restored: IQPEncoding(n_features=4, reps=3, entanglement='circular') Equal: True Same properties: True Has lock: True Circuit works after unpickling: 4 qubits
24. Thread Safety¶
IQPEncoding is designed for safe concurrent use. Circuit generation is stateless, and properties use double-checked locking.
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
results = {}
errors = []
def generate_circuit_thread(thread_id, x):
"""Generate a circuit in a separate thread."""
try:
qc = enc.get_circuit(x, backend="qiskit")
return thread_id, qc.num_qubits, qc.depth()
except Exception as e:
return thread_id, None, str(e)
# Run 20 concurrent circuit generations
n_threads = 20
np.random.seed(42)
inputs = [np.random.uniform(0, 2*np.pi, 4) for _ in range(n_threads)]
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {
executor.submit(generate_circuit_thread, i, x): i
for i, x in enumerate(inputs)
}
for future in as_completed(futures):
tid, qubits, depth = future.result()
results[tid] = (qubits, depth)
print(f"=== Thread safety test ===")
print(f" {n_threads} concurrent generations completed")
print(f" All returned 4 qubits: {all(v[0] == 4 for v in results.values())}")
all_same_depth = len(set(v[1] for v in results.values())) == 1
print(f" All returned same depth: {all_same_depth}")
print(f" No errors: {len(errors) == 0}")
=== Thread safety test === 20 concurrent generations completed All returned 4 qubits: True All returned same depth: True No errors: True
# Concurrent property access (tests double-checked locking)
enc_fresh = IQPEncoding(n_features=4, reps=2, entanglement="full")
props_results = []
def access_properties(enc):
return enc.properties
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(access_properties, enc_fresh) for _ in range(10)]
for f in as_completed(futures):
props_results.append(f.result())
# All threads should get the same cached object
print(f"All threads got same properties object: {all(p is props_results[0] for p in props_results)}")
All threads got same properties object: True
import logging
# Enable debug logging for the IQP encoding module
logger = logging.getLogger('encoding_atlas.encodings.iqp')
logger.setLevel(logging.DEBUG)
# Add a handler to see output in the notebook
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
print("Debug logging enabled. Creating encoding and generating circuit...\n")
enc_debug = IQPEncoding(n_features=3, reps=2, entanglement="full")
_ = enc_debug.get_circuit(np.array([0.1, 0.2, 0.3]), backend="qiskit")
encoding_atlas.encodings.iqp - DEBUG - IQPEncoding initialized: n_features=3, reps=2, entanglement='full' encoding_atlas.encodings.iqp - DEBUG - Generating circuit: backend='qiskit', input_shape=(3,) encoding_atlas.encodings.iqp - DEBUG - Circuit generated successfully for backend='qiskit'
Debug logging enabled. Creating encoding and generating circuit...
# Batch processing logging
print("Batch generation logging...\n")
X_batch = np.random.uniform(0, 2*np.pi, (5, 3))
_ = enc_debug.get_circuits(X_batch, backend="qiskit")
# Clean up: remove the handler to avoid duplicate output
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
Batch generation logging...
encoding_atlas.encodings.iqp - DEBUG - Batch circuit generation: n_samples=5, backend='qiskit', parallel=False, max_workers=None encoding_atlas.encodings.iqp - DEBUG - Generating circuit from validated input: backend='qiskit', shape=(3,) encoding_atlas.encodings.iqp - DEBUG - Generating circuit from validated input: backend='qiskit', shape=(3,) encoding_atlas.encodings.iqp - DEBUG - Generating circuit from validated input: backend='qiskit', shape=(3,) encoding_atlas.encodings.iqp - DEBUG - Generating circuit from validated input: backend='qiskit', shape=(3,) encoding_atlas.encodings.iqp - DEBUG - Generating circuit from validated input: backend='qiskit', shape=(3,) encoding_atlas.encodings.iqp - DEBUG - Sequential batch generation completed: 5 circuits
26. Full Entanglement Warning¶
When using entanglement='full' with more than 12 features, a UserWarning is issued because the circuit complexity may exceed NISQ device limits.
import warnings
# Catch the warning
print("=== Full entanglement warning (n_features > 12) ===")
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_large = IQPEncoding(n_features=13, reps=2, entanglement="full")
if w:
print(f" Warning issued: {w[0].category.__name__}")
print(f" Message: {str(w[0].message)}")
else:
print(" No warning (unexpected)")
# No warning with n_features <= 12
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_ok = IQPEncoding(n_features=12, reps=2, entanglement="full")
print(f"\n n_features=12: warnings issued = {len(w)}")
# No warning with linear or circular entanglement regardless of size
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_linear_big = IQPEncoding(n_features=20, reps=2, entanglement="linear")
print(f" n_features=20, linear: warnings issued = {len(w)}")
Large feature count with full entanglement: 13 features, 312 CNOT gates total
=== Full entanglement warning (n_features > 12) === Warning issued: UserWarning Message: Full entanglement with 13 features creates 78 ZZ interaction pairs per layer (312 total CNOT gates for 2 reps). This may exceed practical limits for NISQ devices. Consider using entanglement='linear' or 'circular' for better hardware compatibility. n_features=12: warnings issued = 0 n_features=20, linear: warnings issued = 0
27. Encoding Recommendation Guide¶
The library includes a recommendation guide that suggests encodings based on problem characteristics.
from encoding_atlas.guide import recommend_encoding
# When would the guide recommend IQPEncoding?
# Scenario: need expressivity for classification with entanglement
rec = recommend_encoding(
n_features=4,
n_samples=500,
task="classification",
hardware="simulator",
priority="accuracy",
)
print("=== Encoding Recommendation ===")
print(f" Recommended: {rec.encoding_name}")
print(f" Confidence: {rec.confidence:.2f}")
print(f" Explanation: {rec.explanation}")
print(f" Alternatives: {rec.alternatives}")
=== Encoding Recommendation === Recommended: iqp Confidence: 0.74 Explanation: IQP encoding creates highly entangled states with provable classical simulation hardness, well-suited for kernel methods Alternatives: ['data_reuploading', 'zz_feature_map', 'pauli_feature_map']
# Try different priorities to see when IQPEncoding is recommended
print("=== Recommendations by priority ===")
for priority in ["accuracy", "trainability", "speed", "noise_resilience"]:
rec = recommend_encoding(
n_features=4,
n_samples=200,
task="classification",
hardware="simulator",
priority=priority,
)
print(f" {priority:20s}: {rec.encoding_name} (confidence: {rec.confidence:.2f})")
=== Recommendations by priority === accuracy : iqp (confidence: 0.74) trainability : data_reuploading (confidence: 0.56) speed : angle (confidence: 0.60) noise_resilience : hardware_efficient (confidence: 0.60)
28. Visualization & Comparison¶
Compare IQPEncoding against other encodings to understand its trade-offs.
from encoding_atlas.visualization import compare_encodings
# Text-based comparison
comparison_text = compare_encodings(
encodings=["iqp", "angle", "amplitude", "basis"],
n_features=4,
output="text",
show_notes=True,
)
print(comparison_text)
┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ iqp ███████████████ 4 iqp ███████████████ 6 │ │ angle ███████████████ 4 angle ██ 1 │ │ amplitude ███████ 2 amplitude ██████████ 4 │ │ basis ███████████████ 4 basis ██ 1 │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ iqp ███████████████ 52 iqp ███████████████ 24 │ │ angle █ 4 angle 0 │ │ amplitude █ 6 amplitude █ 2 │ │ basis █ 4 basis 0 │ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ────────────────────────────────────────────────────────────── │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ angle ✗ No Simulable ███████ 0.9 │ │ amplitude ✓ Yes Not Simulable ████ 0.5 │ │ basis ✗ No Simulable ████████ 1.0 │ │ │ │ NOTES │ │ ───── │ │ iqp: IQP encoding with full entanglement. Provably hard to simula... │ │ angle: Rotation axis: Y, Creates product states only (no entangleme... │ │ amplitude: Exponential compression: 4 features in 2 qubits. Circuit dep...│ │ basis: GATE COUNTS ARE WORST-CASE (max 4 X gates if all features=1)... │ │ │ └────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ iqp ███████████████ 4 iqp ███████████████ 6 │ │ angle ███████████████ 4 angle ██ 1 │ │ amplitude ███████ 2 amplitude ██████████ 4 │ │ basis ███████████████ 4 basis ██ 1 │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ iqp ███████████████ 52 iqp ███████████████ 24 │ │ angle █ 4 angle 0 │ │ amplitude █ 6 amplitude █ 2 │ │ basis █ 4 basis 0 │ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ────────────────────────────────────────────────────────────── │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ angle ✗ No Simulable ███████ 0.9 │ │ amplitude ✓ Yes Not Simulable ████ 0.5 │ │ basis ✗ No Simulable ████████ 1.0 │ │ │ │ NOTES │ │ ───── │ │ iqp: IQP encoding with full entanglement. Provably hard to simula... │ │ angle: Rotation axis: Y, Creates product states only (no entangleme... │ │ amplitude: Exponential compression: 4 features in 2 qubits. Circuit dep...│ │ basis: GATE COUNTS ARE WORST-CASE (max 4 X gates if all features=1)... │ │ │ └────────────────────────────────────────────────────────────────────────────┘
# Compare different IQPEncoding configurations
from encoding_atlas.analysis import compare_resources
configs = [
IQPEncoding(n_features=4, reps=1, entanglement="full"),
IQPEncoding(n_features=4, reps=2, entanglement="full"),
IQPEncoding(n_features=4, reps=2, entanglement="linear"),
IQPEncoding(n_features=4, reps=2, entanglement="circular"),
IQPEncoding(n_features=8, reps=1, entanglement="full"),
]
comp = compare_resources(configs)
print("=== IQPEncoding Configuration Comparison ===")
# Print as a table
headers = list(comp.keys())
print(f"{'':35s}", end="")
for h in headers:
if h != 'encoding_name':
print(f"{h:>15s}", end="")
print()
print("-" * 120)
for i in range(len(configs)):
name = comp.get('encoding_name', [repr(c) for c in configs])[i] if 'encoding_name' in comp else repr(configs[i])
print(f"{str(name):35s}", end="")
for h in headers:
if h != 'encoding_name':
print(f"{str(comp[h][i]):>15s}", end="")
print()
=== IQPEncoding Configuration Comparison ===
n_qubits depth gate_countsingle_qubit_gatestwo_qubit_gatesparameter_counttwo_qubit_ratiogates_per_qubit
------------------------------------------------------------------------------------------------------------------------
IQPEncoding 4 3 26 14 12 100.46153846153846156 6.5
IQPEncoding 4 6 52 28 24 200.46153846153846156 13.0
IQPEncoding 4 6 34 22 12 140.35294117647058826 8.5
IQPEncoding 4 6 40 24 16 16 0.4 10.0
IQPEncoding 8 3 100 44 56 36 0.56 12.5
29. Putting It All Together -- Complete Workflow¶
A realistic end-to-end example: select, configure, analyze, and use IQPEncoding for a quantum machine learning task using the Iris dataset.
from encoding_atlas.core.protocols import ResourceAnalyzable
# Step 1: Choose and configure the encoding
enc = IQPEncoding(n_features=4, reps=2, entanglement="full")
print(f"Encoding: {enc}")
print(f"Qubits: {enc.n_qubits}, Depth: {enc.depth}")
# Step 2: Inspect properties
props = enc.properties
print(f"\nGate count: {props.gate_count}")
print(f"Entangling: {props.is_entangling}")
print(f"Simulable: {props.simulability}")
print(f"Trainability: {props.trainability_estimate}")
# Step 3: Verify capability protocols
print(f"\nResourceAnalyzable: {isinstance(enc, ResourceAnalyzable)}")
print(f"EntanglementQueryable: {isinstance(enc, EntanglementQueryable)}")
# Step 4: Get resource summary
summary = enc.resource_summary()
print(f"\nHardware connectivity needed: {summary['hardware_requirements']['connectivity']}")
print(f"Native gates needed: {summary['hardware_requirements']['native_gates']}")
print(f"Entanglement pairs: {summary['n_entanglement_pairs']}")
Encoding: IQPEncoding(n_features=4, reps=2, entanglement='full') Qubits: 4, Depth: 6 Gate count: 52 Entangling: True Simulable: not_simulable Trainability: 0.7 ResourceAnalyzable: True EntanglementQueryable: True Hardware connectivity needed: all-to-all Native gates needed: ['H', 'RZ', 'CNOT'] Entanglement pairs: 6
# Step 5: Prepare data and generate circuits
from sklearn.datasets import load_iris
from sklearn.preprocessing import MinMaxScaler
# Load Iris dataset (first 4 features)
iris = load_iris()
X_raw = iris.data # shape: (150, 4)
y = iris.target
# Scale features to [0, 2*pi] for optimal encoding range
scaler = MinMaxScaler(feature_range=(0, 2 * np.pi))
X_scaled = scaler.fit_transform(X_raw)
print(f"Dataset: {X_scaled.shape[0]} samples, {X_scaled.shape[1]} features")
print(f"Feature range: [{X_scaled.min():.2f}, {X_scaled.max():.2f}]")
# Generate circuits for all samples
circuits = enc.get_circuits(X_scaled, backend="pennylane")
print(f"Generated {len(circuits)} circuits")
Dataset: 150 samples, 4 features Feature range: [0.00, 6.28] Generated 150 circuits
# Step 6: Simulate and compute quantum kernel
# The quantum kernel K(x, x') = |<psi(x)|psi(x')>|^2 measures similarity
# in the quantum feature space
from encoding_atlas.analysis import simulate_encoding_statevectors_batch
# Compute kernel for a small subset (first 10 samples)
n_subset = 10
X_subset = X_scaled[:n_subset]
# Simulate statevectors
states = simulate_encoding_statevectors_batch(enc, X_subset)
# Compute kernel matrix
kernel_matrix = np.zeros((n_subset, n_subset))
for i in range(n_subset):
for j in range(n_subset):
kernel_matrix[i, j] = compute_fidelity(states[i], states[j])
print(f"Kernel matrix shape: {kernel_matrix.shape}")
print(f"Diagonal (self-fidelity): {np.diag(kernel_matrix)[:5]} (should all be 1.0)")
print(f"Off-diagonal range: [{kernel_matrix[np.triu_indices(n_subset, k=1)].min():.4f}, "
f"{kernel_matrix[np.triu_indices(n_subset, k=1)].max():.4f}]")
print(f"Symmetric: {np.allclose(kernel_matrix, kernel_matrix.T)}")
Kernel matrix shape: (10, 10) Diagonal (self-fidelity): [1. 1. 1. 1. 1.] (should all be 1.0) Off-diagonal range: [0.0005, 0.5981] Symmetric: True
# Step 7: Use the quantum kernel for classification
from sklearn.svm import SVC
from sklearn.model_selection import StratifiedKFold, cross_val_score
# Use a stratified subset of 60 samples (20 per class) for speed
np.random.seed(42)
indices = []
for cls in range(3):
cls_idx = np.where(y == cls)[0]
indices.extend(np.random.choice(cls_idx, size=20, replace=False))
indices = np.array(indices)
X_classify = X_scaled[indices]
y_classify = y[indices]
states_all = simulate_encoding_statevectors_batch(enc, X_classify)
n_classify = len(X_classify)
kernel_full = np.zeros((n_classify, n_classify))
for i in range(n_classify):
for j in range(n_classify):
kernel_full[i, j] = compute_fidelity(states_all[i], states_all[j])
# Use precomputed kernel with SVM
svm = SVC(kernel="precomputed")
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(svm, kernel_full, y_classify, cv=cv)
print(f"=== Quantum Kernel Classification (Iris, {n_classify} samples) ===")
print(f" IQP Encoding: {enc}")
print(f" 5-fold CV accuracy: {scores.mean():.4f} (+/- {scores.std():.4f})")
print(f" Per-fold scores: {scores}")
print()
print(" IQP's entangling structure captures feature interactions,")
print(" potentially providing richer decision boundaries than product-state encodings.")
=== Quantum Kernel Classification (Iris, 60 samples) === IQP Encoding: IQPEncoding(n_features=4, reps=2, entanglement='full') 5-fold CV accuracy: 0.4500 (+/- 0.1354) Per-fold scores: [0.58333333 0.5 0.58333333 0.33333333 0.25 ] IQP's entangling structure captures feature interactions, potentially providing richer decision boundaries than product-state encodings.
Summary¶
This notebook demonstrated every feature of IQPEncoding from the Quantum Encoding Atlas library:
Core Features¶
- Construction with
n_features,reps, andentanglement(full/linear/circular) parameters - Strict validation of all constructor arguments and input data
- Core properties:
n_qubits,depth(= 3 * reps),n_features,config,repr - Lazy, thread-safe properties via
EncodingPropertiesfrozen dataclass - Entanglement pairs: Full (O(n^2)), linear (O(n)), circular (O(n))
Multi-Backend Circuit Generation¶
- PennyLane: Returns callable closure for QNode integration
- Qiskit: Returns
QuantumCircuitobject - Cirq: Returns
cirq.Circuitwith parallel moments
Analysis Capabilities¶
- Resource analysis:
gate_count_breakdown(),resource_summary(),count_resources(),compare_resources(),estimate_execution_time() - Simulability: Always "not_simulable" (provably hard classically)
- Expressibility: High (entangling circuits access more of the Hilbert space)
- Entanglement: Non-zero (ZZ interactions create multi-qubit correlations)
- Trainability: Moderate, decreasing with depth (barren plateau risk)
Low-Level Utilities¶
- Statevector simulation, fidelity, purity, entropy computation
- Partial trace for reduced density matrices
- Random parameter generation and RNG control
Software Engineering Features¶
- Capability protocols:
ResourceAnalyzableandEntanglementQueryable(PEP 544) - Registry system: Factory-style creation via
get_encoding("iqp") - Equality and hashing: Usable in sets and dicts
- Pickle serialization: Full round-trip with lock recreation
- Thread safety: Concurrent circuit generation and property access
- Debug logging: Granular logging for troubleshooting
- Full entanglement warning: UserWarning when n_features > 12
Key Properties of IQPEncoding¶
| Property | Value |
|---|---|
| Qubits | n (one per feature) |
| Depth | 3 * reps |
| Gate count | reps * (2n + 3 * n_pairs) |
| Two-qubit gates | 2 * n_pairs * reps |
| Entangling | Yes |
| Simulable | No (provably hard) |
| Trainability | max(0.3, 0.9 - 0.1 * reps) |
| Hardware connectivity | all-to-all (full), linear, or ring (circular) |