Hardware-Efficient Encoding - Comprehensive Guide¶
This notebook provides a complete walkthrough of HardwareEfficientEncoding from the Quantum Encoding Atlas library. It covers every feature, parameter, method, and edge case.
Hardware-efficient encoding is a quantum data encoding technique designed for NISQ (Noisy Intermediate-Scale Quantum) devices. It uses only native gates (single-qubit rotations + CNOT) and respects physical qubit connectivity constraints, minimizing gate decomposition overhead and reducing circuit depth.
The encoding creates quantum states using:
$$|\psi(x)\rangle = \left[U_{\text{ent}} \cdot U_{\text{rot}}(x)\right]^{\text{reps}} |0\rangle^{\otimes n}$$
where $U_{\text{rot}}$ applies data-dependent rotations and $U_{\text{ent}}$ provides entanglement using connectivity-respecting CNOT gates.
Table of Contents¶
- Installation & Imports
- Basic Instantiation
- Constructor Parameters In-Depth
- Properties & Attributes
- Entanglement Topologies
- Circuit Generation - Single Sample
- Circuit Generation - Batch Processing
- Multi-Backend Support
- Resource Analysis
- Encoding Properties (EncodingProperties)
- Capability Protocols
- Analysis Tools
- Input Validation & Edge Cases
- Equality, Hashing & Serialization
- Thread Safety
- Logging & Debugging
- Comparison with Other Encodings
- Practical Use Cases
# Install encoding-atlas and optional backends
# pip install encoding-atlas pennylane qiskit cirq-core
import numpy as np
import warnings
import pickle
import logging
# Main import
from encoding_atlas import HardwareEfficientEncoding
# Core infrastructure
from encoding_atlas import BaseEncoding, EncodingProperties
# Capability protocols
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
EntanglementQueryable,
DataTransformable,
DataDependentResourceAnalyzable,
is_resource_analyzable,
is_entanglement_queryable,
is_data_transformable,
is_data_dependent_resource_analyzable,
)
# Analysis tools
from encoding_atlas.analysis import (
count_resources,
get_resource_summary,
get_gate_breakdown,
compare_resources,
estimate_execution_time,
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
estimate_entanglement_bound,
simulate_encoding_statevector,
compute_fidelity,
validate_encoding_for_analysis,
)
# Backend availability checks
from encoding_atlas.backends import (
is_pennylane_available,
is_qiskit_available,
is_cirq_available,
get_available_backends,
)
print("encoding-atlas imported successfully!")
print(f"Available backends: {get_available_backends()}")
encoding-atlas imported successfully! Available backends: ['pennylane', 'qiskit', 'cirq']
2. Basic Instantiation¶
HardwareEfficientEncoding requires only n_features (the number of classical features to encode). Each feature maps to one qubit.
# Minimal instantiation - only n_features is required
enc = HardwareEfficientEncoding(n_features=4)
print(enc)
print(f"Type: {type(enc).__name__}")
print(f"Is a BaseEncoding: {isinstance(enc, BaseEncoding)}")
HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear') Type: HardwareEfficientEncoding Is a BaseEncoding: True
# Default parameter values
print(f"n_features: {enc.n_features}") # 4 (what we set)
print(f"reps: {enc.reps}") # 2 (default)
print(f"rotation: {enc.rotation}") # 'Y' (default)
print(f"entanglement:{enc.entanglement}") # 'linear' (default)
n_features: 4 reps: 2 rotation: Y entanglement:linear
# Fully specified instantiation
enc_custom = HardwareEfficientEncoding(
n_features=6,
reps=3,
rotation="X",
entanglement="circular",
)
print(enc_custom)
HardwareEfficientEncoding(n_features=6, reps=3, rotation='X', entanglement='circular')
# n_features determines qubit count
for n in [1, 2, 4, 8]:
enc = HardwareEfficientEncoding(n_features=n)
print(f"n_features={n} -> n_qubits={enc.n_qubits}")
n_features=1 -> n_qubits=1 n_features=2 -> n_qubits=2 n_features=4 -> n_qubits=4 n_features=8 -> n_qubits=8
# Invalid n_features values
for bad_val in [0, -1, 1.5, "4"]:
try:
HardwareEfficientEncoding(n_features=bad_val)
except (ValueError, TypeError) as e:
print(f"n_features={bad_val!r}: {type(e).__name__}: {e}")
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='4': ValueError: n_features must be a positive integer, got 4
3.2 reps - Number of Layer Repetitions¶
Controls how many times the rotation + entanglement layers are repeated. Higher values increase expressivity but also circuit depth and accumulated errors. Must be a positive integer (>= 1).
# Effect of reps on circuit depth
for r in [1, 2, 3, 5, 10]:
enc = HardwareEfficientEncoding(n_features=4, reps=r)
print(f"reps={r:2d} -> depth={enc.depth:2d}, "
f"total_gates={enc.properties.gate_count}")
reps= 1 -> depth= 2, total_gates=7 reps= 2 -> depth= 4, total_gates=14 reps= 3 -> depth= 6, total_gates=21 reps= 5 -> depth=10, total_gates=35 reps=10 -> depth=20, total_gates=70
# Invalid reps values - all rejected with clear error messages
for bad_val in [0, -1, 1.5, True, False]:
try:
HardwareEfficientEncoding(n_features=4, reps=bad_val)
except (ValueError, TypeError) as e:
print(f"reps={bad_val!r}: {type(e).__name__}: {e}")
reps=0: ValueError: reps must be a positive integer, got 0 reps=-1: ValueError: reps must be a positive integer, got -1 reps=1.5: ValueError: reps must be a positive integer, got 1.5 reps=True: ValueError: reps must be a positive integer, got True reps=False: ValueError: reps must be a positive integer, got False
3.3 rotation - Rotation Axis¶
Determines the single-qubit rotation gate used for data encoding. Must be one of "X", "Y", or "Z" (case-sensitive).
| Rotation | Gate | Hardware Notes |
|---|---|---|
"X" |
RX(x) | Native on most superconducting qubits |
"Y" |
RY(x) | Default; creates real-valued amplitudes |
"Z" |
RZ(x) | Often "virtual" with zero error on superconducting devices |
# All three valid rotation axes
for rot in ["X", "Y", "Z"]:
enc = HardwareEfficientEncoding(n_features=4, rotation=rot)
breakdown = enc.gate_count_breakdown()
print(f"rotation='{rot}' -> RX={breakdown['rx']}, "
f"RY={breakdown['ry']}, RZ={breakdown['rz']}")
rotation='X' -> RX=8, RY=0, RZ=0 rotation='Y' -> RX=0, RY=8, RZ=0 rotation='Z' -> RX=0, RY=0, RZ=8
# Invalid rotation values (case-sensitive!)
for bad_val in ["x", "y", "z", "RY", "H", 1]:
try:
HardwareEfficientEncoding(n_features=4, rotation=bad_val)
except (ValueError, TypeError) as e:
print(f"rotation={bad_val!r}: {type(e).__name__}: {e}")
rotation='x': ValueError: rotation must be one of ['X', 'Y', 'Z'], got 'x' rotation='y': ValueError: rotation must be one of ['X', 'Y', 'Z'], got 'y' rotation='z': ValueError: rotation must be one of ['X', 'Y', 'Z'], got 'z' rotation='RY': ValueError: rotation must be one of ['X', 'Y', 'Z'], got 'RY' rotation='H': ValueError: rotation must be one of ['X', 'Y', 'Z'], got 'H' rotation=1: ValueError: rotation must be one of ['X', 'Y', 'Z'], got 1
3.4 entanglement - CNOT Topology¶
Controls the pattern of CNOT gates for entanglement. Must be one of "linear", "circular", or "full".
| Topology | Pairs (n=4) | Count | Best For |
|---|---|---|---|
"linear" |
(0,1), (1,2), (2,3) | n-1 | Superconducting (IBM, Google) |
"circular" |
(0,1), (1,2), (2,3), (3,0) | n | Ring topologies |
"full" |
All pairs where i < j | n(n-1)/2 | Ion traps (IonQ, Quantinuum) |
# All three entanglement topologies
for ent in ["linear", "circular", "full"]:
enc = HardwareEfficientEncoding(n_features=4, entanglement=ent)
pairs = enc.get_entanglement_pairs()
print(f"entanglement='{ent:8s}' -> {len(pairs)} pairs: {pairs}")
entanglement='linear ' -> 3 pairs: [(0, 1), (1, 2), (2, 3)] entanglement='circular' -> 4 pairs: [(0, 1), (1, 2), (2, 3), (3, 0)] entanglement='full ' -> 6 pairs: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
# Invalid entanglement values
for bad_val in ["Linear", "ring", "star", "all"]:
try:
HardwareEfficientEncoding(n_features=4, entanglement=bad_val)
except ValueError as e:
print(f"entanglement={bad_val!r}: {e}")
entanglement='Linear': entanglement must be one of ['circular', 'full', 'linear'], got 'Linear' entanglement='ring': entanglement must be one of ['circular', 'full', 'linear'], got 'ring' entanglement='star': entanglement must be one of ['circular', 'full', 'linear'], got 'star' entanglement='all': entanglement must be one of ['circular', 'full', 'linear'], got 'all'
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation="Y", entanglement="linear")
# Direct instance attributes
print("=== Direct Attributes ===")
print(f"n_features: {enc.n_features}") # From BaseEncoding
print(f"n_qubits: {enc.n_qubits}") # Property: always == n_features
print(f"depth: {enc.depth}") # Property: 2 * reps
print(f"reps: {enc.reps}") # Instance attribute
print(f"rotation: {enc.rotation}") # Instance attribute
print(f"entanglement: {enc.entanglement}") # Instance attribute
=== Direct Attributes === n_features: 4 n_qubits: 4 depth: 4 reps: 2 rotation: Y entanglement: linear
4.2 Configuration Dictionary¶
The config property returns a read-only copy of the encoding's configuration parameters.
config = enc.config
print(f"Config: {config}")
print(f"Type: {type(config)}")
# config is a copy - modifying it doesn't affect the encoding
config['reps'] = 999
print(f"\nAfter modifying config copy:")
print(f" config['reps'] = {config['reps']}")
print(f" enc.reps = {enc.reps} (unchanged)")
Config: {'reps': 2, 'rotation': 'Y', 'entanglement': 'linear'}
Type: <class 'dict'>
After modifying config copy:
config['reps'] = 999
enc.reps = 2 (unchanged)
4.3 String Representation (__repr__)¶
# __repr__ shows all configuration parameters
enc1 = HardwareEfficientEncoding(n_features=4)
enc2 = HardwareEfficientEncoding(n_features=8, reps=3, rotation="Z", entanglement="full")
print(repr(enc1))
print(repr(enc2))
HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear') HardwareEfficientEncoding(n_features=8, reps=3, rotation='Z', entanglement='full')
# Scaling of entanglement pairs with qubit count
print(f"{'n_qubits':>8} | {'Linear':>8} | {'Circular':>10} | {'Full':>6}")
print("-" * 45)
for n in [1, 2, 3, 4, 5, 8, 10]:
linear = len(HardwareEfficientEncoding(n_features=n, entanglement='linear').get_entanglement_pairs())
circular = len(HardwareEfficientEncoding(n_features=n, entanglement='circular').get_entanglement_pairs())
full = len(HardwareEfficientEncoding(n_features=n, entanglement='full').get_entanglement_pairs())
print(f"{n:>8} | {linear:>8} | {circular:>10} | {full:>6}")
n_qubits | Linear | Circular | Full
---------------------------------------------
1 | 0 | 0 | 0
2 | 1 | 1 | 1
3 | 2 | 3 | 3
4 | 3 | 4 | 6
5 | 4 | 5 | 10
8 | 7 | 8 | 28
10 | 9 | 10 | 45
5.1 Linear Entanglement¶
Nearest-neighbor connectivity: CNOT gates connect consecutive qubits. Creates n-1 pairs per layer.
q0 --[RY(x0)]--*-----------[RY(x0)]--*-----------
| |
q1 --[RY(x1)]--X--*--------[RY(x1)]--X--*--------
| |
q2 --[RY(x2)]-----X--*-----[RY(x2)]-----X--*-----
| |
q3 --[RY(x3)]---------X----[RY(x3)]---------X----
|--- rep 1 ---| |--- rep 2 ---|
enc_linear = HardwareEfficientEncoding(n_features=5, entanglement='linear')
pairs = enc_linear.get_entanglement_pairs()
print(f"Linear entanglement (n=5): {pairs}")
print(f"Number of pairs: {len(pairs)} (n-1 = {5-1})")
Linear entanglement (n=5): [(0, 1), (1, 2), (2, 3), (3, 4)] Number of pairs: 4 (n-1 = 4)
5.2 Circular Entanglement¶
Linear connectivity plus a wrap-around CNOT from the last to the first qubit. Creates n pairs per layer (for n > 2).
enc_circ = HardwareEfficientEncoding(n_features=5, entanglement='circular')
pairs = enc_circ.get_entanglement_pairs()
print(f"Circular entanglement (n=5): {pairs}")
print(f"Number of pairs: {len(pairs)} (n = {5})")
print(f"Wrap-around pair: {pairs[-1]} (qubit {5-1} -> qubit 0)")
Circular entanglement (n=5): [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] Number of pairs: 5 (n = 5) Wrap-around pair: (4, 0) (qubit 4 -> qubit 0)
# Special case: n=2 circular has no wrap-around (would duplicate the only pair)
enc_circ_2 = HardwareEfficientEncoding(n_features=2, entanglement='circular')
pairs_2 = enc_circ_2.get_entanglement_pairs()
print(f"Circular entanglement (n=2): {pairs_2}")
print(f"Number of pairs: {len(pairs_2)} (no wrap-around added when n <= 2)")
# At n=2, linear and circular produce identical results
enc_lin_2 = HardwareEfficientEncoding(n_features=2, entanglement='linear')
print(f"Linear entanglement (n=2): {enc_lin_2.get_entanglement_pairs()}")
print(f"Identical: {enc_circ_2.get_entanglement_pairs() == enc_lin_2.get_entanglement_pairs()}")
Circular entanglement (n=2): [(0, 1)] Number of pairs: 1 (no wrap-around added when n <= 2) Linear entanglement (n=2): [(0, 1)] Identical: True
5.3 Full Entanglement¶
All-to-all connectivity: CNOT between every pair of qubits (i, j) where i < j. Creates n(n-1)/2 pairs per layer (O(n^2) scaling).
enc_full = HardwareEfficientEncoding(n_features=5, entanglement='full')
pairs = enc_full.get_entanglement_pairs()
print(f"Full entanglement (n=5): {pairs}")
print(f"Number of pairs: {len(pairs)} (n*(n-1)/2 = {5*4//2})")
Full entanglement (n=5): [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] Number of pairs: 10 (n*(n-1)/2 = 10)
5.4 Entanglement Pair Immutability¶
get_entanglement_pairs() returns a copy of the internal list, so modifying it doesn't affect the encoding.
enc = HardwareEfficientEncoding(n_features=4, entanglement='linear')
pairs = enc.get_entanglement_pairs()
original_pairs = pairs.copy()
# Modify the returned list
pairs.append((99, 100))
pairs.clear()
# Encoding's internal pairs are unaffected
print(f"Modified copy: {pairs}")
print(f"Encoding pairs: {enc.get_entanglement_pairs()}")
print(f"Still matches original: {enc.get_entanglement_pairs() == original_pairs}")
Modified copy: [] Encoding pairs: [(0, 1), (1, 2), (2, 3)] Still matches original: True
6. Circuit Generation - Single Sample¶
Use get_circuit(x, backend) to generate a quantum circuit for a single data sample.
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
x = np.array([0.1, 0.5, 1.0, 1.5])
# Generate a PennyLane circuit (default backend)
circuit_pl = enc.get_circuit(x, backend='pennylane')
print(f"PennyLane circuit type: {type(circuit_pl).__name__}")
print(f"Is callable: {callable(circuit_pl)}")
PennyLane circuit type: function Is callable: True
# Execute the PennyLane circuit in a QNode to get the statevector
if is_pennylane_available():
import pennylane as qml
dev = qml.device('default.qubit', wires=enc.n_qubits)
@qml.qnode(dev)
def qnode():
circuit_pl() # Apply the encoding gates
return qml.state()
state = qnode()
print(f"Statevector dimension: {len(state)} (2^{enc.n_qubits})")
print(f"Statevector (first 4 amplitudes): {state[:4]}")
print(f"Norm: {np.linalg.norm(state):.6f} (should be 1.0)")
Statevector dimension: 16 (2^4) Statevector (first 4 amplitudes): [0.04918411+0.j 0.50389879+0.j 0.72607993+0.j 0.0268694 +0.j] Norm: 1.000000 (should be 1.0)
# Generate a Qiskit circuit
if is_qiskit_available():
circuit_qk = enc.get_circuit(x, backend='qiskit')
print(f"Qiskit circuit type: {type(circuit_qk).__name__}")
print(f"Circuit name: {circuit_qk.name}")
print(f"Number of qubits: {circuit_qk.num_qubits}")
print(f"Circuit depth: {circuit_qk.depth()}")
print()
print(circuit_qk.draw())
Qiskit circuit type: QuantumCircuit
Circuit name: HardwareEfficientEncoding
Number of qubits: 4
Circuit depth: 7
┌─────────┐ ┌─────────┐
q_0: ┤ Ry(0.1) ├──■──┤ Ry(0.1) ├────────────────■───────────────
├─────────┤┌─┴─┐└─────────┘┌─────────┐ ┌─┴─┐
q_1: ┤ Ry(0.5) ├┤ X ├─────■─────┤ Ry(0.5) ├───┤ X ├─────■───────
└┬───────┬┘└───┘ ┌─┴─┐ └─────────┘ ┌─┴───┴─┐ ┌─┴─┐
q_2: ─┤ Ry(1) ├─────────┤ X ├────────■──────┤ Ry(1) ├─┤ X ├──■──
┌┴───────┴┐ └───┘ ┌─┴─┐ ┌┴───────┴┐└───┘┌─┴─┐
q_3: ┤ Ry(1.5) ├───────────────────┤ X ├───┤ Ry(1.5) ├─────┤ X ├
└─────────┘ └───┘ └─────────┘ └───┘
# Generate a Cirq circuit
if is_cirq_available():
circuit_cq = enc.get_circuit(x, backend='cirq')
print(f"Cirq circuit type: {type(circuit_cq).__name__}")
print(f"Number of qubits: {len(circuit_cq.all_qubits())}")
print()
print(circuit_cq)
Cirq circuit type: Circuit
Number of qubits: 4
0: ───Ry(0.032π)───@───Ry(0.032π)────────────────@────────────────────
│ │
1: ───Ry(0.159π)───X───@────────────Ry(0.159π)───X────────────@───────
│ │
2: ───Ry(0.318π)───────X────────────@────────────Ry(0.318π)───X───@───
│ │
3: ───Ry(0.477π)────────────────────X────────────Ry(0.477π)───────X───
6.1 Input Formats¶
get_circuit accepts multiple input formats: numpy arrays, lists, tuples, and 2D arrays with a single row.
enc = HardwareEfficientEncoding(n_features=3)
# 1D numpy array
c1 = enc.get_circuit(np.array([0.1, 0.2, 0.3]), backend='qiskit')
print(f"1D numpy array: OK (qubits={c1.num_qubits})")
# Python list
c2 = enc.get_circuit([0.1, 0.2, 0.3], backend='qiskit')
print(f"Python list: OK (qubits={c2.num_qubits})")
# Python tuple
c3 = enc.get_circuit((0.1, 0.2, 0.3), backend='qiskit')
print(f"Python tuple: OK (qubits={c3.num_qubits})")
# 2D array with single row
c4 = enc.get_circuit(np.array([[0.1, 0.2, 0.3]]), backend='qiskit')
print(f"2D (1, n): OK (qubits={c4.num_qubits})")
# Integer input (auto-converted to float64)
c5 = enc.get_circuit(np.array([1, 2, 3]), backend='qiskit')
print(f"Integer input: OK (qubits={c5.num_qubits})")
1D numpy array: OK (qubits=3) Python list: OK (qubits=3) Python tuple: OK (qubits=3) 2D (1, n): OK (qubits=3) Integer input: OK (qubits=3)
7. Circuit Generation - Batch Processing¶
Use get_circuits(X, backend, parallel, max_workers) to generate circuits for multiple data samples.
enc = HardwareEfficientEncoding(n_features=4, reps=2)
X = np.random.default_rng(42).uniform(0, 2 * np.pi, size=(10, 4))
# Sequential batch processing (default)
circuits = enc.get_circuits(X, backend='qiskit')
print(f"Generated {len(circuits)} circuits (sequential)")
print(f"Each circuit has {circuits[0].num_qubits} qubits")
Generated 10 circuits (sequential) Each circuit has 4 qubits
# Parallel batch processing with ThreadPoolExecutor
circuits_parallel = enc.get_circuits(X, backend='qiskit', parallel=True)
print(f"Generated {len(circuits_parallel)} circuits (parallel)")
# Custom worker count
import os
circuits_custom = enc.get_circuits(
X, backend='qiskit', parallel=True, max_workers=os.cpu_count()
)
print(f"Generated {len(circuits_custom)} circuits (parallel, {os.cpu_count()} workers)")
Generated 10 circuits (parallel) Generated 10 circuits (parallel, 12 workers)
# Order is preserved even with parallel processing
enc = HardwareEfficientEncoding(n_features=3, reps=1)
X_small = np.array([
[0.1, 0.2, 0.3],
[1.0, 2.0, 3.0],
[0.5, 0.5, 0.5],
])
seq_circuits = enc.get_circuits(X_small, backend='qiskit')
par_circuits = enc.get_circuits(X_small, backend='qiskit', parallel=True)
# Compare circuit parameters to verify order preservation
for i, (sc, pc) in enumerate(zip(seq_circuits, par_circuits)):
seq_params = [inst.params[0] for inst in sc.data if inst.params]
par_params = [inst.params[0] for inst in pc.data if inst.params]
match = np.allclose(seq_params, par_params)
print(f"Sample {i}: order preserved = {match}")
Sample 0: order preserved = True Sample 1: order preserved = True Sample 2: order preserved = True
# 1D input is treated as a single sample
x_single = np.array([0.1, 0.2, 0.3, 0.4])
enc = HardwareEfficientEncoding(n_features=4)
circuits = enc.get_circuits(x_single, backend='qiskit')
print(f"1D input -> {len(circuits)} circuit(s)")
1D input -> 1 circuit(s)
8. Multi-Backend Support¶
Hardware-efficient encoding supports three quantum computing frameworks: PennyLane, Qiskit, and Cirq.
# Check which backends are available
print(f"PennyLane available: {is_pennylane_available()}")
print(f"Qiskit available: {is_qiskit_available()}")
print(f"Cirq available: {is_cirq_available()}")
print(f"All available: {get_available_backends()}")
PennyLane available: True Qiskit available: True Cirq available: True All available: ['pennylane', 'qiskit', 'cirq']
# Generate circuits for all available backends
enc = HardwareEfficientEncoding(n_features=3, reps=1, rotation='Y', entanglement='linear')
x = np.array([0.5, 1.0, 1.5])
for backend in get_available_backends():
circuit = enc.get_circuit(x, backend=backend)
print(f"\n--- {backend.upper()} ---")
print(f" Type: {type(circuit).__name__}")
if backend == 'pennylane':
print(f" Callable: {callable(circuit)}")
elif backend == 'qiskit':
print(f" Qubits: {circuit.num_qubits}, Depth: {circuit.depth()}")
elif backend == 'cirq':
print(f" Qubits: {len(circuit.all_qubits())}")
--- PENNYLANE --- Type: function Callable: True --- QISKIT --- Type: QuantumCircuit Qubits: 3, Depth: 3 --- CIRQ --- Type: Circuit Qubits: 3
# Cross-backend statevector consistency
# The same encoding + input should produce the same quantum state regardless of backend
enc = HardwareEfficientEncoding(n_features=3, reps=2, rotation='Y', entanglement='linear')
x = np.array([0.3, 0.7, 1.2])
states = {}
for backend in get_available_backends():
state = simulate_encoding_statevector(enc, x, backend=backend)
states[backend] = state
print(f"{backend:10s} statevector[:3] = {state[:3]}")
# Compare fidelities between backends
backend_list = list(states.keys())
for i in range(len(backend_list)):
for j in range(i + 1, len(backend_list)):
b1, b2 = backend_list[i], backend_list[j]
fid = compute_fidelity(states[b1], states[b2])
print(f"Fidelity({b1}, {b2}) = {fid:.10f}")
pennylane statevector[:3] = [0.31000497+0.j 0.68961987+0.j 0.58627592+0.j] qiskit statevector[:3] = [0.31000497+0.j 0.68961987+0.j 0.58627592+0.j] cirq statevector[:3] = [0.31000497+0.j 0.68961987+0.j 0.58627592+0.j] Fidelity(pennylane, qiskit) = 1.0000000000 Fidelity(pennylane, cirq) = 1.0000000000 Fidelity(qiskit, cirq) = 1.0000000000
# Invalid backend raises ValueError
try:
enc.get_circuit(x, backend='invalid_backend')
except ValueError as e:
print(f"ValueError: {e}")
ValueError: Unknown backend 'invalid_backend'. Supported backends: 'pennylane', 'qiskit', 'cirq'
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
breakdown = enc.gate_count_breakdown()
print("Gate Count Breakdown:")
for key, val in breakdown.items():
print(f" {key:>20s}: {val}")
# Verify formulas
n, r = 4, 2
n_pairs = n - 1 # linear
print(f"\nExpected:")
print(f" RY gates: {n * r} (n * reps)")
print(f" CNOT gates: {n_pairs * r} ((n-1) * reps)")
print(f" Total: {n * r + n_pairs * r}")
Gate Count Breakdown:
rx: 0
ry: 8
rz: 0
cnot: 6
total_single_qubit: 8
total_two_qubit: 6
total: 14
Expected:
RY gates: 8 (n * reps)
CNOT gates: 6 ((n-1) * reps)
Total: 14
# Gate breakdown for each rotation type
for rot in ['X', 'Y', 'Z']:
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation=rot)
bd = enc.gate_count_breakdown()
print(f"rotation='{rot}': RX={bd['rx']}, RY={bd['ry']}, RZ={bd['rz']}, CNOT={bd['cnot']}")
rotation='X': RX=8, RY=0, RZ=0, CNOT=6 rotation='Y': RX=0, RY=8, RZ=0, CNOT=6 rotation='Z': RX=0, RY=0, RZ=8, CNOT=6
9.2 Resource Summary¶
resource_summary() returns a comprehensive dictionary with circuit structure, gate counts, encoding characteristics, and hardware requirements.
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
summary = enc.resource_summary()
print("=== Resource Summary ===")
print(f"\nCircuit Structure:")
print(f" n_qubits: {summary['n_qubits']}")
print(f" n_features: {summary['n_features']}")
print(f" depth: {summary['depth']}")
print(f" reps: {summary['reps']}")
print(f" rotation: {summary['rotation']}")
print(f" entanglement: {summary['entanglement']}")
print(f"\nGate Counts:")
for k, v in summary['gate_counts'].items():
print(f" {k:>20s}: {v}")
print(f"\nEncoding Characteristics:")
print(f" is_entangling: {summary['is_entangling']}")
print(f" simulability: {summary['simulability']}")
print(f" trainability_estimate: {summary['trainability_estimate']}")
print(f"\nHardware Requirements:")
print(f" connectivity: {summary['hardware_requirements']['connectivity']}")
print(f" native_gates: {summary['hardware_requirements']['native_gates']}")
print(f"\nEntanglement Details:")
print(f" n_entanglement_pairs: {summary['n_entanglement_pairs']}")
print(f" entanglement_pairs: {summary['entanglement_pairs']}")
=== Resource Summary ===
Circuit Structure:
n_qubits: 4
n_features: 4
depth: 4
reps: 2
rotation: Y
entanglement: linear
Gate Counts:
rx: 0
ry: 8
rz: 0
cnot: 6
total_single_qubit: 8
total_two_qubit: 6
total: 14
Encoding Characteristics:
is_entangling: True
simulability: not_simulable
trainability_estimate: 0.8
Hardware Requirements:
connectivity: linear
native_gates: ['RY', 'CNOT']
Entanglement Details:
n_entanglement_pairs: 3
entanglement_pairs: [(0, 1), (1, 2), (2, 3)]
# Compare hardware requirements across topologies
for ent in ['linear', 'circular', 'full']:
enc = HardwareEfficientEncoding(n_features=4, rotation='Z', entanglement=ent)
hw = enc.resource_summary()['hardware_requirements']
pairs = enc.resource_summary()['n_entanglement_pairs']
print(f"{ent:>8s}: connectivity={hw['connectivity']}, "
f"native_gates={hw['native_gates']}, pairs/layer={pairs}")
linear: connectivity=linear, native_gates=['RZ', 'CNOT'], pairs/layer=3
circular: connectivity=circular, native_gates=['RZ', 'CNOT'], pairs/layer=4
full: connectivity=full, native_gates=['RZ', 'CNOT'], pairs/layer=6
9.3 Resource Scaling Formulas¶
| Resource | Linear | Circular | Full |
|---|---|---|---|
| Qubits | n | n | n |
| Circuit Depth | 2r | 2r | 2r |
| Single-qubit gates | n*r | n*r | n*r |
| Two-qubit gates | (n-1)*r | n*r | n(n-1)/2*r |
| Parameters | n*r | n*r | n*r |
# Verify scaling formulas empirically
print(f"{'Config':>30s} | {'1Q gates':>8s} | {'2Q gates':>8s} | {'Total':>6s} | {'Depth':>5s}")
print("-" * 75)
for n in [2, 4, 6, 8]:
for r in [1, 2, 3]:
for ent in ['linear', 'circular', 'full']:
enc = HardwareEfficientEncoding(n_features=n, reps=r, entanglement=ent)
props = enc.properties
label = f"n={n}, r={r}, {ent}"
print(f"{label:>30s} | {props.single_qubit_gates:>8d} | "
f"{props.two_qubit_gates:>8d} | {props.gate_count:>6d} | {props.depth:>5d}")
Config | 1Q gates | 2Q gates | Total | Depth
---------------------------------------------------------------------------
n=2, r=1, linear | 2 | 1 | 3 | 2
n=2, r=1, circular | 2 | 1 | 3 | 2
n=2, r=1, full | 2 | 1 | 3 | 2
n=2, r=2, linear | 4 | 2 | 6 | 4
n=2, r=2, circular | 4 | 2 | 6 | 4
n=2, r=2, full | 4 | 2 | 6 | 4
n=2, r=3, linear | 6 | 3 | 9 | 6
n=2, r=3, circular | 6 | 3 | 9 | 6
n=2, r=3, full | 6 | 3 | 9 | 6
n=4, r=1, linear | 4 | 3 | 7 | 2
n=4, r=1, circular | 4 | 4 | 8 | 2
n=4, r=1, full | 4 | 6 | 10 | 2
n=4, r=2, linear | 8 | 6 | 14 | 4
n=4, r=2, circular | 8 | 8 | 16 | 4
n=4, r=2, full | 8 | 12 | 20 | 4
n=4, r=3, linear | 12 | 9 | 21 | 6
n=4, r=3, circular | 12 | 12 | 24 | 6
n=4, r=3, full | 12 | 18 | 30 | 6
n=6, r=1, linear | 6 | 5 | 11 | 2
n=6, r=1, circular | 6 | 6 | 12 | 2
n=6, r=1, full | 6 | 15 | 21 | 2
n=6, r=2, linear | 12 | 10 | 22 | 4
n=6, r=2, circular | 12 | 12 | 24 | 4
n=6, r=2, full | 12 | 30 | 42 | 4
n=6, r=3, linear | 18 | 15 | 33 | 6
n=6, r=3, circular | 18 | 18 | 36 | 6
n=6, r=3, full | 18 | 45 | 63 | 6
n=8, r=1, linear | 8 | 7 | 15 | 2
n=8, r=1, circular | 8 | 8 | 16 | 2
n=8, r=1, full | 8 | 28 | 36 | 2
n=8, r=2, linear | 16 | 14 | 30 | 4
n=8, r=2, circular | 16 | 16 | 32 | 4
n=8, r=2, full | 16 | 56 | 72 | 4
n=8, r=3, linear | 24 | 21 | 45 | 6
n=8, r=3, circular | 24 | 24 | 48 | 6
n=8, r=3, full | 24 | 84 | 108 | 6
10. Encoding Properties (EncodingProperties)¶
The properties attribute returns a frozen (immutable) EncodingProperties dataclass with comprehensive encoding information. It is lazily computed on first access and cached (thread-safe).
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
props = enc.properties
print(f"Type: {type(props).__name__}")
print(f"\n=== Required Fields ===")
print(f"n_qubits: {props.n_qubits}")
print(f"depth: {props.depth}")
print(f"gate_count: {props.gate_count}")
print(f"single_qubit_gates:{props.single_qubit_gates}")
print(f"two_qubit_gates: {props.two_qubit_gates}")
print(f"parameter_count: {props.parameter_count}")
print(f"is_entangling: {props.is_entangling}")
print(f"simulability: {props.simulability}")
print(f"\n=== Optional Fields ===")
print(f"expressibility: {props.expressibility}")
print(f"entanglement_capability: {props.entanglement_capability}")
print(f"trainability_estimate: {props.trainability_estimate}")
print(f"noise_resilience_est: {props.noise_resilience_estimate}")
print(f"notes: {props.notes}")
Type: EncodingProperties === Required Fields === n_qubits: 4 depth: 4 gate_count: 14 single_qubit_gates:8 two_qubit_gates: 6 parameter_count: 8 is_entangling: True simulability: not_simulable === Optional Fields === expressibility: None entanglement_capability: None trainability_estimate: 0.8 noise_resilience_est: None notes: Hardware-efficient with Y rotations and linear entanglement. Optimized for NISQ devices.
# Properties are immutable (frozen dataclass)
try:
props.n_qubits = 999
except AttributeError as e:
print(f"Cannot modify frozen properties: {e}")
Cannot modify frozen properties: cannot assign to field 'n_qubits'
# Convert properties to dictionary
props_dict = props.to_dict()
print("Properties as dict:")
for k, v in props_dict.items():
print(f" {k}: {v}")
Properties as dict: n_qubits: 4 depth: 4 gate_count: 14 single_qubit_gates: 8 two_qubit_gates: 6 parameter_count: 8 is_entangling: True simulability: not_simulable expressibility: None entanglement_capability: None trainability_estimate: 0.8 noise_resilience_estimate: None notes: Hardware-efficient with Y rotations and linear entanglement. Optimized for NISQ devices.
# Gate count consistency check (enforced by EncodingProperties)
assert props.single_qubit_gates + props.two_qubit_gates == props.gate_count
print(f"{props.single_qubit_gates} + {props.two_qubit_gates} = {props.gate_count} (verified)")
8 + 6 = 14 (verified)
11. Capability Protocols¶
The library uses PEP 544 Protocols (structural subtyping) to define optional capabilities. HardwareEfficientEncoding satisfies both ResourceAnalyzable and EntanglementQueryable.
enc = HardwareEfficientEncoding(n_features=4)
# Protocol checks using isinstance()
print("=== Protocol Compliance ===")
print(f"ResourceAnalyzable: {isinstance(enc, ResourceAnalyzable)}")
print(f"EntanglementQueryable: {isinstance(enc, EntanglementQueryable)}")
print(f"DataTransformable: {isinstance(enc, DataTransformable)}")
print(f"DataDependentResourceAnalyzable: {isinstance(enc, DataDependentResourceAnalyzable)}")
# Type guard functions
print(f"\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_transformable: {is_data_transformable(enc)}")
print(f"is_data_dependent_resource_analyzable:{is_data_dependent_resource_analyzable(enc)}")
=== Protocol Compliance === ResourceAnalyzable: True EntanglementQueryable: True DataTransformable: False DataDependentResourceAnalyzable: False === Type Guards === is_resource_analyzable: True is_entanglement_queryable: True is_data_transformable: False is_data_dependent_resource_analyzable:False
# Using protocols for generic, capability-aware code
def analyze_encoding(enc: BaseEncoding) -> None:
"""Analyze any encoding based on its capabilities."""
print(f"Encoding: {enc}")
# ResourceAnalyzable check
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
print(f" Total gates: {summary['gate_counts']['total']}")
else:
print(" (No resource analysis available)")
# EntanglementQueryable check
if isinstance(enc, EntanglementQueryable):
pairs = enc.get_entanglement_pairs()
print(f" Entanglement pairs: {len(pairs)}")
else:
print(" (No entanglement info available)")
# Use with HardwareEfficientEncoding
analyze_encoding(HardwareEfficientEncoding(n_features=4, entanglement='circular'))
Encoding: HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='circular') Total gates: 16 Entanglement pairs: 4
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
# count_resources - analytical resource counting
resources = count_resources(enc)
print("=== count_resources ===")
for k, v in resources.items():
print(f" {k}: {v}")
=== count_resources === n_qubits: 4 depth: 4 gate_count: 14 single_qubit_gates: 8 two_qubit_gates: 6 parameter_count: 8 cnot_count: 6 cz_count: 0 t_gate_count: 0 hadamard_count: 0 rotation_gates: 8 two_qubit_ratio: 0.42857142857142855 gates_per_qubit: 3.5 encoding_name: HardwareEfficientEncoding is_data_dependent: False
# get_resource_summary - quick summary from cached properties
quick_summary = get_resource_summary(enc)
print("=== get_resource_summary ===")
for k, v in quick_summary.items():
print(f" {k}: {v}")
=== get_resource_summary === n_qubits: 4 depth: 4 gate_count: 14 single_qubit_gates: 8 two_qubit_gates: 6 parameter_count: 8 cnot_count: 6 cz_count: 0 t_gate_count: 0 hadamard_count: 0 rotation_gates: 8 two_qubit_ratio: 0.42857142857142855 gates_per_qubit: 3.5 encoding_name: HardwareEfficientEncoding is_data_dependent: False
# get_gate_breakdown - detailed gate-by-gate breakdown
gate_detail = get_gate_breakdown(enc)
print("=== get_gate_breakdown ===")
for k, v in gate_detail.items():
print(f" {k}: {v}")
=== get_gate_breakdown === rx: 0 ry: 8 rz: 0 h: 0 x: 0 y: 0 z: 0 s: 0 t: 0 cnot: 6 cx: 6 cz: 0 swap: 0 total_single_qubit: 8 total_two_qubit: 6 total: 14 encoding_name: HardwareEfficientEncoding
# compare_resources - side-by-side comparison of multiple encodings
# Returns dict[str, list] mapping metric names to lists of values (one per encoding)
enc_lin = HardwareEfficientEncoding(n_features=4, reps=2, entanglement='linear')
enc_cir = HardwareEfficientEncoding(n_features=4, reps=2, entanglement='circular')
enc_ful = HardwareEfficientEncoding(n_features=4, reps=2, entanglement='full')
comparison = compare_resources([enc_lin, enc_cir, enc_ful])
print("=== compare_resources ===")
print(f"Keys: {list(comparison.keys())}")
n_enc = len(comparison['gate_count'])
for i in range(n_enc):
print(f" {comparison['encoding_name'][i]:>30s}: "
f"gates={comparison['gate_count'][i]:>3d}, "
f"2Q={comparison['two_qubit_gates'][i]:>3d}, "
f"depth={comparison['depth'][i]}")
=== compare_resources ===
Keys: ['n_qubits', 'depth', 'gate_count', 'single_qubit_gates', 'two_qubit_gates', 'parameter_count', 'two_qubit_ratio', 'gates_per_qubit', 'encoding_name']
HardwareEfficientEncoding: gates= 14, 2Q= 6, depth=4
HardwareEfficientEncoding: gates= 16, 2Q= 8, depth=4
HardwareEfficientEncoding: gates= 20, 2Q= 12, depth=4
# estimate_execution_time - timing estimate based on gate counts
time_est = estimate_execution_time(enc)
print(f"=== estimate_execution_time ===")
for k, v in time_est.items():
print(f" {k}: {v}")
=== estimate_execution_time === serial_time_us: 2.3600000000000003 estimated_time_us: 1.8 single_qubit_time_us: 0.16 two_qubit_time_us: 1.2000000000000002 measurement_time_us: 1.0 parallelization_factor: 0.5
12.2 Simulability Analysis (No Simulation Required)¶
# check_simulability - determines if encoding can be classically simulated
enc_multi = HardwareEfficientEncoding(n_features=4, reps=2)
enc_single = HardwareEfficientEncoding(n_features=1, reps=2)
for label, e in [("4-qubit", enc_multi), ("1-qubit", enc_single)]:
result = check_simulability(e)
print(f"\n--- {label} ---")
print(f" is_simulable: {result['is_simulable']}")
print(f" simulability_class:{result['simulability_class']}")
print(f" reason: {result['reason']}")
if result.get('recommendations'):
print(f" recommendations: {result['recommendations']}")
--- 4-qubit --- is_simulable: False simulability_class:conditionally_simulable reason: Linear entanglement structure may allow tensor network simulation if entanglement entropy is bounded recommendations: ['Statevector simulation feasible (4 qubits, ~256 bytes memory)', 'Consider MPS (Matrix Product State) simulation', 'May be efficient if entanglement entropy is bounded', 'Tensor network methods scale with bond dimension'] --- 1-qubit --- is_simulable: True simulability_class:simulable reason: Encoding produces only product states (no entanglement) recommendations: ['Can be simulated as independent single-qubit systems', 'Classical computation scales linearly with qubit count O(n)', 'Use standard numerical linear algebra for efficient simulation']
# is_clifford_circuit - Hardware-efficient encoding uses rotation gates, so NOT Clifford
enc = HardwareEfficientEncoding(n_features=4, reps=2)
print(f"Is Clifford circuit: {is_clifford_circuit(enc)}")
# is_matchgate_circuit
print(f"Is matchgate circuit: {is_matchgate_circuit(enc)}")
Is Clifford circuit: False Is matchgate circuit: False
# get_simulability_reason - concise explanation
reason = get_simulability_reason(enc)
print(f"Simulability reason: {reason}")
Simulability reason: Not simulable: Linear entanglement structure may allow tensor network simulation if entanglement entropy is bounded
12.3 Statevector Simulation¶
# simulate_encoding_statevector - simulate circuit and get the quantum state
enc = HardwareEfficientEncoding(n_features=3, reps=2, rotation='Y', entanglement='linear')
x = np.array([0.5, 1.0, 1.5])
state = simulate_encoding_statevector(enc, x, backend='pennylane')
print(f"Statevector shape: {state.shape}")
print(f"Statevector dimension: {len(state)} = 2^{enc.n_qubits}")
print(f"Norm: {np.linalg.norm(state):.10f}")
print(f"\nAmplitudes:")
for i, amp in enumerate(state):
if abs(amp) > 1e-10:
print(f" |{i:0{enc.n_qubits}b}> : {amp:.6f} (prob={abs(amp)**2:.6f})")
Statevector shape: (8,) Statevector dimension: 8 = 2^3 Norm: 1.0000000000 Amplitudes: |000> : 0.049322+0.000000j (prob=0.002433) |001> : 0.505484+0.000000j (prob=0.255515) |010> : 0.727803+0.000000j (prob=0.529697) |011> : 0.026945+0.000000j (prob=0.000726) |100> : 0.011032+0.000000j (prob=0.000122) |101> : 0.441033+0.000000j (prob=0.194510) |110> : 0.128802+0.000000j (prob=0.016590) |111> : 0.020193+0.000000j (prob=0.000408)
# Different inputs produce different quantum states
enc = HardwareEfficientEncoding(n_features=3, reps=1)
x1 = np.array([0.0, 0.0, 0.0])
x2 = np.array([np.pi, np.pi, np.pi])
x3 = np.array([0.5, 1.0, 1.5])
s1 = simulate_encoding_statevector(enc, x1)
s2 = simulate_encoding_statevector(enc, x2)
s3 = simulate_encoding_statevector(enc, x3)
print(f"Fidelity(x=0, x=pi): {compute_fidelity(s1, s2):.6f}")
print(f"Fidelity(x=0, x=mid): {compute_fidelity(s1, s3):.6f}")
print(f"Fidelity(x=pi, x=mid):{compute_fidelity(s2, s3):.6f}")
Fidelity(x=0, x=pi): 0.000000 Fidelity(x=0, x=mid): 0.387077 Fidelity(x=pi, x=mid):0.006537
12.4 Entanglement Bound Estimation¶
# estimate_entanglement_bound - upper bound on entanglement entropy
enc = HardwareEfficientEncoding(n_features=4, reps=2, entanglement='linear')
bound = estimate_entanglement_bound(enc)
print(f"Entanglement bound (linear, 4 qubits): {bound}")
enc_full = HardwareEfficientEncoding(n_features=4, reps=2, entanglement='full')
bound_full = estimate_entanglement_bound(enc_full)
print(f"Entanglement bound (full, 4 qubits): {bound_full}")
Entanglement bound (linear, 4 qubits): 1.7737504279591032 Entanglement bound (full, 4 qubits): 1.282067657983477
12.5 Encoding Validation¶
# validate_encoding_for_analysis - check if encoding is suitable for analysis
enc = HardwareEfficientEncoding(n_features=4, reps=2)
is_valid = validate_encoding_for_analysis(enc)
print(f"Encoding valid for analysis: {is_valid}")
Encoding valid for analysis: None
enc = HardwareEfficientEncoding(n_features=4)
# 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"Shape mismatch: {e}")
# Too many features
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4, 0.5])) # 5 features
except ValueError as e:
print(f"Too many: {e}")
# Wrong 2D shape
try:
enc.get_circuit(np.array([[0.1, 0.2, 0.3]])) # (1, 3) instead of (1, 4)
except ValueError as e:
print(f"Wrong 2D: {e}")
# 3D array
try:
enc.get_circuit(np.ones((2, 2, 4)))
except ValueError as e:
print(f"3D array: {e}")
Shape mismatch: Expected 4 features, got 3 Too many: Expected 4 features, got 5 Wrong 2D: Expected 4 features, got 3 3D array: Input must be 1D or 2D array, got 3D
13.2 Value Validation¶
enc = HardwareEfficientEncoding(n_features=3)
# NaN values
try:
enc.get_circuit(np.array([0.1, np.nan, 0.3]))
except ValueError as e:
print(f"NaN: {e}")
# Infinite values
try:
enc.get_circuit(np.array([0.1, np.inf, 0.3]))
except ValueError as e:
print(f"Inf: {e}")
# Negative infinity
try:
enc.get_circuit(np.array([0.1, -np.inf, 0.3]))
except ValueError as e:
print(f"-Inf: {e}")
NaN: Input contains NaN or infinite values Inf: Input contains NaN or infinite values -Inf: Input contains NaN or infinite values
13.3 Type Validation¶
enc = HardwareEfficientEncoding(n_features=3)
# String inputs rejected
try:
enc.get_circuit(["0.1", "0.2", "0.3"])
except TypeError as e:
print(f"String input: {e}")
# Complex numbers rejected
try:
enc.get_circuit(np.array([0.1 + 0.2j, 0.3, 0.4]))
except TypeError as e:
print(f"Complex input: {e}")
String input: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding. Complex input: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only.
13.4 Single-Qubit Edge Case¶
With n_features=1, the encoding has no entanglement and is classically simulable.
# Single qubit - minimal encoding
enc_1q = HardwareEfficientEncoding(n_features=1, reps=2, rotation='Y')
print(f"n_qubits: {enc_1q.n_qubits}")
print(f"n_features: {enc_1q.n_features}")
print(f"depth: {enc_1q.depth}")
print(f"is_entangling: {enc_1q.properties.is_entangling}")
print(f"simulability: {enc_1q.properties.simulability}")
print(f"gate_count: {enc_1q.properties.gate_count}")
print(f"two_qubit_gates:{enc_1q.properties.two_qubit_gates}")
# No entanglement pairs for any topology
for ent in ['linear', 'circular', 'full']:
e = HardwareEfficientEncoding(n_features=1, entanglement=ent)
print(f" {ent} pairs: {e.get_entanglement_pairs()}")
n_qubits: 1 n_features: 1 depth: 4 is_entangling: False simulability: simulable gate_count: 2 two_qubit_gates:0 linear pairs: [] circular pairs: [] full pairs: []
# Single qubit circuit generation works correctly
x = np.array([np.pi / 4])
state = simulate_encoding_statevector(enc_1q, x)
print(f"Single-qubit state: {state}")
print(f"Probabilities: |0>={abs(state[0])**2:.4f}, |1>={abs(state[1])**2:.4f}")
Single-qubit state: [0.70710678+0.j 0.70710678+0.j] Probabilities: |0>=0.5000, |1>=0.5000
13.5 Two-Qubit Edge Case¶
With n_features=2, linear and circular topologies produce identical entanglement pairs.
# At n=2, all entanglement topologies produce the same pairs
for ent in ['linear', 'circular', 'full']:
enc = HardwareEfficientEncoding(n_features=2, entanglement=ent)
print(f" {ent:>8s}: {enc.get_entanglement_pairs()}")
# Verify circuits produce the same state
x = np.array([0.5, 1.0])
states_2q = {}
for ent in ['linear', 'circular', 'full']:
enc = HardwareEfficientEncoding(n_features=2, reps=1, entanglement=ent)
states_2q[ent] = simulate_encoding_statevector(enc, x)
fid_lc = compute_fidelity(states_2q['linear'], states_2q['circular'])
fid_lf = compute_fidelity(states_2q['linear'], states_2q['full'])
print(f"\nFidelity(linear, circular) = {fid_lc:.10f}")
print(f"Fidelity(linear, full) = {fid_lf:.10f}")
linear: [(0, 1)]
circular: [(0, 1)]
full: [(0, 1)]
Fidelity(linear, circular) = 1.0000000000
Fidelity(linear, full) = 1.0000000000
13.6 Rotation Gate Periodicity (2pi)¶
Rotation gates are 2pi-periodic: R(theta) = R(theta + 2pik). Values outside the typical range still work, but a debug log is emitted.
enc = HardwareEfficientEncoding(n_features=2, reps=1)
# Demonstrate 2pi periodicity
x_base = np.array([0.5, 1.0])
x_shifted = x_base + 2 * np.pi # Shift by 2pi
s_base = simulate_encoding_statevector(enc, x_base)
s_shifted = simulate_encoding_statevector(enc, x_shifted)
fid = compute_fidelity(s_base, s_shifted)
print(f"x_base = {x_base}")
print(f"x_shifted = {x_shifted}")
print(f"Fidelity = {fid:.10f} (should be ~1.0, confirming 2pi periodicity)")
x_base = [0.5 1. ] x_shifted = [6.78318531 7.28318531] Fidelity = 1.0000000000 (should be ~1.0, confirming 2pi periodicity)
# Zero input encodes the |0...0> state (rotation by 0 does nothing except entanglement)
enc = HardwareEfficientEncoding(n_features=3, reps=1, rotation='Y', entanglement='linear')
x_zero = np.array([0.0, 0.0, 0.0])
state_zero = simulate_encoding_statevector(enc, x_zero)
print("State from zero input:")
for i, amp in enumerate(state_zero):
if abs(amp) > 1e-10:
print(f" |{i:0{enc.n_qubits}b}> : {amp:.6f}")
print(f"\nRY(0) = Identity, so CNOT on |0> states has no visible effect.")
print(f"P(|000>) = {abs(state_zero[0])**2:.6f}")
State from zero input: |000> : 1.000000+0.000000j RY(0) = Identity, so CNOT on |0> states has no visible effect. P(|000>) = 1.000000
13.7 Large Qubit Count Warnings¶
The encoding warns when using non-linear topologies with more than 20 qubits.
# Circular entanglement with >20 qubits triggers a UserWarning
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
enc_big_circ = HardwareEfficientEncoding(n_features=25, entanglement='circular')
if w:
print(f"Warning category: {w[0].category.__name__}")
print(f"Warning message: {w[0].message}")
else:
print("No warning (unexpected)")
Large qubit count (25) with circular entanglement may have hardware compatibility issues
Warning category: UserWarning Warning message: Using circular entanglement with 25 qubits. Circular topology requires wrap-around connectivity which may not be available on all quantum hardware. Consider using 'linear' entanglement for better hardware compatibility.
# Full entanglement with >20 qubits triggers a UserWarning about O(n^2) scaling
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
enc_big_full = HardwareEfficientEncoding(n_features=25, entanglement='full')
if w:
print(f"Warning category: {w[0].category.__name__}")
print(f"Warning message: {w[0].message}")
print(f"\nCNOT pairs per layer: {len(enc_big_full.get_entanglement_pairs())}")
print(f"Expected: 25*24/2 = {25*24//2}")
Large qubit count (25) with full entanglement: 300 CNOT pairs per layer (O(n²) scaling)
Warning category: UserWarning Warning message: Using full entanglement with 25 qubits creates 300 CNOT pairs per layer (O(n²) scaling). This requires all-to-all qubit connectivity, available on ion trap devices but not most superconducting hardware. Circuit depth and gate count grow quadratically with qubit count. CNOT pairs per layer: 300 Expected: 25*24/2 = 300
# Linear entanglement with >20 qubits does NOT trigger a warning
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
enc_big_lin = HardwareEfficientEncoding(n_features=25, entanglement='linear')
print(f"Number of warnings: {len(w)} (no warning for linear topology)")
Number of warnings: 0 (no warning for linear topology)
13.8 Input Immutability¶
The encoding creates defensive copies of input data, ensuring thread safety.
enc = HardwareEfficientEncoding(n_features=3)
x = np.array([0.1, 0.2, 0.3])
x_original = x.copy()
# Generate circuit
circuit = enc.get_circuit(x, backend='qiskit')
# Modify original array AFTER circuit generation
x[0] = 999.0
# The circuit was built with the original values, not the modified ones
print(f"Original x: {x_original}")
print(f"Modified x: {x}")
print(f"Circuit uses original values (defensive copy protects against mutation)")
Original x: [0.1 0.2 0.3] Modified x: [9.99e+02 2.00e-01 3.00e-01] Circuit uses original values (defensive copy protects against mutation)
# Encodings with same parameters are equal
enc1 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
enc2 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
print(f"Same params: {enc1 == enc2}") # True
print(f"Same object: {enc1 is enc2}") # False (different instances)
# Different parameters => not equal
enc3 = HardwareEfficientEncoding(n_features=4, reps=3, rotation='Y', entanglement='linear')
enc4 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='X', entanglement='linear')
enc5 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='circular')
enc6 = HardwareEfficientEncoding(n_features=5, reps=2, rotation='Y', entanglement='linear')
print(f"Different reps: {enc1 == enc3}") # False
print(f"Different rotation: {enc1 == enc4}") # False
print(f"Different entanglement:{enc1 == enc5}") # False
print(f"Different n_features: {enc1 == enc6}") # False
Same params: True Same object: False Different reps: False Different rotation: False Different entanglement:False Different n_features: False
# Cross-type comparison returns NotImplemented (Python falls back to identity)
print(f"Compared to string: {enc1 == 'not an encoding'}")
print(f"Compared to int: {enc1 == 42}")
print(f"Compared to None: {enc1 == None}")
Compared to string: False Compared to int: False Compared to None: False
14.2 Hashing (Usable as Dict Keys and in Sets)¶
enc1 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
enc2 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
enc3 = HardwareEfficientEncoding(n_features=4, reps=2, rotation='X', entanglement='linear')
# Same config => same hash
print(f"hash(enc1) == hash(enc2): {hash(enc1) == hash(enc2)}")
print(f"hash(enc1) == hash(enc3): {hash(enc1) == hash(enc3)}")
# Usable in sets
encoding_set = {enc1, enc2, enc3}
print(f"\nSet of encodings: {len(encoding_set)} unique (enc1 and enc2 deduplicated)")
# Usable as dict keys
results = {enc1: "best", enc3: "good"}
print(f"Lookup by equal encoding: results[enc2] = '{results[enc2]}'")
hash(enc1) == hash(enc2): True hash(enc1) == hash(enc3): False Set of encodings: 2 unique (enc1 and enc2 deduplicated) Lookup by equal encoding: results[enc2] = 'best'
14.3 Pickle Serialization¶
enc = HardwareEfficientEncoding(n_features=4, reps=3, rotation='Z', entanglement='full')
# Force property computation before serialization
_ = enc.properties
# Serialize
serialized = pickle.dumps(enc)
print(f"Serialized size: {len(serialized)} bytes")
# Deserialize
enc_restored = pickle.loads(serialized)
# Verify all attributes preserved
print(f"\nOriginal: {enc}")
print(f"Restored: {enc_restored}")
print(f"Equal: {enc == enc_restored}")
print(f"n_features:{enc_restored.n_features}")
print(f"reps: {enc_restored.reps}")
print(f"rotation: {enc_restored.rotation}")
print(f"entangle: {enc_restored.entanglement}")
print(f"pairs: {enc_restored.get_entanglement_pairs()}")
# Cached properties are preserved (no recomputation needed)
print(f"\nProperties preserved: {enc_restored.properties == enc.properties}")
print(f"Gate count: {enc_restored.properties.gate_count}")
Serialized size: 697 bytes Original: HardwareEfficientEncoding(n_features=4, reps=3, rotation='Z', entanglement='full') Restored: HardwareEfficientEncoding(n_features=4, reps=3, rotation='Z', entanglement='full') Equal: True n_features:4 reps: 3 rotation: Z entangle: full pairs: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] Properties preserved: True Gate count: 30
# Restored encoding generates identical circuits
x = np.array([0.1, 0.5, 1.0, 2.0])
state_orig = simulate_encoding_statevector(enc, x)
state_rest = simulate_encoding_statevector(enc_restored, x)
fid = compute_fidelity(state_orig, state_rest)
print(f"Fidelity(original, restored) = {fid:.10f}")
Fidelity(original, restored) = 1.0000000000
import concurrent.futures
import threading
enc = HardwareEfficientEncoding(n_features=4, reps=2)
# Concurrent circuit generation from multiple threads
rng = np.random.default_rng(42)
inputs = [rng.uniform(0, 2 * np.pi, size=4) for _ in range(20)]
def generate_circuit(x):
"""Generate a circuit in a separate thread."""
tid = threading.current_thread().name
circuit = enc.get_circuit(x, backend='qiskit')
return (tid, circuit.num_qubits)
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(generate_circuit, inputs))
print(f"Successfully generated {len(results)} circuits across threads")
threads_used = set(r[0] for r in results)
print(f"Threads used: {len(threads_used)}")
print(f"All have correct qubit count: {all(r[1] == 4 for r in results)}")
Successfully generated 20 circuits across threads Threads used: 2 All have correct qubit count: True
# Thread-safe lazy property initialization
# Multiple threads accessing .properties simultaneously is safe
enc_fresh = HardwareEfficientEncoding(n_features=4, reps=2)
properties_seen = []
def access_properties():
props = enc_fresh.properties
return props.gate_count
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
futures = [executor.submit(access_properties) for _ in range(50)]
gate_counts = [f.result() for f in futures]
print(f"All 50 threads got same gate_count: {len(set(gate_counts)) == 1}")
print(f"Gate count: {gate_counts[0]}")
All 50 threads got same gate_count: True Gate count: 14
# Set up logging to see debug output
logger = logging.getLogger('encoding_atlas.encodings.hardware_efficient')
logger.setLevel(logging.DEBUG)
# Create a handler to display logs in the notebook
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
# Now create an encoding and generate a circuit - debug logs will be visible
print("=== Creating encoding ===")
enc_debug = HardwareEfficientEncoding(n_features=3, reps=1, rotation='Y', entanglement='linear')
print("\n=== Generating circuit ===")
x = np.array([0.5, 1.0, 1.5])
circuit = enc_debug.get_circuit(x, backend='qiskit')
# Clean up handler to prevent duplicate logs
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
=== Creating encoding ===
encoding_atlas.encodings.hardware_efficient - DEBUG - Computed entanglement pairs for linear topology: 2 pairs encoding_atlas.encodings.hardware_efficient - DEBUG - HardwareEfficientEncoding initialized: n_features=3, reps=1, rotation='Y', entanglement='linear', n_entanglement_pairs=2 encoding_atlas.encodings.hardware_efficient - DEBUG - Generating circuit: backend='qiskit', input_shape=(3,) encoding_atlas.encodings.hardware_efficient - DEBUG - Circuit generated successfully for backend='qiskit'
=== Generating circuit ===
# Large input values trigger a debug-level range warning
logger = logging.getLogger('encoding_atlas.encodings.hardware_efficient')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
logger.addHandler(handler)
print("=== Input outside typical range ===")
x_large = np.array([100.0, 200.0, 300.0])
enc_debug = HardwareEfficientEncoding(n_features=3, reps=1)
circuit = enc_debug.get_circuit(x_large, backend='qiskit')
print("(Circuit still generated - values are valid but rotation gates are 2pi-periodic)")
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
=== Input outside typical range ===
DEBUG - Computed entanglement pairs for linear topology: 2 pairs DEBUG - HardwareEfficientEncoding initialized: n_features=3, reps=1, rotation='Y', entanglement='linear', n_entanglement_pairs=2 DEBUG - Generating circuit: backend='qiskit', input_shape=(3,) DEBUG - Input values [100, 300] are outside typical range [-2π, 2π]. Rotation gates are periodic with period 2π. Consider scaling features to [0, 2π] or [-π, π] for optimal encoding. DEBUG - Circuit generated successfully for backend='qiskit'
(Circuit still generated - values are valid but rotation gates are 2pi-periodic)
17. Comparison with Other Encodings¶
Compare HardwareEfficientEncoding with other encodings from the library.
from encoding_atlas import (
AngleEncoding,
IQPEncoding,
ZZFeatureMap,
DataReuploading,
)
# Create encodings with comparable settings
n = 4
encodings = [
AngleEncoding(n_features=n, reps=2),
HardwareEfficientEncoding(n_features=n, reps=2, entanglement='linear'),
HardwareEfficientEncoding(n_features=n, reps=2, entanglement='circular'),
HardwareEfficientEncoding(n_features=n, reps=2, entanglement='full'),
IQPEncoding(n_features=n, reps=2),
ZZFeatureMap(n_features=n, reps=2),
]
# Resource comparison (returns dict[str, list])
comparison = compare_resources(encodings)
print(f"{'Encoding':>30s} | {'Gates':>5s} | {'1Q':>4s} | {'2Q':>4s} | {'Depth':>5s}")
print("-" * 60)
for i in range(len(comparison['gate_count'])):
print(f"{comparison['encoding_name'][i]:>30s} | {comparison['gate_count'][i]:>5d} | "
f"{comparison['single_qubit_gates'][i]:>4d} | {comparison['two_qubit_gates'][i]:>4d} | "
f"{comparison['depth'][i]:>5d}")
Encoding | Gates | 1Q | 2Q | Depth
------------------------------------------------------------
AngleEncoding | 8 | 8 | 0 | 2
HardwareEfficientEncoding | 14 | 8 | 6 | 4
HardwareEfficientEncoding | 16 | 8 | 8 | 4
HardwareEfficientEncoding | 20 | 8 | 12 | 4
IQPEncoding | 52 | 28 | 24 | 6
ZZFeatureMap | 52 | 28 | 24 | 22
# Simulability comparison
print(f"{'Encoding':>55s} | {'Simulable':>10s} | {'Entangling':>10s}")
print("-" * 85)
for enc in encodings:
sim = check_simulability(enc)
props = enc.properties
print(f"{repr(enc):>55s} | {sim['simulability_class']:>10s} | "
f"{str(props.is_entangling):>10s}")
Encoding | Simulable | Entangling
-------------------------------------------------------------------------------------
AngleEncoding(n_features=4, rotation='Y', reps=2) | simulable | False
HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear') | conditionally_simulable | True
HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='circular') | conditionally_simulable | True
HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='full') | not_simulable | True
IQPEncoding(n_features=4, reps=2, entanglement='full') | not_simulable | True
ZZFeatureMap(n_features=4, reps=2, entanglement='full') | not_simulable | True
# Optimal config for IBM/Google superconducting hardware
enc_sc = HardwareEfficientEncoding(
n_features=4,
reps=2,
rotation='Z', # Virtual RZ gates: zero error
entanglement='linear', # Matches nearest-neighbor connectivity
)
summary = enc_sc.resource_summary()
print("Superconducting Hardware Config:")
print(f" Native gates: {summary['hardware_requirements']['native_gates']}")
print(f" Connectivity: {summary['hardware_requirements']['connectivity']}")
print(f" Total gates: {summary['gate_counts']['total']}")
print(f" Circuit depth: {summary['depth']}")
print(f" 2Q gate ratio: {summary['gate_counts']['total_two_qubit'] / summary['gate_counts']['total']:.2%}")
Superconducting Hardware Config: Native gates: ['RZ', 'CNOT'] Connectivity: linear Total gates: 14 Circuit depth: 4 2Q gate ratio: 42.86%
18.2 Ion Trap Deployment¶
Full entanglement exploits the all-to-all connectivity of ion trap devices.
# Optimal config for IonQ/Quantinuum ion trap hardware
enc_ion = HardwareEfficientEncoding(
n_features=4,
reps=2,
rotation='Y', # RY gates for real amplitudes
entanglement='full', # All-to-all connectivity on ion traps
)
summary = enc_ion.resource_summary()
print("Ion Trap Hardware Config:")
print(f" Native gates: {summary['hardware_requirements']['native_gates']}")
print(f" Connectivity: {summary['hardware_requirements']['connectivity']}")
print(f" Total gates: {summary['gate_counts']['total']}")
print(f" CNOT gates: {summary['gate_counts']['cnot']}")
print(f" Entanglement pairs per layer: {summary['n_entanglement_pairs']}")
Ion Trap Hardware Config: Native gates: ['RY', 'CNOT'] Connectivity: full Total gates: 20 CNOT gates: 12 Entanglement pairs per layer: 6
18.3 Feature Encoding with Preprocessing¶
# Simulate encoding real-world data
# Features should be scaled to [0, 2*pi] for rotation gates
rng = np.random.default_rng(42)
# Raw features with different scales
raw_features = rng.uniform(low=[0, -100, 0.001, 50],
high=[1, 100, 0.01, 150],
size=(5, 4))
print("Raw features (first sample):")
print(f" {raw_features[0]}")
print(f" Ranges: min={raw_features.min(axis=0)}, max={raw_features.max(axis=0)}")
# Scale to [0, 2*pi]
from encoding_atlas.utils import scale_features
scaled = scale_features(raw_features, range_min=0, range_max=2 * np.pi)
print(f"\nScaled features (first sample):")
print(f" {scaled[0]}")
print(f" Range: [{scaled.min():.3f}, {scaled.max():.3f}]")
# Generate circuits for all samples
enc = HardwareEfficientEncoding(n_features=4, reps=2)
circuits = enc.get_circuits(scaled, backend='qiskit')
print(f"\nGenerated {len(circuits)} circuits for {len(raw_features)} samples")
Raw features (first sample): [ 7.73956049e-01 -1.22243120e+01 8.72738128e-03 1.19736803e+02] Ranges: min=[ 9.41773479e-02 -8.72365488e+01 4.33718222e-03 7.27238722e+01], max=[7.73956049e-01 9.51244703e+01 8.72738128e-03 1.42676499e+02] Scaled features (first sample): [2.4051976 2.04997406 2.38428503 5.65627717] Range: [0.000, 6.283] Generated 5 circuits for 5 samples
18.4 Comparing Rotation Axes on the Same Data¶
# How rotation axis affects the encoding of the same data
x = np.array([0.5, 1.0, 1.5, 2.0])
print(f"Input: {x}")
print()
for rot in ['X', 'Y', 'Z']:
enc = HardwareEfficientEncoding(n_features=4, reps=1, rotation=rot, entanglement='linear')
state = simulate_encoding_statevector(enc, x)
# Show probability distribution
probs = np.abs(state) ** 2
top_idx = np.argsort(probs)[::-1][:3]
top_states = [(f"|{i:04b}>", probs[i]) for i in top_idx]
print(f"R{rot} encoding - top 3 states:")
for label, p in top_states:
print(f" {label}: {p:.4f}")
print()
Input: [0.5 1. 1.5 2. ] RX encoding - top 3 states: |0001>: 0.2741 |0010>: 0.2379 |0000>: 0.1130 RY encoding - top 3 states: |0001>: 0.2741 |0010>: 0.2379 |0000>: 0.1130 RZ encoding - top 3 states: |0000>: 1.0000 |1111>: 0.0000 |1101>: 0.0000
18.5 Expressivity vs. Depth Trade-off¶
# Show how increasing reps affects the encoding
x = np.array([0.5, 1.0, 1.5])
print(f"{'reps':>4s} | {'depth':>5s} | {'gates':>5s} | {'2Q gates':>8s} | {'P(|000>)':>10s}")
print("-" * 50)
for r in [1, 2, 3, 5, 10]:
enc = HardwareEfficientEncoding(n_features=3, reps=r, entanglement='linear')
state = simulate_encoding_statevector(enc, x)
p000 = abs(state[0]) ** 2
props = enc.properties
print(f"{r:>4d} | {props.depth:>5d} | {props.gate_count:>5d} | "
f"{props.two_qubit_gates:>8d} | {p000:>10.6f}")
reps | depth | gates | 2Q gates | P(|000>) -------------------------------------------------- 1 | 2 | 5 | 2 | 0.387077 2 | 4 | 10 | 4 | 0.002433 3 | 6 | 15 | 6 | 0.183240 5 | 10 | 25 | 10 | 0.002864 10 | 20 | 50 | 20 | 0.001735
18.6 Visualizing a Qiskit Circuit¶
# Generate and display a Qiskit circuit
if is_qiskit_available():
enc = HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear')
x = np.array([0.5, 1.0, 1.5, 2.0])
qc = enc.get_circuit(x, backend='qiskit')
print(qc.draw())
print(f"\nCircuit stats: qubits={qc.num_qubits}, depth={qc.depth()}, "
f"ops={qc.count_ops()}")
┌─────────┐ ┌─────────┐
q_0: ┤ Ry(0.5) ├──■──┤ Ry(0.5) ├──────────────■───────────────
└┬───────┬┘┌─┴─┐└─────────┘┌───────┐ ┌─┴─┐
q_1: ─┤ Ry(1) ├─┤ X ├─────■─────┤ Ry(1) ├───┤ X ├─────■───────
┌┴───────┴┐└───┘ ┌─┴─┐ └───────┘┌──┴───┴──┐┌─┴─┐
q_2: ┤ Ry(1.5) ├────────┤ X ├───────■────┤ Ry(1.5) ├┤ X ├──■──
└┬───────┬┘ └───┘ ┌─┴─┐ └┬───────┬┘└───┘┌─┴─┐
q_3: ─┤ Ry(2) ├───────────────────┤ X ├───┤ Ry(2) ├──────┤ X ├
└───────┘ └───┘ └───────┘ └───┘
Circuit stats: qubits=4, depth=7, ops=OrderedDict([('ry', 8), ('cx', 6)])
# Visualize each entanglement topology side by side
if is_qiskit_available():
x = np.array([0.5, 1.0, 1.5, 2.0])
for ent in ['linear', 'circular', 'full']:
enc = HardwareEfficientEncoding(n_features=4, reps=1, rotation='Y', entanglement=ent)
qc = enc.get_circuit(x, backend='qiskit')
print(f"\n=== {ent.upper()} entanglement (1 rep) ===")
print(qc.draw())
print(f"Ops: {qc.count_ops()}")
=== LINEAR entanglement (1 rep) ===
┌─────────┐
q_0: ┤ Ry(0.5) ├──■────────────
└┬───────┬┘┌─┴─┐
q_1: ─┤ Ry(1) ├─┤ X ├──■───────
┌┴───────┴┐└───┘┌─┴─┐
q_2: ┤ Ry(1.5) ├─────┤ X ├──■──
└┬───────┬┘ └───┘┌─┴─┐
q_3: ─┤ Ry(2) ├───────────┤ X ├
└───────┘ └───┘
Ops: OrderedDict([('ry', 4), ('cx', 3)])
=== CIRCULAR entanglement (1 rep) ===
┌─────────┐ ┌───┐
q_0: ┤ Ry(0.5) ├──■────────────┤ X ├
└┬───────┬┘┌─┴─┐ └─┬─┘
q_1: ─┤ Ry(1) ├─┤ X ├──■─────────┼──
┌┴───────┴┐└───┘┌─┴─┐ │
q_2: ┤ Ry(1.5) ├─────┤ X ├──■────┼──
└┬───────┬┘ └───┘┌─┴─┐ │
q_3: ─┤ Ry(2) ├───────────┤ X ├──■──
└───────┘ └───┘
Ops: OrderedDict([('ry', 4), ('cx', 4)])
=== FULL entanglement (1 rep) ===
┌─────────┐
q_0: ┤ Ry(0.5) ├──■────■────■─────────────────
└┬───────┬┘┌─┴─┐ │ │
q_1: ─┤ Ry(1) ├─┤ X ├──┼────┼────■────■───────
┌┴───────┴┐└───┘┌─┴─┐ │ ┌─┴─┐ │
q_2: ┤ Ry(1.5) ├─────┤ X ├──┼──┤ X ├──┼────■──
└┬───────┬┘ └───┘┌─┴─┐└───┘┌─┴─┐┌─┴─┐
q_3: ─┤ Ry(2) ├───────────┤ X ├─────┤ X ├┤ X ├
└───────┘ └───┘ └───┘└───┘
Ops: OrderedDict([('cx', 6), ('ry', 4)])
18.7 Registry and Discovery¶
from encoding_atlas import list_encodings, get_encoding
# List all available encodings in the library
all_encodings = list_encodings()
print(f"Total encodings available: {len(all_encodings)}")
for name in sorted(all_encodings):
print(f" - {name}")
Total encodings available: 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
# Get an encoding instance by name from the registry
enc_from_registry = get_encoding('hardware_efficient', n_features=4, reps=2)
print(f"Created from registry: {enc_from_registry}")
print(f"Type: {type(enc_from_registry).__name__}")
print(f"Is HardwareEfficientEncoding: {isinstance(enc_from_registry, HardwareEfficientEncoding)}")
Created from registry: HardwareEfficientEncoding(n_features=4, reps=2, rotation='Y', entanglement='linear') Type: HardwareEfficientEncoding Is HardwareEfficientEncoding: True
Summary¶
This notebook demonstrated every feature of HardwareEfficientEncoding from the Quantum Encoding Atlas library:
Core Features:
- Constructor with 4 parameters:
n_features,reps,rotation,entanglement - Three rotation axes (RX, RY, RZ) and three entanglement topologies (linear, circular, full)
- Circuit generation for single samples (
get_circuit) and batches (get_circuits) - Multi-backend support: PennyLane, Qiskit, Cirq
Analysis & Introspection:
- Resource analysis:
gate_count_breakdown(),resource_summary() - Encoding properties:
properties(lazy, thread-safe, cachedEncodingProperties) - Capability protocols:
ResourceAnalyzable,EntanglementQueryable - Analysis module:
count_resources,check_simulability,compare_resources,simulate_encoding_statevector, etc.
Robustness:
- Comprehensive input validation (shape, dtype, NaN/Inf, complex numbers, strings)
- Thread-safe design (defensive copies, double-checked locking)
- Pickle serialization with full state preservation
- Equality and hashing for use in sets and dicts
- Warnings for large qubit counts with non-linear topologies
- Configurable logging for debugging
Edge Cases:
- Single qubit (n=1): no entanglement, classically simulable
- Two qubits (n=2): all topologies equivalent
- 2pi periodicity of rotation gates
- Zero input encodes |0...0>
- Large qubit warnings (n > 20)