ZZFeatureMap: Complete Feature Demonstration¶
Library: encoding-atlas
Version: 0.2.0
Author: Ashutosh Mishra
This notebook provides an exhaustive, hands-on demonstration of ZZFeatureMap from the Quantum Encoding Atlas library. ZZFeatureMap is a second-order Pauli-Z expansion feature map widely used in quantum kernel methods and Quantum Support Vector Machines (QSVM), following the Qiskit convention for ZZ interactions.
What This Notebook Covers¶
| # | Section | Description |
|---|---|---|
| 1 | Installation & Setup | Installing the library and verifying the environment |
| 2 | Creating a ZZFeatureMap | Constructor parameters, defaults, and validation |
| 3 | Core Properties | n_qubits, depth, n_features, config |
| 4 | Encoding Properties (Lazy, Thread-Safe) | Thread-safe EncodingProperties frozen dataclass |
| 5 | Entanglement Topologies Deep Dive | Full, linear, circular pairs and edge cases |
| 6 | Circuit Depth Analysis | Depth formula, topology comparison, scaling |
| 7 | Circuit Generation — PennyLane | Generating and executing PennyLane circuits |
| 8 | Circuit Generation — Qiskit | Generating and visualizing Qiskit circuits |
| 9 | Circuit Generation — Cirq | Generating and inspecting Cirq circuits |
| 10 | Batch Circuit Generation | get_circuits() — sequential and parallel |
| 11 | Gate Count Breakdown | gate_count_breakdown() — all fields, topology comparison |
| 12 | Resource Summary | resource_summary() — all fields, hardware requirements |
| 13 | Input Validation & Edge Cases | Shape mismatch, NaN/inf, strings, complex, immutability |
| 14 | Resource Analysis Module | count_resources, compare_resources, estimate_execution_time |
| 15 | Simulability Analysis | Classical simulability checks |
| 16 | Expressibility Analysis | Hilbert space coverage measurement |
| 17 | Entanglement Capability | Meyer-Wallach entanglement measure |
| 18 | Trainability Analysis | Barren plateau detection and gradient variance |
| 19 | Low-Level Utilities | Statevector simulation, fidelity, purity, entropy |
| 20 | Capability Protocols | ResourceAnalyzable, EntanglementQueryable, etc. |
| 21 | Registry System | Creating encodings by name via get_encoding() |
| 22 | Equality, Hashing & Serialization | __eq__, __hash__, pickle round-trip |
| 23 | Thread Safety | Concurrent circuit generation |
| 24 | Logging & Debugging | Enabling debug logs for troubleshooting |
| 25 | Visualization & Comparison | Comparing ZZFeatureMap to other encodings |
| 26 | Encoding Recommendation Guide | Using the decision guide |
| 27 | Data Preprocessing Utilities | scale_features, normalize_features |
| 28 | Complete End-to-End Workflow | QSVM-style kernel computation |
| 29 | Summary | Feature checklist table |
Mathematical Background¶
ZZFeatureMap creates quantum states of the form:
$$|\psi(\mathbf{x})\rangle = \left[U_{ZZ}(\mathbf{x}) \cdot H^{\otimes n}\right]^{\text{reps}} |0\rangle^{\otimes n}$$
where $U_{ZZ}$ consists of:
- Single-qubit phase gates: $P(2x_i)$ on each qubit $i$
- Two-qubit ZZ interactions: $\text{CNOT}(i,j) \cdot P(2(\pi - x_i)(\pi - x_j)) \cdot \text{CNOT}(i,j)$ for each pair $(i,j)$
The phase convention $2(\pi - x_i)(\pi - x_j)$ differs from IQP's direct $x_i x_j$ product, creating a different kernel geometry:
$$K(\mathbf{x}, \mathbf{x}') = |\langle\psi(\mathbf{x})|\psi(\mathbf{x}')\rangle|^2$$
Because ZZFeatureMap creates entangled states via two-qubit gates, it is:
- Not classically simulable (exponential resources needed)
- Potentially subject to barren plateaus at high depth
- Hardware-demanding (requires qubit connectivity for CNOT gates)
1. Installation & Setup¶
# Install the library (uncomment if not already installed)
# !pip install encoding-atlas
# For full multi-backend support:
# !pip install encoding-atlas[qiskit,cirq]
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 name, version in backends_available.items():
status = 'Available' if version != 'NOT INSTALLED' else 'Missing'
print(f" {name:12s}: {status} ({version})")
Backend availability: pennylane : Available (0.42.3) qiskit : Available (2.3.0) cirq : Available (1.5.0)
2. Creating a ZZFeatureMap¶
The ZZFeatureMap 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 ZZFeatureMap
# Basic creation with defaults (reps=2, entanglement='full')
enc_default = ZZFeatureMap(n_features=4)
print(f"Default: {enc_default}")
print(f" reps={enc_default.reps}, entanglement={enc_default.entanglement!r}")
print()
# Custom configurations
enc_linear = ZZFeatureMap(n_features=4, reps=3, entanglement='linear')
print(f"Linear: {enc_linear}")
enc_circular = ZZFeatureMap(n_features=4, reps=1, entanglement='circular')
print(f"Circular: {enc_circular}")
enc_full = ZZFeatureMap(n_features=6, reps=2, entanglement='full')
print(f"Full (6 features): {enc_full}")
Default: ZZFeatureMap(n_features=4, reps=2, entanglement='full') reps=2, entanglement='full' Linear: ZZFeatureMap(n_features=4, reps=3, entanglement='linear') Circular: ZZFeatureMap(n_features=4, reps=1, entanglement='circular') Full (6 features): ZZFeatureMap(n_features=6, reps=2, 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 ===")
for bad_n in [0, -1, 1.5, 'abc', None]:
try:
ZZFeatureMap(n_features=bad_n)
print(f" n_features={bad_n!r}: UNEXPECTED SUCCESS")
except (ValueError, TypeError) as e:
print(f" n_features={bad_n!r}: {type(e).__name__}: {e}")
print()
# --- Invalid reps (including bool rejection) ---
print("=== reps validation ===")
for bad_reps in [0, -1, 1.5, True, False]:
try:
ZZFeatureMap(n_features=4, reps=bad_reps)
print(f" reps={bad_reps!r}: UNEXPECTED SUCCESS")
except (ValueError, TypeError) as e:
print(f" reps={bad_reps!r}: {type(e).__name__}: {e}")
print()
# --- Invalid entanglement ---
print("=== entanglement validation ===")
for bad_ent in ['FULL', 'ring', 'star', 'all', 1, None]:
try:
ZZFeatureMap(n_features=4, entanglement=bad_ent)
print(f" entanglement={bad_ent!r}: UNEXPECTED SUCCESS")
except (ValueError, TypeError) as e:
print(f" entanglement={bad_ent!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=1.5: ValueError: n_features must be a positive integer, got 1.5 n_features='abc': ValueError: n_features must be a positive integer, got abc n_features=None: ValueError: n_features must be a positive integer, got None === reps validation === reps=0: ValueError: reps must be a positive integer (>= 1), got 0 reps=-1: ValueError: reps must be a positive integer (>= 1), got -1 reps=1.5: ValueError: reps must be a positive integer, got float 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) === entanglement validation === entanglement='FULL': ValueError: entanglement must be one of ['circular', 'full', 'linear'], got 'FULL' entanglement='ring': ValueError: entanglement must be one of ['circular', 'full', 'linear'], got 'ring' entanglement='star': ValueError: entanglement must be one of ['circular', 'full', 'linear'], got 'star' 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
# --- Large feature count warning (full entanglement with n_features > 10) ---
import warnings
print("=== Large feature warning ===")
# n_features=10: no warning (threshold is >10)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_10 = ZZFeatureMap(n_features=10, entanglement='full')
print(f" n_features=10, full: warnings caught = {len(w)}")
# n_features=11: triggers UserWarning
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_11 = ZZFeatureMap(n_features=11, entanglement='full')
print(f" n_features=11, full: warnings caught = {len(w)}")
if w:
print(f" Warning message: {w[0].message}")
# Linear entanglement: no warning even with many features
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_20_linear = ZZFeatureMap(n_features=20, entanglement='linear')
print(f" n_features=20, linear: warnings caught = {len(w)}")
Large feature count with full entanglement: 11 features, 220 CNOT gates total
=== Large feature warning ===
n_features=10, full: warnings caught = 0
n_features=11, full: warnings caught = 1
Warning message: Full entanglement with 11 features creates 55 ZZ interaction pairs per layer (220 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=20, linear: warnings caught = 0
3. Core Properties¶
ZZFeatureMap exposes several properties inherited from BaseEncoding plus its own attributes.
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
print("=== Core Properties ===")
print(f" n_features : {enc.n_features}")
print(f" n_qubits : {enc.n_qubits}")
print(f" depth : {enc.depth}")
print(f" reps : {enc.reps}")
print(f" entanglement: {enc.entanglement!r}")
print()
# n_qubits always equals n_features for ZZFeatureMap
assert enc.n_qubits == enc.n_features
print(f" n_qubits == n_features: {enc.n_qubits == enc.n_features}")
=== Core Properties === n_features : 4 n_qubits : 4 depth : 22 reps : 2 entanglement: 'full' n_qubits == n_features: True
# The config property returns a copy of the encoding-specific parameters
config = enc.config
print(f"config = {config}")
print(f"type(config) = {type(config).__name__}")
print()
# Verify it's a copy (mutating doesn't affect the encoding)
config['reps'] = 999
assert enc.config['reps'] == 2, "config should be a defensive copy"
print("Mutating config copy does not affect encoding (defensive copy confirmed)")
config = {'reps': 2, 'entanglement': 'full'}
type(config) = dict
Mutating config copy does not affect encoding (defensive copy confirmed)
4. Encoding Properties (Lazy, Thread-Safe)¶
The properties attribute returns an EncodingProperties frozen dataclass. It is computed lazily on first access and cached for subsequent calls. The computation is thread-safe via double-checked locking.
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
props = enc.properties
print(f"type: {type(props).__name__}")
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 n_qubits : 4 depth : 22 gate_count : 52 single_qubit_gates : 28 two_qubit_gates : 24 parameter_count : 20 is_entangling : True simulability : 'not_simulable' trainability_estimate: 0.6499999999999999 notes : 'ZZ Feature Map with full entanglement, 2 rep(s). Qiskit-compatible (pi-x) phase convention. Creates 6 ZZ interactions per layer.'
# The properties object is frozen (immutable)
from dataclasses import FrozenInstanceError
try:
props.n_qubits = 10
print("UNEXPECTED: mutation succeeded")
except FrozenInstanceError as e:
print(f"Frozen dataclass: cannot mutate properties ({type(e).__name__})")
Frozen dataclass: cannot mutate properties (FrozenInstanceError)
# Verify properties are cached (same object returned on second access)
props_1 = enc.properties
props_2 = enc.properties
assert props_1 is props_2, "Properties should be the same cached object"
print(f"Same object returned: {props_1 is props_2}")
print(f"id(props_1) = {id(props_1)}")
print(f"id(props_2) = {id(props_2)}")
Same object returned: True id(props_1) = 2184815454880 id(props_2) = 2184815454880
# to_dict() method
props_dict = props.to_dict()
print("=== properties.to_dict() ===")
for key, value in props_dict.items():
print(f" {key}: {value!r}")
=== properties.to_dict() === n_qubits: 4 depth: 22 gate_count: 52 single_qubit_gates: 28 two_qubit_gates: 24 parameter_count: 20 is_entangling: True simulability: 'not_simulable' expressibility: None entanglement_capability: None trainability_estimate: 0.6499999999999999 noise_resilience_estimate: None notes: 'ZZ Feature Map with full entanglement, 2 rep(s). Qiskit-compatible (pi-x) phase convention. Creates 6 ZZ interactions per layer.'
5. Entanglement Topologies Deep Dive¶
ZZFeatureMap supports three entanglement topologies that determine which qubit pairs receive ZZ interactions:
| Topology | Pairs for n qubits | Description |
|---|---|---|
"full" |
$n(n-1)/2$ | All-to-all connectivity |
"linear" |
$n-1$ | Nearest-neighbor chain |
"circular" |
$n$ (for $n>2$), $n-1$ (for $n \le 2$) | Nearest-neighbor with wrap-around |
print("=== Entanglement Pairs for n_features=4 ===")
for topology in ['full', 'linear', 'circular']:
enc_t = ZZFeatureMap(n_features=4, entanglement=topology)
pairs = enc_t.get_entanglement_pairs()
print(f"\n {topology:8s}: {len(pairs)} pairs")
for p in pairs:
print(f" qubit {p[0]} <-> qubit {p[1]}")
=== Entanglement Pairs for n_features=4 ===
full : 6 pairs
qubit 0 <-> qubit 1
qubit 0 <-> qubit 2
qubit 0 <-> qubit 3
qubit 1 <-> qubit 2
qubit 1 <-> qubit 3
qubit 2 <-> qubit 3
linear : 3 pairs
qubit 0 <-> qubit 1
qubit 1 <-> qubit 2
qubit 2 <-> qubit 3
circular: 4 pairs
qubit 0 <-> qubit 1
qubit 1 <-> qubit 2
qubit 2 <-> qubit 3
qubit 3 <-> qubit 0
# Verify pair count formulas
print("=== Pair count verification ===")
for n in range(2, 8):
full_pairs = len(ZZFeatureMap(n_features=n, entanglement='full').get_entanglement_pairs())
linear_pairs = len(ZZFeatureMap(n_features=n, entanglement='linear').get_entanglement_pairs())
circular_pairs = len(ZZFeatureMap(n_features=n, entanglement='circular').get_entanglement_pairs())
expected_full = n * (n - 1) // 2
expected_linear = n - 1
expected_circular = n if n > 2 else n - 1
assert full_pairs == expected_full
assert linear_pairs == expected_linear
assert circular_pairs == expected_circular
print(f" n={n}: full={full_pairs} (n(n-1)/2={expected_full}), "
f"linear={linear_pairs} (n-1={expected_linear}), "
f"circular={circular_pairs} (expected={expected_circular})")
=== Pair count verification === n=2: full=1 (n(n-1)/2=1), linear=1 (n-1=1), circular=1 (expected=1) n=3: full=3 (n(n-1)/2=3), linear=2 (n-1=2), circular=3 (expected=3) n=4: full=6 (n(n-1)/2=6), linear=3 (n-1=3), circular=4 (expected=4) n=5: full=10 (n(n-1)/2=10), linear=4 (n-1=4), circular=5 (expected=5) n=6: full=15 (n(n-1)/2=15), linear=5 (n-1=5), circular=6 (expected=6) n=7: full=21 (n(n-1)/2=21), linear=6 (n-1=6), circular=7 (expected=7)
# Edge case: n_features=2, circular == linear
print("=== Circular edge case: n_features=2 ===")
enc_lin2 = ZZFeatureMap(n_features=2, entanglement='linear')
enc_circ2 = ZZFeatureMap(n_features=2, entanglement='circular')
print(f" linear pairs: {enc_lin2.get_entanglement_pairs()}")
print(f" circular pairs: {enc_circ2.get_entanglement_pairs()}")
assert enc_lin2.get_entanglement_pairs() == enc_circ2.get_entanglement_pairs()
print(" Confirmed: circular == linear for n=2 (no duplicate wrap-around)")
=== Circular edge case: n_features=2 === linear pairs: [(0, 1)] circular pairs: [(0, 1)] Confirmed: circular == linear for n=2 (no duplicate wrap-around)
6. Circuit Depth Analysis¶
The circuit depth depends on the entanglement topology due to parallelization constraints:
Per repetition:
- Single-qubit layer: depth 2 (H + P in parallel)
- ZZ layer depth:
- Linear: $3 \times (n-1)$ (sequential pairs)
- Circular: $3 \times n$ for $n>2$, $3$ for $n=2$
- Full: $3 \times \chi$ where $\chi$ is the chromatic index ($n-1$ for even $n$, $n$ for odd $n$)
$$\text{depth} = \text{reps} \times (2 + \text{zz\_depth})$$
print("=== Circuit Depth by Topology ===")
print(f"{'n':>3s} {'full':>6s} {'linear':>6s} {'circular':>8s}")
print("-" * 30)
for n in range(2, 9):
d_full = ZZFeatureMap(n_features=n, reps=1, entanglement='full').depth
d_linear = ZZFeatureMap(n_features=n, reps=1, entanglement='linear').depth
d_circular = ZZFeatureMap(n_features=n, reps=1, entanglement='circular').depth
print(f"{n:3d} {d_full:6d} {d_linear:6d} {d_circular:8d}")
=== Circuit Depth by Topology === n full linear circular ------------------------------ 2 5 5 5 3 11 8 11 4 11 11 14 5 17 14 17 6 17 17 20 7 23 20 23 8 23 23 26
# Verify depth formula
print("=== Depth formula verification ===")
for n in range(2, 7):
for reps in [1, 2, 3]:
enc_t = ZZFeatureMap(n_features=n, reps=reps, entanglement='full')
chromatic_index = n if n % 2 == 1 else n - 1
expected = reps * (2 + 3 * chromatic_index)
assert enc_t.depth == expected, f"n={n}, reps={reps}: {enc_t.depth} != {expected}"
print("All depth formula checks passed for full entanglement.")
for n in range(2, 7):
enc_t = ZZFeatureMap(n_features=n, reps=1, entanglement='linear')
expected = 1 * (2 + 3 * (n - 1))
assert enc_t.depth == expected
print("All depth formula checks passed for linear entanglement.")
for n in range(2, 7):
enc_t = ZZFeatureMap(n_features=n, reps=1, entanglement='circular')
n_pairs = n if n > 2 else 1
expected = 1 * (2 + 3 * n_pairs)
assert enc_t.depth == expected
print("All depth formula checks passed for circular entanglement.")
=== Depth formula verification === All depth formula checks passed for full entanglement. All depth formula checks passed for linear entanglement. All depth formula checks passed for circular entanglement.
7. Circuit Generation — PennyLane Backend¶
PennyLane is the default backend. get_circuit() returns a callable (closure) that applies the ZZ Feature Map gates when invoked within a PennyLane QNode context.
import pennylane as qml
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
x = np.array([0.5, 1.0, 1.5, 2.0])
# get_circuit returns a callable
circuit_fn = enc.get_circuit(x, backend='pennylane')
print(f"Type: {type(circuit_fn)}")
print(f"Callable: {callable(circuit_fn)}")
# Use it inside a QNode to get a statevector
dev = qml.device("default.qubit", wires=enc.n_qubits)
@qml.qnode(dev)
def run_circuit(x_input):
circuit_fn = enc.get_circuit(x_input, backend='pennylane')
circuit_fn()
return qml.state()
state = run_circuit(x)
print(f"\nStatevector shape: {state.shape}")
print(f"Statevector (first 4 amplitudes): {state[:4]}")
print(f"Norm: {np.linalg.norm(state):.6f}")
Type: <class 'function'> Callable: True Statevector shape: (16,) Statevector (first 4 amplitudes): [-0.07061671+0.2313706j 0.13409092+0.00157173j -0.18385319-0.12448533j 0.16373653+0.22804342j] Norm: 1.000000
# 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("=== ZZFeatureMap Circuit (4 qubits, reps=2, full) ===")
print(qml.draw(draw_circuit)(x))
=== ZZFeatureMap Circuit (4 qubits, reps=2, full) === 0: ──H──Rϕ(1.00)─╭●────────────╭●─╭●───────────╭●─╭●───────────╭●──H──Rϕ(1.00)──────────────── ··· 1: ──H──Rϕ(2.00)─╰X──Rϕ(11.31)─╰X─│────────────│──│────────────│──╭●───────────╭●─╭●────────── ··· 2: ──H──Rϕ(3.00)──────────────────╰X──Rϕ(8.67)─╰X─│────────────│──╰X──Rϕ(7.03)─╰X─│─────────── ··· 3: ──H──Rϕ(4.00)──────────────────────────────────╰X──Rϕ(6.03)─╰X─────────────────╰X──Rϕ(4.89) ··· 0: ··· ─────────────────╭●────────────╭●────────╭●───────────╭●─╭●───────────╭●──────────────── ··· 1: ··· ─╭●──H──Rϕ(2.00)─╰X──Rϕ(11.31)─╰X────────│────────────│──│────────────│──╭●───────────╭● ··· 2: ··· ─│──╭●───────────╭●──H──────────Rϕ(3.00)─╰X──Rϕ(8.67)─╰X─│────────────│──╰X──Rϕ(7.03)─╰X ··· 3: ··· ─╰X─╰X──Rϕ(3.75)─╰X──H──────────Rϕ(4.00)─────────────────╰X──Rϕ(6.03)─╰X──────────────── ··· 0: ··· ─────────────────────────────────┤ State 1: ··· ─╭●───────────╭●─────────────────┤ State 2: ··· ─│────────────│──╭●───────────╭●─┤ State 3: ··· ─╰X──Rϕ(4.89)─╰X─╰X──Rϕ(3.75)─╰X─┤ State
# With reps=1 for simpler visualization
enc_1rep = ZZFeatureMap(n_features=3, reps=1, entanglement='linear')
dev_3 = qml.device("default.qubit", wires=3)
@qml.qnode(dev_3)
def draw_linear(x_input):
fn = enc_1rep.get_circuit(x_input, backend='pennylane')
fn()
return qml.state()
print("=== ZZFeatureMap Circuit (3 qubits, reps=1, linear) ===")
print(qml.draw(draw_linear)(np.array([0.5, 1.0, 1.5])))
=== ZZFeatureMap Circuit (3 qubits, reps=1, linear) === 0: ──H──Rϕ(1.00)─╭●────────────╭●─────────────────┤ State 1: ──H──Rϕ(2.00)─╰X──Rϕ(11.31)─╰X─╭●───────────╭●─┤ State 2: ──H──Rϕ(3.00)──────────────────╰X──Rϕ(7.03)─╰X─┤ State
8. Circuit Generation — Qiskit Backend¶
The Qiskit backend returns a QuantumCircuit object that can be visualized, transpiled, and executed on Qiskit-compatible simulators and hardware.
enc = ZZFeatureMap(n_features=4, reps=2, 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"Gate count: {qc.size()}")
print()
print(qc.draw(output='text'))
Type: QuantumCircuit
Num qubits: 4
Depth: 31
Gate count: 52
┌───┐┌──────┐ »
q_0: ┤ H ├┤ P(1) ├──■─────────────────■────■─────────────────■────■──»
├───┤├──────┤┌─┴─┐┌───────────┐┌─┴─┐ │ │ │ »
q_1: ┤ H ├┤ P(2) ├┤ X ├┤ P(11.314) ├┤ X ├──┼─────────────────┼────┼──»
├───┤├──────┤└───┘└───────────┘└───┘┌─┴─┐┌───────────┐┌─┴─┐ │ »
q_2: ┤ H ├┤ P(3) ├───────────────────────┤ X ├┤ P(8.6728) ├┤ X ├──┼──»
├───┤├──────┤ └───┘└───────────┘└───┘┌─┴─┐»
q_3: ┤ H ├┤ P(4) ├──────────────────────────────────────────────┤ X ├»
└───┘└──────┘ └───┘»
« ┌───┐┌──────┐ »
«q_0: ────────────────────────────■──┤ H ├┤ P(1) ├───────────────────────»
« │ └───┘└──────┘ ┌───┐»
«q_1: ──────■─────────────────────┼────■─────■───────────────────■──┤ H ├»
« ┌─┴─┐ ┌───────────┐ │ ┌─┴─┐ │ │ └───┘»
«q_2: ────┤ X ├────┤ P(7.0312) ├──┼──┤ X ├───┼───────────────────┼────■──»
« ┌───┴───┴───┐└───────────┘┌─┴─┐└───┘ ┌─┴─┐ ┌───────────┐┌─┴─┐┌─┴─┐»
«q_3: ┤ P(6.0312) ├─────────────┤ X ├──────┤ X ├──┤ P(4.8897) ├┤ X ├┤ X ├»
« └───────────┘ └───┘ └───┘ └───────────┘└───┘└───┘»
« »
«q_0: ───────────────■──────────────────■──────■─────────────────■────■──»
« ┌──────┐ ┌─┴─┐┌───────────┐ ┌─┴─┐ │ │ │ »
«q_1: ───┤ P(2) ├──┤ X ├┤ P(11.314) ├─┤ X ├────┼─────────────────┼────┼──»
« └──────┘ └───┘└───┬───┬───┘┌┴───┴─┐┌─┴─┐┌───────────┐┌─┴─┐ │ »
«q_2: ───────────────■──────┤ H ├────┤ P(3) ├┤ X ├┤ P(8.6728) ├┤ X ├──┼──»
« ┌───────────┐┌─┴─┐ ├───┤ ├──────┤└───┘└───────────┘└───┘┌─┴─┐»
«q_3: ┤ P(3.7481) ├┤ X ├────┤ H ├────┤ P(4) ├───────────────────────┤ X ├»
« └───────────┘└───┘ └───┘ └──────┘ └───┘»
« »
«q_0: ────────────────────────────■───────────────────────────────────»
« │ »
«q_1: ──────■─────────────────────┼────■────■─────────────────■───────»
« ┌─┴─┐ ┌───────────┐ │ ┌─┴─┐ │ │ »
«q_2: ────┤ X ├────┤ P(7.0312) ├──┼──┤ X ├──┼─────────────────┼────■──»
« ┌───┴───┴───┐└───────────┘┌─┴─┐└───┘┌─┴─┐┌───────────┐┌─┴─┐┌─┴─┐»
«q_3: ┤ P(6.0312) ├─────────────┤ X ├─────┤ X ├┤ P(4.8897) ├┤ X ├┤ X ├»
« └───────────┘ └───┘ └───┘└───────────┘└───┘└───┘»
«
«q_0: ──────────────────
«
«q_1: ──────────────────
«
«q_2: ───────────────■──
« ┌───────────┐┌─┴─┐
«q_3: ┤ P(3.7481) ├┤ X ├
« └───────────┘└───┘
# Linear entanglement - simpler circuit
enc_lin = ZZFeatureMap(n_features=4, reps=1, entanglement='linear')
qc_lin = enc_lin.get_circuit(x, backend='qiskit')
print("=== Linear entanglement (reps=1) ===")
print(qc_lin.draw(output='text'))
=== Linear entanglement (reps=1) ===
┌───┐┌──────┐ »
q_0: ┤ H ├┤ P(1) ├──■─────────────────■──────────────────────────────»
├───┤├──────┤┌─┴─┐┌───────────┐┌─┴─┐ »
q_1: ┤ H ├┤ P(2) ├┤ X ├┤ P(11.314) ├┤ X ├──■─────────────────■───────»
├───┤├──────┤└───┘└───────────┘└───┘┌─┴─┐┌───────────┐┌─┴─┐ »
q_2: ┤ H ├┤ P(3) ├───────────────────────┤ X ├┤ P(7.0312) ├┤ X ├──■──»
├───┤├──────┤ └───┘└───────────┘└───┘┌─┴─┐»
q_3: ┤ H ├┤ P(4) ├──────────────────────────────────────────────┤ X ├»
└───┘└──────┘ └───┘»
«
«q_0: ──────────────────
«
«q_1: ──────────────────
«
«q_2: ───────────────■──
« ┌───────────┐┌─┴─┐
«q_3: ┤ P(3.7481) ├┤ X ├
« └───────────┘└───┘
# Circular entanglement
enc_circ = ZZFeatureMap(n_features=4, reps=1, entanglement='circular')
qc_circ = enc_circ.get_circuit(x, backend='qiskit')
print("=== Circular entanglement (reps=1) ===")
print(qc_circ.draw(output='text'))
=== Circular entanglement (reps=1) ===
┌───┐┌──────┐ »
q_0: ┤ H ├┤ P(1) ├──■─────────────────■──────────────────────────────»
├───┤├──────┤┌─┴─┐┌───────────┐┌─┴─┐ »
q_1: ┤ H ├┤ P(2) ├┤ X ├┤ P(11.314) ├┤ X ├──■─────────────────■───────»
├───┤├──────┤└───┘└───────────┘└───┘┌─┴─┐┌───────────┐┌─┴─┐ »
q_2: ┤ H ├┤ P(3) ├───────────────────────┤ X ├┤ P(7.0312) ├┤ X ├──■──»
├───┤├──────┤ └───┘└───────────┘└───┘┌─┴─┐»
q_3: ┤ H ├┤ P(4) ├──────────────────────────────────────────────┤ X ├»
└───┘└──────┘ └───┘»
« ┌───┐┌───────────┐┌───┐
«q_0: ──────────────────┤ X ├┤ P(6.0312) ├┤ X ├
« └─┬─┘└───────────┘└─┬─┘
«q_1: ────────────────────┼─────────────────┼──
« │ │
«q_2: ───────────────■────┼─────────────────┼──
« ┌───────────┐┌─┴─┐ │ │
«q_3: ┤ P(3.7481) ├┤ X ├──■─────────────────■──
« └───────────┘└───┘
import cirq
enc = ZZFeatureMap(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"Moments: {len(cirq_circuit.moments)}")
print()
print(cirq_circuit)
Type: Circuit
Moments: 17
┌──┐ ┌──┐
0: ───H───Rz(0.318π)───@─────────────────@───@────────────────@────@───────────────────@─────────────────────────────────────────────
│ │ │ │ │ │
1: ───H───Rz(0.637π)───X───Rz(-0.399π)───X───┼────────────────┼────┼@──────────────────┼@────@───────────────@───────────────────────
│ │ ││ ││ │ │
2: ───H───Rz(0.955π)─────────────────────────X───Rz(-1.24π)───X────┼X────Rz(-1.76π)────┼X────┼───────────────┼───@───────────────@───
│ │ │ │ │ │
3: ───H───Rz(1.27π)────────────────────────────────────────────────X─────Rz(1.92π)─────X─────X───Rz(1.56π)───X───X───Rz(1.19π)───X───
└──┘ └──┘
# Simulate the Cirq circuit to get the statevector
simulator = cirq.Simulator()
result = simulator.simulate(cirq_circuit)
print(f"Statevector (first 4 amplitudes):")
for i in range(4):
print(f" |{i:04b}> = {result.final_state_vector[i]:.6f}")
print(f"Norm: {np.linalg.norm(result.final_state_vector):.6f}")
Statevector (first 4 amplitudes): |0000> = 0.189428-0.163147j |0001> = 0.157043-0.194518j |0010> = -0.242228+0.061851j |0011> = 0.018168+0.249339j Norm: 1.000000
10. Batch Circuit Generation¶
get_circuits() generates circuits for multiple data samples at once, with optional parallel processing via ThreadPoolExecutor.
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
np.random.seed(42)
X_batch = np.random.uniform(0, 2 * np.pi, size=(20, 4))
# Sequential processing (default)
circuits_seq = enc.get_circuits(X_batch, backend='pennylane')
print(f"Sequential: {len(circuits_seq)} circuits, all callable: {all(callable(c) for c in circuits_seq)}")
# Parallel processing
circuits_par = enc.get_circuits(X_batch, backend='pennylane', parallel=True)
print(f"Parallel: {len(circuits_par)} circuits, all callable: {all(callable(c) for c in circuits_par)}")
Sequential: 20 circuits, all callable: True Parallel: 20 circuits, all callable: True
# 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='pennylane')
print(f"1D input -> {len(circuits_1d)} circuit(s)")
assert len(circuits_1d) == 1
1D input -> 1 circuit(s)
# Timing comparison: sequential vs parallel
import time
X_large = np.random.uniform(0, 2 * np.pi, size=(100, 4))
start = time.perf_counter()
_ = enc.get_circuits(X_large, backend='qiskit', parallel=False)
t_seq = time.perf_counter() - start
start = time.perf_counter()
_ = enc.get_circuits(X_large, backend='qiskit', parallel=True)
t_par = time.perf_counter() - start
print(f"Sequential: {t_seq:.4f}s")
print(f"Parallel: {t_par:.4f}s")
print(f"Speedup: {t_seq / t_par:.2f}x")
Sequential: 0.0635s Parallel: 0.0742s Speedup: 0.86x
# Verify order is preserved with parallel processing
enc_order = ZZFeatureMap(n_features=2, reps=1, entanglement='full')
X_test = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
circuits_seq = enc_order.get_circuits(X_test, backend='qiskit', parallel=False)
circuits_par = enc_order.get_circuits(X_test, backend='qiskit', parallel=True)
from qiskit.quantum_info import Statevector
for i in range(len(X_test)):
sv_seq = Statevector.from_instruction(circuits_seq[i])
sv_par = Statevector.from_instruction(circuits_par[i])
match = np.allclose(sv_seq.data, sv_par.data)
print(f" Sample {i}: sequential == parallel -> {match}")
assert match
print("Order preservation confirmed.")
Sample 0: sequential == parallel -> True Sample 1: sequential == parallel -> True Sample 2: sequential == parallel -> True Order preservation confirmed.
11. Gate Count Breakdown¶
gate_count_breakdown() returns a GateCountBreakdown TypedDict with counts for every gate type.
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
breakdown = enc.gate_count_breakdown()
print("=== gate_count_breakdown() ===")
print(f" hadamard : {breakdown['hadamard']}")
print(f" phase_single : {breakdown['phase_single']}")
print(f" phase_zz : {breakdown['phase_zz']}")
print(f" cnot : {breakdown['cnot']}")
print(f" total_single_qubit: {breakdown['total_single_qubit']}")
print(f" total_two_qubit : {breakdown['total_two_qubit']}")
print(f" total : {breakdown['total']}")
print()
# Verify aggregation
assert breakdown['total_single_qubit'] == breakdown['hadamard'] + breakdown['phase_single'] + breakdown['phase_zz']
assert breakdown['total_two_qubit'] == breakdown['cnot']
assert breakdown['total'] == breakdown['total_single_qubit'] + breakdown['total_two_qubit']
print("Aggregation checks passed.")
=== gate_count_breakdown() === hadamard : 8 phase_single : 8 phase_zz : 12 cnot : 24 total_single_qubit: 28 total_two_qubit : 24 total : 52 Aggregation checks passed.
# Compare gate counts across topologies
print("=== Gate Count Comparison (n_features=6, reps=2) ===")
print(f"{'Topology':>10s} {'H':>4s} {'P_single':>8s} {'P_zz':>5s} {'CNOT':>5s} {'Total':>6s}")
print("-" * 48)
for topology in ['full', 'linear', 'circular']:
enc_t = ZZFeatureMap(n_features=6, reps=2, entanglement=topology)
b = enc_t.gate_count_breakdown()
print(f"{topology:>10s} {b['hadamard']:4d} {b['phase_single']:8d} {b['phase_zz']:5d} {b['cnot']:5d} {b['total']:6d}")
=== Gate Count Comparison (n_features=6, reps=2) ===
Topology H P_single P_zz CNOT Total
------------------------------------------------
full 12 12 30 60 114
linear 12 12 10 20 54
circular 12 12 12 24 60
12. Resource Summary¶
resource_summary() generates a comprehensive resource summary including qubit requirements, circuit depth, gate counts, entanglement information, hardware requirements, and Qiskit compatibility.
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
summary = enc.resource_summary()
print("=== resource_summary() ===")
for key, value in summary.items():
if key == 'gate_counts':
print(f" {key}:")
for gk, gv in value.items():
print(f" {gk}: {gv}")
elif key == 'hardware_requirements':
print(f" {key}:")
for hk, hv in value.items():
print(f" {hk}: {hv}")
elif key == 'entanglement_pairs':
print(f" {key}: {value[:3]}... ({len(value)} total)")
else:
print(f" {key}: {value!r}")
=== resource_summary() ===
n_qubits: 4
n_features: 4
depth: 22
reps: 2
entanglement: 'full'
entanglement_pairs: [(0, 1), (0, 2), (0, 3)]... (6 total)
n_entanglement_pairs: 6
gate_counts:
hadamard: 8
phase_single: 8
phase_zz: 12
cnot: 24
total_single_qubit: 28
total_two_qubit: 24
total: 52
is_entangling: True
simulability: 'not_simulable'
trainability_estimate: 0.6499999999999999
phase_convention: '2(π - xᵢ)(π - xⱼ) for ZZ interactions'
qiskit_compatible: True
hardware_requirements:
connectivity: all-to-all
native_gates: ['H', 'P', 'CNOT']
min_two_qubit_gate_fidelity: 0.99
# Hardware requirements depend on topology
print("=== Hardware Connectivity by Topology ===")
for topology in ['full', 'linear', 'circular']:
enc_t = ZZFeatureMap(n_features=4, reps=2, entanglement=topology)
s = enc_t.resource_summary()
print(f" {topology:8s}: connectivity={s['hardware_requirements']['connectivity']!r}, "
f"native_gates={s['hardware_requirements']['native_gates']}, "
f"is_entangling={s['is_entangling']}, "
f"qiskit_compatible={s['qiskit_compatible']}")
# Phase convention
print(f"\nPhase convention: {summary['phase_convention']!r}")
=== Hardware Connectivity by Topology === full : connectivity='all-to-all', native_gates=['H', 'P', 'CNOT'], is_entangling=True, qiskit_compatible=True linear : connectivity='linear', native_gates=['H', 'P', 'CNOT'], is_entangling=True, qiskit_compatible=True circular: connectivity='ring', native_gates=['H', 'P', 'CNOT'], is_entangling=True, qiskit_compatible=True Phase convention: '2(π - xᵢ)(π - xⱼ) for ZZ interactions'
13. Input Validation & Edge Cases¶
ZZFeatureMap performs thorough input validation. This section demonstrates every validation pathway.
enc = ZZFeatureMap(n_features=4, reps=2)
# --- Shape validation ---
print("=== Shape validation ===")
# Wrong number of features
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3])) # 3 features, expected 4
except ValueError as e:
print(f" Wrong features: {e}")
# Wrong 2D shape
try:
enc.get_circuit(np.array([[0.1, 0.2]])) # 2 features, expected 4
except ValueError as e:
print(f" Wrong 2D shape: {e}")
=== Shape validation === Wrong features: Expected 4 features, got 3 Wrong 2D shape: Expected 4 features, got 2
# --- 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}")
# Infinite 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
try:
enc.get_circuit(np.array([0.1 + 0j, 0.2 + 0j, 0.3 + 0j, 0.4 + 0j]))
except (ValueError, TypeError) as e:
print(f" Complex: {type(e).__name__}: {e}")
# String inputs
try:
enc.get_circuit(['a', 'b', 'c', 'd'])
except (ValueError, TypeError) as e:
print(f" Strings: {type(e).__name__}: {e}")
# String-dtype numpy array
try:
enc.get_circuit(np.array(['0.1', '0.2', '0.3', '0.4']))
except (ValueError, TypeError) as e:
print(f" String array: {type(e).__name__}: {e}")
=== Type validation === Complex: TypeError: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only. Strings: TypeError: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding. String array: TypeError: Input array has non-numeric dtype '<U3'. Expected numeric data (float or int).
# --- Accepted 2D input ---
print("=== Accepted 2D input ===")
x_2d = np.array([[0.1, 0.2, 0.3, 0.4]])
circuit_2d = enc.get_circuit(x_2d, backend='pennylane')
print(f" 2D input shape {x_2d.shape}: callable={callable(circuit_2d)}")
# Python list input
circuit_list = enc.get_circuit([0.1, 0.2, 0.3, 0.4], backend='pennylane')
print(f" Python list: callable={callable(circuit_list)}")
=== Accepted 2D input === 2D input shape (1, 4): callable=True Python list: callable=True
# --- Defensive copy (immutability) demonstration ---
print("=== Defensive copy (input isolation) ===")
x_original = np.array([0.5, 1.0, 1.5, 2.0])
qc1 = enc.get_circuit(x_original.copy(), backend='qiskit')
# Modify the original array
x_modified = x_original.copy()
x_modified[0] = 999.0
qc2 = enc.get_circuit(x_original.copy(), backend='qiskit')
# The original circuit is not affected
from qiskit.quantum_info import Statevector
sv1 = Statevector.from_instruction(qc1)
sv2 = Statevector.from_instruction(qc2)
print(f" Circuits match after input mutation: {np.allclose(sv1.data, sv2.data)}")
=== Defensive copy (input isolation) === Circuits match after input mutation: True
14. Resource Analysis Module¶
The encoding_atlas.analysis module provides standalone resource analysis functions.
from encoding_atlas.analysis import (
count_resources,
get_resource_summary,
get_gate_breakdown,
compare_resources,
estimate_execution_time,
)
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
# count_resources()
res_count = count_resources(enc)
print("=== count_resources() ===")
print(f" type: {type(res_count).__name__}")
for key, value in res_count.items():
print(f" {key}: {value}")
=== count_resources() === type: dict n_qubits: 4 depth: 22 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: 0 two_qubit_ratio: 0.46153846153846156 gates_per_qubit: 13.0 encoding_name: ZZFeatureMap is_data_dependent: False
# get_gate_breakdown()
gate_bd = get_gate_breakdown(enc)
print("=== get_gate_breakdown() ===")
for key, value in gate_bd.items():
print(f" {key}: {value}")
=== get_gate_breakdown() === rx: 0 ry: 0 rz: 0 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: ZZFeatureMap
# compare_resources() across different configurations
from encoding_atlas import AngleEncoding, IQPEncoding
encodings_to_compare = [
ZZFeatureMap(n_features=4, reps=2, entanglement='full'),
ZZFeatureMap(n_features=4, reps=2, entanglement='linear'),
AngleEncoding(n_features=4, reps=2),
IQPEncoding(n_features=4, reps=2),
]
comparison = compare_resources(encodings_to_compare)
print("=== compare_resources() ===")
for key, value in comparison.items():
print(f" {key}: {value}")
=== compare_resources() === n_qubits: [4, 4, 4, 4] depth: [22, 22, 2, 6] gate_count: [52, 34, 8, 52] single_qubit_gates: [28, 22, 8, 28] two_qubit_gates: [24, 12, 0, 24] parameter_count: [20, 14, 8, 20] two_qubit_ratio: [0.46153846153846156, 0.35294117647058826, 0.0, 0.46153846153846156] gates_per_qubit: [13.0, 8.5, 2.0, 13.0] encoding_name: ['ZZFeatureMap', 'ZZFeatureMap', 'AngleEncoding', 'IQPEncoding']
# estimate_execution_time()
exec_time = estimate_execution_time(
enc,
single_qubit_gate_time_us=0.02,
two_qubit_gate_time_us=0.2,
)
print("=== estimate_execution_time() ===")
for key, value in exec_time.items():
print(f" {key}: {value}")
=== estimate_execution_time() === serial_time_us: 6.360000000000001 estimated_time_us: 5.4 single_qubit_time_us: 0.56 two_qubit_time_us: 4.800000000000001 measurement_time_us: 1.0 parallelization_factor: 0.5
15. Simulability Analysis¶
ZZFeatureMap creates entangled states via ZZ interactions, making it not classically simulable in general. This contrasts with product-state encodings like AngleEncoding.
from encoding_atlas.analysis import (
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
)
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
result = check_simulability(enc)
print("=== check_simulability() ===")
print(f" is_simulable : {result['is_simulable']}")
print(f" simulability_class: {result['simulability_class']!r}")
print(f" reason : {result['reason']!r}")
print(f" details : {result['details']}")
print(f" recommendations :")
for rec in result['recommendations']:
print(f" - {rec}")
=== check_simulability() ===
is_simulable : False
simulability_class: 'not_simulable'
reason : 'High entanglement circuit with 24 two-qubit gates and non-Clifford operations'
details : {'is_entangling': True, 'is_clifford': False, 'is_matchgate': False, 'entanglement_pattern': 'full', 'two_qubit_gate_count': 24, 'n_qubits': 4, 'n_features': 4, 'declared_simulability': 'not_simulable', 'encoding_name': 'ZZFeatureMap', 'has_non_clifford_gates': True, 'has_t_gates': False, 'has_parameterized_rotations': True}
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
# Quick one-line reason
reason = get_simulability_reason(enc)
print(f"Quick reason: {reason}")
# Clifford check (ZZFeatureMap uses non-Clifford P gates)
print(f"Is Clifford circuit: {is_clifford_circuit(enc)}")
# Matchgate check
print(f"Is matchgate circuit: {is_matchgate_circuit(enc)}")
Quick reason: Not simulable: High entanglement circuit with 24 two-qubit gates and non-Clifford operations Is Clifford circuit: False Is matchgate circuit: False
# Compare simulability across topologies
print("=== Simulability by Topology ===")
for topology in ['full', 'linear', 'circular']:
enc_t = ZZFeatureMap(n_features=4, reps=2, entanglement=topology)
sim_result = check_simulability(enc_t)
print(f" {topology:8s}: is_simulable={sim_result['is_simulable']}, "
f"class={sim_result['simulability_class']!r}")
=== Simulability by Topology === full : is_simulable=False, class='not_simulable' linear : is_simulable=False, class='conditionally_simulable' circular: is_simulable=False, class='conditionally_simulable'
16. Expressibility Analysis¶
Expressibility measures how well an encoding can explore the Hilbert space compared to a Haar-random distribution. Lower KL divergence = more expressible.
from encoding_atlas.analysis import compute_expressibility
enc = ZZFeatureMap(n_features=2, reps=2, entanglement='full')
# Simple scalar result
expr_value = compute_expressibility(enc, n_samples=100, seed=42)
print(f"Expressibility (KL divergence): {expr_value:.6f}")
print(f" Lower = more expressible")
Expressibility (KL divergence): 0.957656 Lower = more expressible
# Detailed result with distributions
expr_detailed = compute_expressibility(
enc,
n_samples=100,
seed=42,
return_distributions=True,
)
print("=== Detailed Expressibility ===")
print(f" type: {type(expr_detailed).__name__}")
print(f" keys: {list(expr_detailed.keys())}")
print(f" expressibility: {expr_detailed['expressibility']:.6f}")
print(f" kl_divergence: {expr_detailed['kl_divergence']:.6f}")
print(f" fidelity_distribution shape: {np.array(expr_detailed['fidelity_distribution']).shape}")
print(f" haar_distribution shape: {np.array(expr_detailed['haar_distribution']).shape}")
print(f" convergence_estimate: {expr_detailed['convergence_estimate']:.6f}")
print(f" mean_fidelity: {expr_detailed['mean_fidelity']:.6f}")
print(f" std_fidelity: {expr_detailed['std_fidelity']:.6f}")
=== Detailed Expressibility === type: dict keys: ['expressibility', 'kl_divergence', 'fidelity_distribution', 'haar_distribution', 'bin_edges', 'n_samples', 'n_bins', 'convergence_estimate', 'mean_fidelity', 'std_fidelity'] expressibility: 0.957656 kl_divergence: 0.423442 fidelity_distribution shape: (75,) haar_distribution shape: (75,) convergence_estimate: 0.082741 mean_fidelity: 0.262348 std_fidelity: 0.227002
# Compare expressibility across reps
print("=== Expressibility vs reps ===")
for reps in [1, 2, 3]:
enc_r = ZZFeatureMap(n_features=2, reps=reps, entanglement='full')
expr = compute_expressibility(enc_r, n_samples=100, seed=42)
print(f" reps={reps}: KL divergence = {expr:.6f}")
=== Expressibility vs reps === reps=1: KL divergence = 0.961075 reps=2: KL divergence = 0.957656 reps=3: KL divergence = 0.962183
17. Entanglement Capability¶
ZZFeatureMap creates entangled states. The entanglement capability is measured using the Meyer-Wallach entanglement measure, which quantifies bipartite entanglement averaged over all qubit bipartitions.
from encoding_atlas.analysis import (
compute_entanglement_capability,
compute_meyer_wallach,
)
enc = ZZFeatureMap(n_features=2, reps=2, entanglement='full')
# Simple scalar result
ent_value = compute_entanglement_capability(enc, n_samples=100, seed=42)
print(f"Entanglement capability: {ent_value:.6f}")
print(f" 0 = no entanglement, 1 = maximum entanglement")
assert ent_value > 0, "ZZFeatureMap should produce non-zero entanglement"
Entanglement capability: 0.274076 0 = no entanglement, 1 = maximum entanglement
# Detailed result
ent_detailed = compute_entanglement_capability(
enc, n_samples=100, seed=42, return_details=True
)
print("=== Detailed Entanglement ===")
print(f" type: {type(ent_detailed).__name__}")
print(f" keys: {list(ent_detailed.keys())}")
print(f" entanglement_capability: {ent_detailed['entanglement_capability']:.6f}")
print(f" std_error: {ent_detailed['std_error']:.6f}")
print(f" n_samples: {ent_detailed['n_samples']}")
print(f" measure: {ent_detailed['measure']}")
samples = np.array(ent_detailed['entanglement_samples'])
print(f" samples shape: {samples.shape}")
print(f" samples min: {samples.min():.6f}")
print(f" samples max: {samples.max():.6f}")
per_qubit = np.array(ent_detailed['per_qubit_entanglement'])
print(f" per_qubit_entanglement: {per_qubit}")
=== Detailed Entanglement === type: dict keys: ['entanglement_capability', 'entanglement_samples', 'std_error', 'n_samples', 'per_qubit_entanglement', 'measure', 'scott_k'] entanglement_capability: 0.274076 std_error: 0.024495 n_samples: 100 measure: meyer_wallach samples shape: (100,) samples min: 0.000092 samples max: 0.967544 per_qubit_entanglement: [0.13703779 0.13703779]
# Verify with direct Meyer-Wallach on a specific state
from encoding_atlas.analysis import simulate_encoding_statevector
x_test = np.array([0.5, 1.0])
state = simulate_encoding_statevector(enc, x_test)
mw = compute_meyer_wallach(state, n_qubits=2)
print(f"Meyer-Wallach for x={x_test}: {mw:.6f}")
assert mw > 0, "ZZ states should be entangled for non-trivial inputs"
Meyer-Wallach for x=[0.5 1. ]: 0.045995
# Compare entanglement across topologies
print("=== Entanglement by Topology (n_features=3) ===")
for topology in ['full', 'linear', 'circular']:
enc_t = ZZFeatureMap(n_features=3, reps=2, entanglement=topology)
ent = compute_entanglement_capability(enc_t, n_samples=50, seed=42)
print(f" {topology:8s}: {ent:.6f}")
=== Entanglement by Topology (n_features=3) ===
C:\Users\ashut\AppData\Local\Temp\ipykernel_39196\3129257047.py:6: UserWarning: n_samples=50 is low. For reliable entanglement capability estimates, use at least 100 samples. ent = compute_entanglement_capability(enc_t, n_samples=50, seed=42)
full : 0.609730 linear : 0.476895 circular: 0.609730
18. Trainability Analysis¶
Trainability estimates susceptibility to barren plateaus. Higher trainability means larger gradient variance and easier optimization.
from encoding_atlas.analysis import estimate_trainability, compute_gradient_variance, detect_barren_plateau
enc = ZZFeatureMap(n_features=3, reps=2, entanglement='full')
# Simple scalar result
train_value = estimate_trainability(enc, n_samples=100, seed=42)
print(f"Trainability estimate: {train_value:.6f}")
# Theoretical estimate from properties
props = enc.properties
print(f"Theoretical estimate (from properties): {props.trainability_estimate}")
print(f" Formula: max(0.3, 0.85 - 0.1 * reps) = max(0.3, 0.85 - 0.1 * {enc.reps}) = {max(0.3, 0.85 - 0.1 * enc.reps)}")
Trainability estimate: 0.116933 Theoretical estimate (from properties): 0.6499999999999999 Formula: max(0.3, 0.85 - 0.1 * reps) = max(0.3, 0.85 - 0.1 * 2) = 0.6499999999999999
# Detailed result
train_detailed = estimate_trainability(
enc, n_samples=100, seed=42, return_details=True
)
print("=== Detailed Trainability ===")
print(f" type: {type(train_detailed).__name__}")
print(f" keys: {list(train_detailed.keys())}")
print(f" trainability_estimate: {train_detailed['trainability_estimate']:.6f}")
print(f" gradient_variance: {train_detailed['gradient_variance']:.6f}")
print(f" barren_plateau_risk: {train_detailed['barren_plateau_risk']}")
print(f" effective_dimension: {train_detailed['effective_dimension']}")
print(f" n_successful_samples: {train_detailed['n_successful_samples']}")
ppv = np.array(train_detailed['per_parameter_variance'])
print(f" per_parameter_variance shape: {ppv.shape}")
print(f" per_parameter_variance mean: {ppv.mean():.6f}")
=== Detailed Trainability === type: dict keys: ['trainability_estimate', 'gradient_variance', 'barren_plateau_risk', 'effective_dimension', 'n_samples', 'n_successful_samples', 'per_parameter_variance', 'n_failed_samples'] trainability_estimate: 0.116933 gradient_variance: 0.009752 barren_plateau_risk: low effective_dimension: 3.0 n_successful_samples: 100 per_parameter_variance shape: (3,) per_parameter_variance mean: 0.009752
# Gradient variance and barren plateau detection
grad_var = compute_gradient_variance(enc, n_samples=100, seed=42)
print(f"Gradient variance: {grad_var:.8f}")
bp_risk = detect_barren_plateau(
gradient_variance=grad_var,
n_qubits=enc.n_qubits,
n_params=enc.properties.parameter_count,
)
print()
print("=== Barren Plateau Detection ===")
print(f" risk level: {bp_risk}")
print(" (returns one of: low, medium, high)")
# Interpretation
if bp_risk == "low":
print(" -> Gradient variance is healthy; training should work well.")
elif bp_risk == "medium":
print(" -> Borderline variance; may need careful hyperparameter tuning.")
else:
print(" -> Very low variance; likely barren plateau, training may fail.")
Gradient variance: 0.00975219 === Barren Plateau Detection === risk level: low (returns one of: low, medium, high) -> Gradient variance is healthy; training should work well.
# Trainability estimate scaling with reps
print("=== Trainability Estimate vs Reps ===")
for reps in [1, 2, 3, 5, 8]:
enc_r = ZZFeatureMap(n_features=3, reps=reps, entanglement='full')
te = enc_r.properties.trainability_estimate
print(f" reps={reps}: trainability_estimate = {te:.2f} "
f"(max(0.3, 0.85 - 0.1*{reps}) = {max(0.3, 0.85 - 0.1*reps):.2f})")
=== Trainability Estimate vs Reps === reps=1: trainability_estimate = 0.75 (max(0.3, 0.85 - 0.1*1) = 0.75) reps=2: trainability_estimate = 0.65 (max(0.3, 0.85 - 0.1*2) = 0.65) reps=3: trainability_estimate = 0.55 (max(0.3, 0.85 - 0.1*3) = 0.55) reps=5: trainability_estimate = 0.35 (max(0.3, 0.85 - 0.1*5) = 0.35) reps=8: trainability_estimate = 0.30 (max(0.3, 0.85 - 0.1*8) = 0.30)
19. 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,
partial_trace_single_qubit,
partial_trace_subsystem,
compute_purity,
compute_linear_entropy,
compute_von_neumann_entropy,
validate_encoding_for_analysis,
validate_statevector,
generate_random_parameters,
create_rng,
)
# --- Statevector simulation ---
enc = ZZFeatureMap(n_features=3, reps=1, entanglement='full')
x = np.array([0.5, 1.0, 1.5])
state = simulate_encoding_statevector(enc, x)
print("=== Statevector ===")
print(f" Shape: {state.shape}")
print(f" Norm: {np.linalg.norm(state):.6f}")
print(f" Amplitudes:")
for i, amp in enumerate(state):
if abs(amp) > 1e-6:
print(f" |{i:03b}> = {amp:.6f} (prob = {abs(amp)**2:.6f})")
=== Statevector ===
Shape: (8,)
Norm: 1.000000
Amplitudes:
|000> = 0.353553+0.000000j (prob = 0.125000)
|001> = 0.349819-0.051251j (prob = 0.125000)
|010> = 0.026377+0.352568j (prob = 0.125000)
|011> = 0.349819-0.051251j (prob = 0.125000)
|100> = -0.189870+0.298244j (prob = 0.125000)
|101> = -0.331566-0.122735j (prob = 0.125000)
|110> = 0.349819-0.051251j (prob = 0.125000)
|111> = 0.339471-0.098788j (prob = 0.125000)
# --- Batch statevector 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)
print("=== Batch Statevectors ===")
for i, st in enumerate(states):
print(f" Sample {i}: norm={np.linalg.norm(st):.6f}, "
f"max_prob={max(abs(st)**2):.6f}")
=== Batch Statevectors === Sample 0: norm=1.000000, max_prob=0.125000 Sample 1: norm=1.000000, max_prob=0.125000 Sample 2: norm=1.000000, max_prob=0.125000
# --- 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
state3 = simulate_encoding_statevector(enc, np.array([2.0, 2.5, 3.0])) # different
print("=== Fidelity ===")
print(f" F(state1, state1) = {compute_fidelity(state1, state1):.6f} (self)")
print(f" F(state1, state2) = {compute_fidelity(state1, state2):.6f} (identical)")
print(f" F(state1, state3) = {compute_fidelity(state1, state3):.6f} (different)")
=== Fidelity === F(state1, state1) = 1.000000 (self) F(state1, state2) = 1.000000 (identical) F(state1, state3) = 0.004350 (different)
# --- Partial trace and reduced density matrix ---
# For entangled states, tracing out qubits gives mixed reduced states
enc_2q = ZZFeatureMap(n_features=2, reps=2, entanglement='full')
state_2q = simulate_encoding_statevector(enc_2q, np.array([0.5, 1.5]))
# Trace out qubit 1, keep qubit 0
rho_0 = partial_trace_single_qubit(state_2q, n_qubits=2, keep_qubit=0)
print("=== Partial Trace (keep qubit 0) ===")
print(f" rho_0 shape: {rho_0.shape}")
print(f" rho_0:\n{rho_0}")
print(f" Purity: {compute_purity(rho_0):.6f} (1.0 = pure, 0.5 = maximally mixed for 1 qubit)")
print(f" Linear entropy: {compute_linear_entropy(rho_0):.6f}")
print(f" Von Neumann entropy: {compute_von_neumann_entropy(rho_0):.6f}")
=== Partial Trace (keep qubit 0) === rho_0 shape: (2, 2) rho_0: [[0.30269098+1.61736324e-18j 0.0628355 +3.78456761e-01j] [0.0628355 -3.78456761e-01j 0.69730902-8.99529787e-18j]] Purity: 0.872217 (1.0 = pure, 0.5 = maximally mixed for 1 qubit) Linear entropy: 0.127783 Von Neumann entropy: 0.360665
# --- Partial trace for subsystem ---
enc_3q = ZZFeatureMap(n_features=3, reps=1, entanglement='full')
state_3q = simulate_encoding_statevector(enc_3q, np.array([0.5, 1.0, 1.5]))
rho_01 = partial_trace_subsystem(state_3q, n_qubits=3, keep_qubits=[0, 1])
print("=== Partial Trace (keep qubits 0,1) ===")
print(f" rho_01 shape: {rho_01.shape}")
print(f" Purity: {compute_purity(rho_01):.6f}")
print(f" Von Neumann entropy: {compute_von_neumann_entropy(rho_01):.6f}")
=== Partial Trace (keep qubits 0,1) === rho_01 shape: (4, 4) Purity: 0.643308 Von Neumann entropy: 0.782030
# --- Validation utilities ---
print("=== validate_encoding_for_analysis() ===")
validate_encoding_for_analysis(enc) # Should not raise
print(" Encoding validated successfully.")
print("\n=== validate_statevector() ===")
validated = validate_statevector(state_3q)
print(f" Validated statevector shape: {validated.shape}")
print(f" Norm: {np.linalg.norm(validated):.6f}")
=== validate_encoding_for_analysis() === Encoding validated successfully. === validate_statevector() === Validated statevector shape: (8,) Norm: 1.000000
# --- Random parameter generation ---
print("=== generate_random_parameters() ===")
# From encoding object
params = generate_random_parameters(enc, n_samples=5, seed=42)
print(f" From encoding: shape={params.shape}")
print(f" First sample: {params[0]}")
# From integer
params_int = generate_random_parameters(3, n_samples=5, param_min=0, param_max=2*np.pi, seed=42)
print(f" From int: shape={params_int.shape}")
print(f" First sample: {params_int[0]}")
=== generate_random_parameters() === From encoding: shape=(5, 3) First sample: [4.86290927 2.75755456 5.39472984] From int: shape=(5, 3) First sample: [4.86290927 2.75755456 5.39472984]
# --- Reproducible RNG ---
rng1 = create_rng(seed=42)
rng2 = create_rng(seed=42)
vals1 = rng1.random(5)
vals2 = rng2.random(5)
assert np.array_equal(vals1, vals2)
print(f"RNG reproducibility: {np.array_equal(vals1, vals2)}")
print(f" vals1: {vals1}")
print(f" vals2: {vals2}")
RNG reproducibility: True vals1: [0.77395605 0.43887844 0.85859792 0.69736803 0.09417735] vals2: [0.77395605 0.43887844 0.85859792 0.69736803 0.09417735]
20. Capability Protocols¶
The library uses Python's structural subtyping (PEP 544) to define optional capability protocols.
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
DataDependentResourceAnalyzable,
EntanglementQueryable,
DataTransformable,
is_resource_analyzable,
is_data_dependent_resource_analyzable,
is_entanglement_queryable,
is_data_transformable,
)
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
print("=== Protocol Checks (isinstance) ===")
print(f" ResourceAnalyzable : {isinstance(enc, ResourceAnalyzable)}")
print(f" EntanglementQueryable : {isinstance(enc, EntanglementQueryable)}")
print(f" DataDependentResourceAnalyzable: {isinstance(enc, DataDependentResourceAnalyzable)}")
print(f" DataTransformable : {isinstance(enc, DataTransformable)}")
print("\n=== Type Guards ===")
print(f" is_resource_analyzable() : {is_resource_analyzable(enc)}")
print(f" is_entanglement_queryable(): {is_entanglement_queryable(enc)}")
print(f" is_data_dependent_resource_analyzable(): {is_data_dependent_resource_analyzable(enc)}")
print(f" is_data_transformable() : {is_data_transformable(enc)}")
=== Protocol Checks (isinstance) === ResourceAnalyzable : True EntanglementQueryable : True DataDependentResourceAnalyzable: False DataTransformable : False === Type Guards === is_resource_analyzable() : True is_entanglement_queryable(): True is_data_dependent_resource_analyzable(): False is_data_transformable() : False
# Writing generic analysis code using protocols
def analyze_encoding(enc):
"""Generic function that works with any encoding via protocols."""
print(f"Analyzing: {enc}")
if is_resource_analyzable(enc):
summary = enc.resource_summary()
print(f" Total gates: {summary['gate_counts']['total']}")
print(f" Depth: {summary['depth']}")
if is_entanglement_queryable(enc):
pairs = enc.get_entanglement_pairs()
print(f" Entanglement pairs: {len(pairs)}")
else:
print(f" No entanglement query available")
if is_data_transformable(enc):
print(f" Data transformable: Yes")
else:
print(f" Data transformable: No")
# Test with ZZFeatureMap and AngleEncoding
analyze_encoding(ZZFeatureMap(n_features=4, entanglement='full'))
print()
analyze_encoding(AngleEncoding(n_features=4))
Analyzing: ZZFeatureMap(n_features=4, reps=2, entanglement='full') Total gates: 52 Depth: 22 Entanglement pairs: 6 Data transformable: No Analyzing: AngleEncoding(n_features=4, rotation='Y', reps=1) Total gates: 4 Depth: 1 No entanglement query available Data transformable: No
21. Registry System¶
ZZFeatureMap is registered in the global encoding registry under the name "zz_feature_map".
from encoding_atlas import get_encoding, list_encodings
# List all registered encodings
all_encodings = list_encodings()
print(f"Registered encodings ({len(all_encodings)}):\n {all_encodings}")
print()
assert 'zz_feature_map' in all_encodings
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']
# Create ZZFeatureMap via registry
enc_via_name = get_encoding("zz_feature_map", n_features=4, reps=3, entanglement='linear')
print(f"Via registry: {enc_via_name}")
print(f" Type: {type(enc_via_name).__name__}")
assert isinstance(enc_via_name, ZZFeatureMap)
assert enc_via_name.reps == 3
assert enc_via_name.entanglement == 'linear'
Via registry: ZZFeatureMap(n_features=4, reps=3, entanglement='linear') Type: ZZFeatureMap
# RegistryError for unknown names
from encoding_atlas.core.exceptions import RegistryError
try:
get_encoding("nonexistent_encoding", n_features=4)
except RegistryError as e:
print(f"RegistryError: {e}")
RegistryError: Unknown encoding 'nonexistent_encoding'. 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
22. Equality, Hashing & Serialization¶
ZZFeatureMap supports equality comparison, hashing (usable in sets/dicts), and pickle serialization.
# --- Equality ---
enc_a = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
enc_b = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
enc_c = ZZFeatureMap(n_features=4, reps=3, entanglement='full')
enc_d = ZZFeatureMap(n_features=4, reps=2, entanglement='linear')
print("=== Equality ===")
print(f" enc_a == enc_b (same config): {enc_a == enc_b}")
print(f" enc_a == enc_c (different reps): {enc_a == enc_c}")
print(f" enc_a == enc_d (different topology): {enc_a == enc_d}")
print(f" enc_a == 'not an encoding': {enc_a == 'not an encoding'}")
assert enc_a == enc_b
assert enc_a != enc_c
assert enc_a != enc_d
=== Equality === enc_a == enc_b (same config): True enc_a == enc_c (different reps): False enc_a == enc_d (different topology): False enc_a == 'not an encoding': False
# --- Hashing ---
print("=== Hashing ===")
print(f" hash(enc_a): {hash(enc_a)}")
print(f" hash(enc_b): {hash(enc_b)}")
assert hash(enc_a) == hash(enc_b)
print(f" hash(enc_a) == hash(enc_b): {hash(enc_a) == hash(enc_b)}")
# Use in sets and dicts
enc_set = {enc_a, enc_b, enc_c, enc_d}
print(f" Set of 4 encodings (2 duplicates): {len(enc_set)} unique")
assert len(enc_set) == 3 # enc_a == enc_b
enc_dict = {enc_a: "full-2", enc_c: "full-3", enc_d: "linear-2"}
print(f" Dict lookup enc_b (== enc_a): {enc_dict[enc_b]!r}")
=== Hashing === hash(enc_a): -8930804591261932944 hash(enc_b): -8930804591261932944 hash(enc_a) == hash(enc_b): True Set of 4 encodings (2 duplicates): 3 unique Dict lookup enc_b (== enc_a): 'full-2'
# --- Pickle Serialization ---
import pickle
enc_original = ZZFeatureMap(n_features=4, reps=3, entanglement='circular')
# Access properties to ensure they are cached before serialization
_ = enc_original.properties
# Serialize
data = pickle.dumps(enc_original)
print(f"Serialized size: {len(data)} bytes")
# Deserialize
enc_restored = pickle.loads(data)
print(f"Original: {enc_original}")
print(f"Restored: {enc_restored}")
print(f"Equal: {enc_original == enc_restored}")
# Verify functionality is preserved
x = np.array([0.1, 0.2, 0.3, 0.4])
circuit = enc_restored.get_circuit(x, backend='pennylane')
print(f"Restored encoding works: callable={callable(circuit)}")
assert enc_original == enc_restored
Serialized size: 699 bytes Original: ZZFeatureMap(n_features=4, reps=3, entanglement='circular') Restored: ZZFeatureMap(n_features=4, reps=3, entanglement='circular') Equal: True Restored encoding works: callable=True
23. Thread Safety¶
ZZFeatureMap is designed for safe concurrent use. Circuit generation is stateless, and property access uses double-checked locking.
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
# Concurrent circuit generation
np.random.seed(42)
X_concurrent = np.random.uniform(0, 2 * np.pi, (50, 4))
results = {}
errors = []
def generate_circuit(idx):
try:
circuit = enc.get_circuit(X_concurrent[idx], backend='pennylane')
return idx, callable(circuit)
except Exception as e:
return idx, str(e)
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {executor.submit(generate_circuit, i): i for i in range(50)}
for future in as_completed(futures):
idx, result = future.result()
results[idx] = result
all_ok = all(v is True for v in results.values())
print(f"Concurrent circuit generation: {len(results)} circuits, all OK: {all_ok}")
assert all_ok
Concurrent circuit generation: 50 circuits, all OK: True
# Concurrent property access (tests double-checked locking)
enc_fresh = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
property_results = []
def access_properties():
props = enc_fresh.properties
return props.n_qubits, props.depth, props.gate_count
with ThreadPoolExecutor(max_workers=8) as executor:
futures = [executor.submit(access_properties) for _ in range(20)]
property_results = [f.result() for f in futures]
# All threads should get the same result
assert all(r == property_results[0] for r in property_results)
print(f"Concurrent property access: all 20 threads got {property_results[0]}")
print(f"Thread safety confirmed.")
Concurrent property access: all 20 threads got (4, 22, 52) Thread safety confirmed.
import logging
# Enable debug logging for the ZZ feature map module
logger = logging.getLogger('encoding_atlas.encodings.zz_feature_map')
logger.setLevel(logging.DEBUG)
# Add a handler to see output
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
print("Creating ZZFeatureMap with debug logging enabled...\n")
enc_debug = ZZFeatureMap(n_features=3, reps=1, entanglement='linear')
print()
print("Generating circuit...\n")
_ = enc_debug.get_circuit(np.array([0.5, 1.0, 1.5]), backend='pennylane')
# Clean up handler
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
encoding_atlas.encodings.zz_feature_map - DEBUG - Entanglement pairs computed: topology='linear', n_qubits=3, n_pairs=2 encoding_atlas.encodings.zz_feature_map - DEBUG - ZZFeatureMap initialized: n_features=3, n_qubits=3, reps=1, entanglement='linear' encoding_atlas.encodings.zz_feature_map - DEBUG - get_circuit called: backend='pennylane', input_shape=(3,)
Creating ZZFeatureMap with debug logging enabled... Generating circuit...
encoding_atlas.encodings.zz_feature_map - DEBUG - PennyLane circuit generated: n_qubits=3
# Input range debug logging (values outside optimal [0, 2pi] range)
logger = logging.getLogger('encoding_atlas.encodings.zz_feature_map')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
logger.addHandler(handler)
print("Generating circuit with large input values...\n")
x_large = np.array([100.0, 200.0, 300.0])
_ = enc_debug.get_circuit(x_large, backend='pennylane')
# Clean up
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
DEBUG - get_circuit called: backend='pennylane', input_shape=(3,) DEBUG - Input values [100, 300] are outside optimal range [0, 2π]. The (π - x) phase convention works best with scaled inputs. Consider normalizing features to [0, 2π] or [-π, π]. DEBUG - PennyLane circuit generated: n_qubits=3
Generating circuit with large input values...
# Batch processing logging
logger = logging.getLogger('encoding_atlas.encodings.zz_feature_map')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
logger.addHandler(handler)
print("Batch generation logging...\n")
X_batch = np.random.uniform(0, 2*np.pi, (5, 3))
_ = enc_debug.get_circuits(X_batch, backend='pennylane', parallel=True)
# Clean up
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
DEBUG - Batch processing started: n_samples=5, backend='pennylane', parallel=True, max_workers=None DEBUG - Parallel batch processing completed: generated 5 circuits using ThreadPoolExecutor
Batch generation logging...
25. Visualization & Comparison¶
Compare ZZFeatureMap against other encodings to understand its trade-offs.
from encoding_atlas.visualization import compare_encodings
# Text-based comparison
comparison_text = compare_encodings(
["zz_feature_map", "iqp", "angle", "pauli_feature_map"],
n_features=4,
output="text",
)
print(comparison_text)
┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ zz_feature_map ███████████████ 4 zz_feature_map ███████████████│ │ iqp ███████████████ 4 iqp ████ │ │ angle ███████████████ 4 angle │ │ pauli_feature_map ███████████████ 4 pauli_feature_map ██████ │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ zz_feature_map ███████████████ 52 zz_feature_map ███████████████│ │ iqp ███████████████ 52 iqp ███████████████│ │ angle █ 4 angle │ │ pauli_feature_map ███████████████ 52 pauli_feature_map ███████████████│ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ────────────────────────────────────────────────────────────────────── │ │ zz_feature_map ✓ Yes Not Simulable █████ 0.6 │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ angle ✗ No Simulable ███████ 0.9 │ │ pauli_feature_map ✓ Yes Not Simulable ███ 0.4 │ │ │ └────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ zz_feature_map ███████████████ 4 zz_feature_map ███████████████│ │ iqp ███████████████ 4 iqp ████ │ │ angle ███████████████ 4 angle │ │ pauli_feature_map ███████████████ 4 pauli_feature_map ██████ │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ zz_feature_map ███████████████ 52 zz_feature_map ███████████████│ │ iqp ███████████████ 52 iqp ███████████████│ │ angle █ 4 angle │ │ pauli_feature_map ███████████████ 52 pauli_feature_map ███████████████│ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ────────────────────────────────────────────────────────────────────── │ │ zz_feature_map ✓ Yes Not Simulable █████ 0.6 │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ angle ✗ No Simulable ███████ 0.9 │ │ pauli_feature_map ✓ Yes Not Simulable ███ 0.4 │ │ │ └────────────────────────────────────────────────────────────────────────────┘
# Compare different ZZFeatureMap configurations
from encoding_atlas.analysis import compare_resources
configs = [
ZZFeatureMap(n_features=4, reps=1, entanglement='full'),
ZZFeatureMap(n_features=4, reps=2, entanglement='full'),
ZZFeatureMap(n_features=4, reps=2, entanglement='linear'),
ZZFeatureMap(n_features=4, reps=2, entanglement='circular'),
]
comparison = compare_resources(configs)
print("=== ZZFeatureMap Configuration Comparison ===")
for key, value in comparison.items():
print(f" {key}: {value}")
=== ZZFeatureMap Configuration Comparison === n_qubits: [4, 4, 4, 4] depth: [11, 22, 22, 28] gate_count: [26, 52, 34, 40] single_qubit_gates: [14, 28, 22, 24] two_qubit_gates: [12, 24, 12, 16] parameter_count: [10, 20, 14, 16] two_qubit_ratio: [0.46153846153846156, 0.46153846153846156, 0.35294117647058826, 0.4] gates_per_qubit: [6.5, 13.0, 8.5, 10.0] encoding_name: ['ZZFeatureMap', 'ZZFeatureMap', 'ZZFeatureMap', 'ZZFeatureMap']
26. Encoding Recommendation Guide¶
The library includes a recommendation guide that suggests encodings based on problem characteristics.
from encoding_atlas.guide import recommend_encoding
# Scenario: classification with moderate features, accuracy priority
rec = recommend_encoding(
n_features=4,
n_samples=500,
task="classification",
hardware="simulator",
priority="accuracy",
)
print("=== Recommendation (accuracy priority) ===")
print(f" Encoding: {rec.encoding_name}")
print(f" Explanation: {rec.explanation}")
print(f" Alternatives: {rec.alternatives}")
print(f" Confidence: {rec.confidence:.2f}")
=== Recommendation (accuracy priority) === Encoding: iqp 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'] Confidence: 0.74
# Try different priorities to see when ZZFeatureMap is recommended
print("=== Recommendations by priority ===")
for priority in ['accuracy', 'trainability', 'speed', 'noise_resilience']:
rec = recommend_encoding(
n_features=4,
n_samples=500,
task='classification',
hardware='simulator',
priority=priority,
)
zz_mentioned = 'zz' in rec.encoding_name.lower() or 'zz' in str(rec.alternatives).lower()
print(f" {priority:16s}: recommended={rec.encoding_name!r}, "
f"ZZ mentioned={'Yes' if zz_mentioned else 'No'}")
=== Recommendations by priority === accuracy : recommended='iqp', ZZ mentioned=Yes trainability : recommended='data_reuploading', ZZ mentioned=Yes speed : recommended='angle', ZZ mentioned=Yes noise_resilience: recommended='hardware_efficient', ZZ mentioned=Yes
27. Data Preprocessing Utilities¶
The library provides utilities to scale and normalize features before encoding.
from encoding_atlas.utils import scale_features, normalize_features
# Raw data (e.g., from a dataset)
raw_data = np.array([10.0, 25.0, 50.0, 100.0])
# Scale to [0, pi] (default)
scaled_pi = scale_features(raw_data)
print(f"Raw data: {raw_data}")
print(f"Scaled [0,pi]: {scaled_pi}")
# Scale to [0, 2*pi] for ZZFeatureMap's (pi-x) convention
scaled_2pi = scale_features(raw_data, range_min=0.0, range_max=2*np.pi)
print(f"Scaled [0,2pi]: {scaled_2pi}")
# Normalize to unit norm
normalized = normalize_features(raw_data)
print(f"Normalized: {normalized}")
print(f" Norm: {np.linalg.norm(normalized):.6f}")
Raw data: [ 10. 25. 50. 100.] Scaled [0,pi]: [0. 0.52359878 1.3962634 3.14159265] Scaled [0,2pi]: [0. 1.04719755 2.7925268 6.28318531] Normalized: [0.08695652 0.2173913 0.43478261 0.86956522] Norm: 1.000000
# Use scaled data with ZZFeatureMap
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
# Generate circuit with scaled data
circuit = enc.get_circuit(scaled_2pi, backend='pennylane')
print(f"Circuit from scaled data: callable={callable(circuit)}")
# The (pi - x) convention means:
# x near 0 -> large interaction: (pi - 0)^2 = pi^2
# x near pi -> zero interaction: (pi - pi)^2 = 0
print("\nPhase convention insight:")
for val in [0, np.pi/4, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi]:
interaction = (np.pi - val) ** 2
print(f" x = {val:.4f} ({val/np.pi:.2f}*pi): (pi-x)^2 = {interaction:.4f}")
Circuit from scaled data: callable=True Phase convention insight: x = 0.0000 (0.00*pi): (pi-x)^2 = 9.8696 x = 0.7854 (0.25*pi): (pi-x)^2 = 5.5517 x = 1.5708 (0.50*pi): (pi-x)^2 = 2.4674 x = 3.1416 (1.00*pi): (pi-x)^2 = 0.0000 x = 4.7124 (1.50*pi): (pi-x)^2 = 2.4674 x = 6.2832 (2.00*pi): (pi-x)^2 = 9.8696
28. Complete End-to-End Workflow¶
A realistic end-to-end example: use ZZFeatureMap for QSVM-style quantum kernel computation.
# Step 1: Choose and configure the encoding
enc = ZZFeatureMap(n_features=4, reps=2, entanglement='full')
print(f"Encoding: {enc}")
print(f"Properties:")
print(f" Qubits: {enc.n_qubits}")
print(f" Depth: {enc.depth}")
print(f" Entangling: {enc.properties.is_entangling}")
print(f" Simulability: {enc.properties.simulability}")
print(f" ZZ pairs: {len(enc.get_entanglement_pairs())}")
Encoding: ZZFeatureMap(n_features=4, reps=2, entanglement='full') Properties: Qubits: 4 Depth: 22 Entangling: True Simulability: not_simulable ZZ pairs: 6
# Step 2: Analyze resources
breakdown = enc.gate_count_breakdown()
print("Gate counts:")
print(f" Single-qubit: {breakdown['total_single_qubit']}")
print(f" Two-qubit: {breakdown['total_two_qubit']}")
print(f" Total: {breakdown['total']}")
sim_result = check_simulability(enc)
print(f"\nSimulability: {sim_result['simulability_class']}")
print(f" {sim_result['reason']}")
Gate counts: Single-qubit: 28 Two-qubit: 24 Total: 52 Simulability: not_simulable High entanglement circuit with 24 two-qubit gates and non-Clifford operations
# Step 3: Prepare data
from sklearn.datasets import load_iris
from sklearn.preprocessing import MinMaxScaler
# Load Iris dataset (4 features, binary classification: class 0 vs class 1)
iris = load_iris()
# Select 10 samples from each of class 0 and class 1
idx_0 = np.where(iris.target == 0)[0][:10]
idx_1 = np.where(iris.target == 1)[0][:10]
idx = np.concatenate([idx_0, idx_1])
X_raw = iris.data[idx]
y = iris.target[idx]
# Scale features to [0, 2*pi] for ZZFeatureMap's (pi-x) convention
scaler = MinMaxScaler(feature_range=(0, 2 * np.pi))
X_scaled = scaler.fit_transform(X_raw)
print(f"Data shape: {X_scaled.shape}")
print(f"Feature range: [{X_scaled.min():.4f}, {X_scaled.max():.4f}]")
print(f"Labels: {np.unique(y)}, counts: {np.bincount(y)}")
Data shape: (20, 4) Feature range: [0.0000, 6.2832] Labels: [0 1], counts: [10 10]
# Step 4: Compute quantum kernel matrix
# K(x, x') = |<psi(x)|psi(x')>|^2
n_samples = len(X_scaled)
kernel_matrix = np.zeros((n_samples, n_samples))
# Compute statevectors for all samples
states = simulate_encoding_statevectors_batch(enc, X_scaled)
# Compute kernel matrix
for i in range(n_samples):
for j in range(i, n_samples):
fidelity = compute_fidelity(states[i], states[j])
kernel_matrix[i, j] = fidelity
kernel_matrix[j, i] = fidelity
print(f"Kernel matrix shape: {kernel_matrix.shape}")
print(f"Diagonal (self-fidelity): {kernel_matrix.diagonal()[:5]}")
print(f"Off-diagonal range: [{kernel_matrix[np.triu_indices(n_samples, 1)].min():.6f}, "
f"{kernel_matrix[np.triu_indices(n_samples, 1)].max():.6f}]")
# Verify: diagonal should be all 1.0 (self-fidelity)
assert np.allclose(kernel_matrix.diagonal(), 1.0, atol=1e-6)
print("\nKernel matrix diagonal is all 1.0 (verified)")
# Verify: kernel is symmetric
assert np.allclose(kernel_matrix, kernel_matrix.T)
print("Kernel matrix is symmetric (verified)")
Kernel matrix shape: (20, 20) Diagonal (self-fidelity): [1. 1. 1. 1. 1.] Off-diagonal range: [0.000117, 0.294371] Kernel matrix diagonal is all 1.0 (verified) Kernel matrix is symmetric (verified)
# Step 5: Use quantum kernel with SVM
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
# Create SVM with precomputed quantum kernel
svm = SVC(kernel='precomputed')
svm.fit(kernel_matrix, y)
train_score = svm.score(kernel_matrix, y)
print(f"QSVM Training accuracy: {train_score:.4f}")
print(f"\nEnd-to-end QSVM workflow complete!")
QSVM Training accuracy: 1.0000 End-to-end QSVM workflow complete!
Summary¶
This notebook demonstrated every feature of ZZFeatureMap 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,n_features,config,reps,entanglement - Lazy, thread-safe properties via
EncodingPropertiesfrozen dataclass
Entanglement & Circuit Structure¶
- Three entanglement topologies: full (all-to-all), linear (chain), circular (ring)
- Entanglement pair computation:
get_entanglement_pairs()with correct edge cases - Circuit depth analysis: topology-dependent depth formula with chromatic index optimization
- Phase convention: $2(\pi - x_i)(\pi - x_j)$ for ZZ interactions (Qiskit-compatible)
Multi-Backend Circuit Generation¶
- PennyLane: Returns callable closure for QNode integration
- Qiskit: Returns
QuantumCircuitobject - Cirq: Returns
cirq.Circuitwith parallel moments - Batch processing: Sequential and parallel via
get_circuits()
Analysis Capabilities¶
- Gate count breakdown:
gate_count_breakdown()with all gate types - Resource summary:
resource_summary()with hardware requirements - Resource analysis:
count_resources(),compare_resources(),estimate_execution_time() - Simulability: Not classically simulable (entangling, non-Clifford, non-matchgate)
- Expressibility: Measured via KL divergence from Haar random
- Entanglement: Non-zero Meyer-Wallach measure
- Trainability: Estimated with barren plateau detection
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:
ResourceAnalyzable,EntanglementQueryable(PEP 544) - Registry system: Factory-style creation via
get_encoding("zz_feature_map") - 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
Key Properties of ZZFeatureMap¶
| Property | Value |
|---|---|
| Qubits | $n$ (one per feature) |
| Depth | $\text{reps} \times (2 + 3 \times \chi)$ where $\chi$ depends on topology |
| Single-qubit gates | $\text{reps} \times (2n + n_{\text{pairs}})$ |
| Two-qubit gates (CNOT) | $\text{reps} \times 2 \times n_{\text{pairs}}$ |
| Entangling | Yes |
| Simulable | No |
| Trainability | $\max(0.3, 0.85 - 0.1 \times \text{reps})$ |
| Hardware connectivity | Depends on topology (all-to-all / linear / ring) |
| Qiskit compatible | Yes |