AngleEncoding: Complete Feature Demonstration¶
Library: encoding-atlas
This notebook provides an exhaustive, hands-on demonstration of AngleEncoding from the Quantum Encoding Atlas library. AngleEncoding is a fundamental quantum data encoding technique that maps classical features directly to rotation angles of single-qubit quantum gates.
What This Notebook Covers¶
| # | Section | Description |
|---|---|---|
| 1 | Installation & Setup | Installing the library and verifying the environment |
| 2 | Creating an AngleEncoding | Constructor parameters, defaults, and validation |
| 3 | Core Properties | n_qubits, depth, n_features, config |
| 4 | Encoding Properties (Lazy) | Thread-safe EncodingProperties dataclass |
| 5 | Circuit Generation — PennyLane | Generating and executing PennyLane circuits |
| 6 | Circuit Generation — Qiskit | Generating and visualizing Qiskit circuits |
| 7 | Circuit Generation — Cirq | Generating and inspecting Cirq circuits |
| 8 | Rotation Axis Comparison (RX, RY, RZ) | How each axis affects quantum states |
| 9 | Repetitions (reps) — Deepening the Encoding |
Effect of multiple layers |
| 10 | Batch Circuit Generation | get_circuits() — sequential and parallel |
| 11 | Input Validation & Edge Cases | Robust error handling demonstration |
| 12 | Resource Analysis | gate_count_breakdown() and resource_summary() |
| 13 | Simulability Analysis | Classical simulability checks |
| 14 | Expressibility Analysis | Hilbert space coverage measurement |
| 15 | Entanglement Capability | Verifying zero entanglement (product states) |
| 16 | Trainability Analysis | Barren plateau detection and gradient variance |
| 17 | Low-Level Utilities | Statevector simulation, fidelity, purity, entropy |
| 18 | Capability Protocols | ResourceAnalyzable, EntanglementQueryable, etc. |
| 19 | Registry System | Creating encodings by name via get_encoding() |
| 20 | Equality, Hashing & Serialization | __eq__, __hash__, pickle round-trip |
| 21 | Thread Safety | Concurrent circuit generation |
| 22 | Logging & Debugging | Enabling debug logs for troubleshooting |
| 23 | Encoding Recommendation Guide | Using the decision guide with AngleEncoding |
| 24 | Visualization & Comparison | Comparing AngleEncoding to other encodings |
| 25 | Mathematical Background | The theory behind angle encoding |
Mathematical Background¶
AngleEncoding creates quantum states of the form:
$$|\psi(\mathbf{x})\rangle = \bigotimes_{i=0}^{n-1} R_a(x_i)|0\rangle$$
where $R_a \in \{R_X, R_Y, R_Z\}$ is the rotation gate around axis $a$, and $x_i$ is the $i$-th feature value used as the rotation angle. The single-qubit rotation gates are:
$$R_X(\theta) = \cos(\theta/2)I - i\sin(\theta/2)X, \quad R_Y(\theta) = \cos(\theta/2)I - i\sin(\theta/2)Y, \quad R_Z(\theta) = e^{-i\theta/2}|0\rangle\langle 0| + e^{i\theta/2}|1\rangle\langle 1|$$
Because the state is a tensor product of individual qubit states, there is no entanglement, making AngleEncoding:
- Classically simulable with $O(n)$ complexity
- Free from barren plateaus (high trainability)
- Hardware-efficient (only single-qubit gates, no qubit connectivity required)
1. Installation & Setup¶
# Install the library (uncomment if not already installed)
#!pip install encoding-atlas
# For full multi-backend support, also install:
# pip install encoding-atlas[qiskit] # Qiskit backend
# pip install encoding-atlas[cirq] # Cirq backend
# pip install encoding-atlas[viz] # Matplotlib visualizations
# Or install everything at once:
#!pip install encoding-atlas[qiskit,cirq,viz]
import numpy as np
import encoding_atlas
print(f"encoding-atlas version: {encoding_atlas.__version__}")
print(f"NumPy version: {np.__version__}")
encoding-atlas version: 0.2.0 NumPy version: 2.2.6
# Check which backends are available
backends_available = {}
try:
import pennylane as qml
backends_available['pennylane'] = qml.__version__
except ImportError:
backends_available['pennylane'] = 'NOT INSTALLED'
try:
import qiskit
backends_available['qiskit'] = qiskit.__version__
except ImportError:
backends_available['qiskit'] = 'NOT INSTALLED'
try:
import cirq
backends_available['cirq'] = cirq.__version__
except ImportError:
backends_available['cirq'] = 'NOT INSTALLED'
print("Backend availability:")
for backend, version in backends_available.items():
status = "Available" if version != 'NOT INSTALLED' else "Not installed"
print(f" {backend:12s}: {status} ({version})")
Backend availability: pennylane : Available (0.42.3) qiskit : Available (2.3.0) cirq : Available (1.5.0)
2. Creating an AngleEncoding¶
The AngleEncoding constructor accepts three parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
n_features |
int |
required | Number of classical features to encode (also determines qubit count) |
rotation |
"X", "Y", "Z" |
"Y" |
Rotation axis for the encoding gates |
reps |
int |
1 |
Number of times to repeat the encoding layer |
from encoding_atlas import AngleEncoding
# Basic creation with defaults (RY rotation, 1 repetition)
enc_default = AngleEncoding(n_features=4)
print(f"Default: {enc_default}")
# Specify rotation axis
enc_rx = AngleEncoding(n_features=4, rotation="X")
enc_ry = AngleEncoding(n_features=4, rotation="Y")
enc_rz = AngleEncoding(n_features=4, rotation="Z")
print(f"RX axis: {enc_rx}")
print(f"RY axis: {enc_ry}")
print(f"RZ axis: {enc_rz}")
# Specify repetitions
enc_reps = AngleEncoding(n_features=3, rotation="Y", reps=3)
print(f"3 reps: {enc_reps}")
# Single feature (minimum)
enc_single = AngleEncoding(n_features=1)
print(f"Single: {enc_single}")
Default: AngleEncoding(n_features=4, rotation='Y', reps=1) RX axis: AngleEncoding(n_features=4, rotation='X', reps=1) RY axis: AngleEncoding(n_features=4, rotation='Y', reps=1) RZ axis: AngleEncoding(n_features=4, rotation='Z', reps=1) 3 reps: AngleEncoding(n_features=3, rotation='Y', reps=3) Single: AngleEncoding(n_features=1, rotation='Y', reps=1)
2.1 Constructor Validation¶
The constructor validates all parameters strictly. Let's verify each validation rule.
# --- Invalid n_features ---
print("=== n_features validation ===")
# n_features must be a positive integer
for bad_n in [0, -1, -5]:
try:
AngleEncoding(n_features=bad_n)
except ValueError as e:
print(f" n_features={bad_n!r}: ValueError - {e}")
# Non-integer types are rejected
for bad_n in [2.5, "4", None]:
try:
AngleEncoding(n_features=bad_n)
except (ValueError, TypeError) as e:
print(f" n_features={bad_n!r}: {type(e).__name__} - {e}")
=== n_features validation === n_features=0: ValueError - n_features must be a positive integer, got 0 n_features=-1: ValueError - n_features must be a positive integer, got -1 n_features=-5: ValueError - n_features must be a positive integer, got -5 n_features=2.5: ValueError - n_features must be a positive integer, got 2.5 n_features='4': ValueError - n_features must be a positive integer, got 4 n_features=None: ValueError - n_features must be a positive integer, got None
# --- Invalid rotation ---
print("=== rotation validation ===")
for bad_rot in ["x", "y", "z", "A", "RX", "XY", 1, None]:
try:
AngleEncoding(n_features=4, rotation=bad_rot)
except (ValueError, TypeError) as e:
print(f" rotation={bad_rot!r}: {type(e).__name__} - {e}")
=== rotation validation === 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='A': ValueError - rotation must be one of ['X', 'Y', 'Z'], got 'A' rotation='RX': ValueError - rotation must be one of ['X', 'Y', 'Z'], got 'RX' rotation='XY': ValueError - rotation must be one of ['X', 'Y', 'Z'], got 'XY' rotation=1: ValueError - rotation must be one of ['X', 'Y', 'Z'], got 1 rotation=None: ValueError - rotation must be one of ['X', 'Y', 'Z'], got None
# --- Invalid reps ---
print("=== reps validation ===")
for bad_reps in [0, -1, 1.5, True, False]:
try:
AngleEncoding(n_features=4, reps=bad_reps)
except ValueError as e:
print(f" reps={bad_reps!r}: ValueError - {e}")
# Note: True and False are rejected even though bool is a subclass of int
print("\n (bool values are explicitly rejected to prevent accidental misuse)")
=== reps validation === reps=0: ValueError - reps must be 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 (bool values are explicitly rejected to prevent accidental misuse)
3. Core Properties¶
AngleEncoding exposes several properties inherited from BaseEncoding plus its own attributes.
enc = AngleEncoding(n_features=4, rotation="Y", reps=2)
print("=== Core Properties ===")
print(f" n_features : {enc.n_features} (number of classical features)")
print(f" n_qubits : {enc.n_qubits} (one qubit per feature)")
print(f" depth : {enc.depth} (equals reps — all rotations are parallel per layer)")
print(f" rotation : {enc.rotation!r} (rotation axis)")
print(f" reps : {enc.reps} (number of encoding layer repetitions)")
# n_qubits always equals n_features for AngleEncoding
assert enc.n_qubits == enc.n_features, "n_qubits should equal n_features"
# depth always equals reps
assert enc.depth == enc.reps, "depth should equal reps"
print("\n Assertions passed: n_qubits == n_features, depth == reps")
=== Core Properties === n_features : 4 (number of classical features) n_qubits : 4 (one qubit per feature) depth : 2 (equals reps — all rotations are parallel per layer) rotation : 'Y' (rotation axis) reps : 2 (number of encoding layer repetitions) Assertions passed: n_qubits == n_features, depth == reps
# The config property returns a copy of the encoding-specific parameters
config = enc.config
print(f"config = {config}")
print(f"type = {type(config).__name__}")
# It's a defensive copy — modifying it doesn't affect the encoding
config['rotation'] = 'MODIFIED'
config['reps'] = 999
print(f"\nAfter modifying the returned dict:")
print(f" enc.config = {enc.config} (unchanged)")
print(f" enc.rotation = {enc.rotation!r} (unchanged)")
config = {'rotation': 'Y', 'reps': 2}
type = dict
After modifying the returned dict:
enc.config = {'rotation': 'Y', 'reps': 2} (unchanged)
enc.rotation = 'Y' (unchanged)
4. Encoding Properties (Lazy, Thread-Safe)¶
The properties attribute returns an EncodingProperties frozen dataclass. It is:
- Lazily computed on first access (not at construction time)
- Thread-safe via double-checked locking
- Cached after first computation
enc = AngleEncoding(n_features=4, rotation="Y", reps=2)
props = enc.properties
print(f"type: {type(props).__name__}")
print(f"")
print(f"=== EncodingProperties ===")
print(f" n_qubits : {props.n_qubits}")
print(f" depth : {props.depth}")
print(f" gate_count : {props.gate_count} (n_features * reps = 4 * 2)")
print(f" single_qubit_gates : {props.single_qubit_gates}")
print(f" two_qubit_gates : {props.two_qubit_gates} (always 0 — no entanglement)")
print(f" parameter_count : {props.parameter_count} (one data-dependent param per gate)")
print(f" is_entangling : {props.is_entangling}")
print(f" simulability : {props.simulability!r}")
print(f" trainability_estimate: {props.trainability_estimate}")
print(f" notes : {props.notes!r}")
type: EncodingProperties === EncodingProperties === n_qubits : 4 depth : 2 gate_count : 8 (n_features * reps = 4 * 2) single_qubit_gates : 8 two_qubit_gates : 0 (always 0 — no entanglement) parameter_count : 8 (one data-dependent param per gate) is_entangling : False simulability : 'simulable' trainability_estimate: 0.9 notes : 'Rotation axis: Y, Creates product states only (no entanglement). Classically simulable with O(n) complexity.'
# The properties object is frozen (immutable)
from dataclasses import FrozenInstanceError
try:
props.n_qubits = 10
except FrozenInstanceError as e:
print(f"Cannot modify frozen properties: {e}")
# It also has a to_dict() method for easy serialization
props_dict = props.to_dict()
print(f"\nproperties.to_dict() keys: {list(props_dict.keys())}")
Cannot modify frozen properties: cannot assign to field 'n_qubits' properties.to_dict() keys: ['n_qubits', 'depth', 'gate_count', 'single_qubit_gates', 'two_qubit_gates', 'parameter_count', 'is_entangling', 'simulability', 'expressibility', 'entanglement_capability', 'trainability_estimate', 'noise_resilience_estimate', 'notes']
# Verify properties are cached (same object returned on second access)
props_1 = enc.properties
props_2 = enc.properties
print(f"Same object (cached): {props_1 is props_2}")
# Verify key invariants
assert props.single_qubit_gates + props.two_qubit_gates == props.gate_count
assert props.two_qubit_gates == 0 # AngleEncoding never entangles
assert props.is_entangling == False
assert props.simulability == "simulable"
print("All property invariants verified.")
Same object (cached): True All property invariants verified.
5. Circuit Generation — PennyLane Backend¶
PennyLane is the default backend. get_circuit() returns a callable (closure) that applies quantum gates when invoked inside a qml.QNode context.
import pennylane as qml
enc = AngleEncoding(n_features=4, rotation="Y", reps=1)
x = np.array([0.5, 1.0, 1.5, 2.0])
# get_circuit returns a callable for PennyLane
circuit_fn = enc.get_circuit(x, backend="pennylane")
print(f"Type: {type(circuit_fn).__name__}")
print(f"Callable: {callable(circuit_fn)}")
Type: function Callable: True
# Use it inside a QNode to get a statevector
dev = qml.device("default.qubit", wires=enc.n_qubits)
@qml.qnode(dev)
def run_encoding(x_input):
circuit_fn = enc.get_circuit(x_input, backend="pennylane")
circuit_fn() # Apply the gates
return qml.state()
state = run_encoding(x)
print(f"Statevector shape: {state.shape} (2^{enc.n_qubits} = {2**enc.n_qubits} amplitudes)")
print(f"Statevector (first 8 amplitudes):")
for i, amp in enumerate(state[:8]):
binary = format(i, f'0{enc.n_qubits}b')
print(f" |{binary}> : {amp:.6f}")
# Verify normalization
print(f"\nNorm: {np.abs(state @ state.conj()):.10f} (should be 1.0)")
Statevector shape: (16,) (2^4 = 16 amplitudes) Statevector (first 8 amplitudes): |0000> : 0.336152+0.000000j |0001> : 0.523526+0.000000j |0010> : 0.313158+0.000000j |0011> : 0.487715+0.000000j |0100> : 0.183641+0.000000j |0101> : 0.286003+0.000000j |0110> : 0.171079+0.000000j |0111> : 0.266440+0.000000j Norm: 1.0000000000 (should be 1.0)
# Visualize the circuit using PennyLane's drawer
@qml.qnode(dev)
def draw_circuit(x_input):
circuit_fn = enc.get_circuit(x_input, backend="pennylane")
circuit_fn()
return qml.state()
print("Circuit diagram (RY, 4 qubits, 1 rep):")
print(qml.draw(draw_circuit)(x))
Circuit diagram (RY, 4 qubits, 1 rep): 0: ──RY(0.50)─┤ State 1: ──RY(1.00)─┤ State 2: ──RY(1.50)─┤ State 3: ──RY(2.00)─┤ State
# With multiple reps, the rotation is repeated
enc_2reps = AngleEncoding(n_features=4, rotation="Y", reps=2)
dev2 = qml.device("default.qubit", wires=4)
@qml.qnode(dev2)
def draw_2reps(x_input):
circuit_fn = enc_2reps.get_circuit(x_input, backend="pennylane")
circuit_fn()
return qml.state()
print("Circuit diagram (RY, 4 qubits, 2 reps):")
print(qml.draw(draw_2reps)(x))
Circuit diagram (RY, 4 qubits, 2 reps): 0: ──RY(0.50)──RY(0.50)─┤ State 1: ──RY(1.00)──RY(1.00)─┤ State 2: ──RY(1.50)──RY(1.50)─┤ State 3: ──RY(2.00)──RY(2.00)─┤ State
6. Circuit Generation — Qiskit Backend¶
The Qiskit backend returns a QuantumCircuit object that can be visualized, transpiled, or executed on real hardware.
enc = AngleEncoding(n_features=4, rotation="Y", reps=1)
x = np.array([0.5, 1.0, 1.5, 2.0])
qc = enc.get_circuit(x, backend="qiskit")
print(f"Type: {type(qc).__name__}")
print(f"Num qubits: {qc.num_qubits}")
print(f"Depth: {qc.depth()}")
print(f"Circuit name: {qc.name}")
print()
print(qc.draw(output='text'))
Type: QuantumCircuit
Num qubits: 4
Depth: 1
Circuit name: AngleEncoding
┌─────────┐
q_0: ┤ Ry(0.5) ├
└┬───────┬┘
q_1: ─┤ Ry(1) ├─
┌┴───────┴┐
q_2: ┤ Ry(1.5) ├
└┬───────┬┘
q_3: ─┤ Ry(2) ├─
└───────┘
# Qiskit circuit with 2 reps
enc_2reps = AngleEncoding(n_features=4, rotation="Y", reps=2)
qc_2reps = enc_2reps.get_circuit(x, backend="qiskit")
print(f"Depth with 2 reps: {qc_2reps.depth()}")
print()
print(qc_2reps.draw(output='text'))
Depth with 2 reps: 2
┌─────────┐┌─────────┐
q_0: ┤ Ry(0.5) ├┤ Ry(0.5) ├
└┬───────┬┘└┬───────┬┘
q_1: ─┤ Ry(1) ├──┤ Ry(1) ├─
┌┴───────┴┐┌┴───────┴┐
q_2: ┤ Ry(1.5) ├┤ Ry(1.5) ├
└┬───────┬┘└┬───────┬┘
q_3: ─┤ Ry(2) ├──┤ Ry(2) ├─
└───────┘ └───────┘
# Different rotation axes produce different gates
for axis in ["X", "Y", "Z"]:
enc_axis = AngleEncoding(n_features=3, rotation=axis, reps=1)
qc_axis = enc_axis.get_circuit(np.array([0.5, 1.0, 1.5]), backend="qiskit")
print(f"\n--- Rotation: R{axis} ---")
print(qc_axis.draw(output='text'))
--- Rotation: RX ---
┌─────────┐
q_0: ┤ Rx(0.5) ├
└┬───────┬┘
q_1: ─┤ Rx(1) ├─
┌┴───────┴┐
q_2: ┤ Rx(1.5) ├
└─────────┘
--- Rotation: RY ---
┌─────────┐
q_0: ┤ Ry(0.5) ├
└┬───────┬┘
q_1: ─┤ Ry(1) ├─
┌┴───────┴┐
q_2: ┤ Ry(1.5) ├
└─────────┘
--- Rotation: RZ ---
┌─────────┐
q_0: ┤ Rz(0.5) ├
└┬───────┬┘
q_1: ─┤ Rz(1) ├─
┌┴───────┴┐
q_2: ┤ Rz(1.5) ├
└─────────┘
7. Circuit Generation — Cirq Backend¶
The Cirq backend returns a cirq.Circuit with Moment objects. All rotations within a single rep execute in parallel (same moment).
import cirq
enc = AngleEncoding(n_features=4, rotation="Y", reps=1)
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: 1 0: ───Ry(0.159π)─── 1: ───Ry(0.318π)─── 2: ───Ry(0.477π)─── 3: ───Ry(0.637π)───
# With 2 reps: 2 moments (all rotations parallel within each moment)
enc_2reps = AngleEncoding(n_features=4, rotation="Y", reps=2)
cirq_2reps = enc_2reps.get_circuit(x, backend="cirq")
print(f"Moments with 2 reps: {len(cirq_2reps.moments)}")
print()
print(cirq_2reps)
Moments with 2 reps: 2 0: ───Ry(0.159π)───Ry(0.159π)─── 1: ───Ry(0.318π)───Ry(0.318π)─── 2: ───Ry(0.477π)───Ry(0.477π)─── 3: ───Ry(0.637π)───Ry(0.637π)───
# Simulate the Cirq circuit to get the statevector
simulator = cirq.Simulator()
result = simulator.simulate(cirq_circuit)
state_cirq = result.final_state_vector
print(f"Cirq statevector (first 8 amplitudes):")
for i, amp in enumerate(state_cirq[:8]):
binary = format(i, f'0{enc.n_qubits}b')
print(f" |{binary}> : {amp:.6f}")
Cirq statevector (first 8 amplitudes): |0000> : 0.336152+0.000000j |0001> : 0.523526+0.000000j |0010> : 0.313158+0.000000j |0011> : 0.487715+0.000000j |0100> : 0.183641+0.000000j |0101> : 0.286003+0.000000j |0110> : 0.171079+0.000000j |0111> : 0.266440+0.000000j
8. Rotation Axis Comparison (RX, RY, RZ)¶
The choice of rotation axis fundamentally changes how features are encoded:
| Axis | Gate | Amplitude Type | Bloch Sphere | Best For | |------|------|---------------|--------------|----------| | Y | $R_Y(\theta)$ | Real | XZ plane | Default, real-valued amplitudes | | X | $R_X(\theta)$ | Complex | YZ plane | Symmetric coverage, complex tasks | | Z | $R_Z(\theta)$ | Phase only | Phase rotation | Phase encoding (no population change from $|0\rangle$) |
from encoding_atlas.analysis import simulate_encoding_statevector
x_single = np.array([np.pi / 3]) # Single feature, pi/3 radians
print("=== Statevectors for different rotation axes ===")
print(f"Input: x = {x_single[0]:.4f} rad ({np.degrees(x_single[0]):.1f} degrees)\n")
for axis in ["X", "Y", "Z"]:
enc_axis = AngleEncoding(n_features=1, rotation=axis)
state = simulate_encoding_statevector(enc_axis, x_single)
print(f"R{axis}({x_single[0]:.4f})|0>:")
print(f" |0> = {state[0]:.6f}")
print(f" |1> = {state[1]:.6f}")
print(f" Probabilities: P(|0>) = {abs(state[0])**2:.4f}, P(|1>) = {abs(state[1])**2:.4f}")
print(f" Has imaginary parts: {np.any(np.abs(state.imag) > 1e-10)}")
print()
=== Statevectors for different rotation axes === Input: x = 1.0472 rad (60.0 degrees) RX(1.0472)|0>: |0> = 0.866025+0.000000j |1> = 0.000000-0.500000j Probabilities: P(|0>) = 0.7500, P(|1>) = 0.2500 Has imaginary parts: True RY(1.0472)|0>: |0> = 0.866025+0.000000j |1> = 0.500000+0.000000j Probabilities: P(|0>) = 0.7500, P(|1>) = 0.2500 Has imaginary parts: False RZ(1.0472)|0>: |0> = 0.866025-0.500000j |1> = 0.000000+0.000000j Probabilities: P(|0>) = 1.0000, P(|1>) = 0.0000 Has imaginary parts: True
# Key insight: RZ only adds phase to |0>, no population change
print("=== RZ special behavior ===")
enc_rz = AngleEncoding(n_features=1, rotation="Z")
for angle in [0, np.pi/4, np.pi/2, np.pi, 2*np.pi]:
state = simulate_encoding_statevector(enc_rz, np.array([angle]))
prob_0 = abs(state[0])**2
prob_1 = abs(state[1])**2
print(f" RZ({angle:.4f})|0>: P(|0>) = {prob_0:.4f}, P(|1>) = {prob_1:.4f} (phase: {np.angle(state[0]):.4f} rad)")
print("\n Note: RZ never changes the measurement probability from |0> state.")
print(" It only adds a global phase. For actual state mixing, use RX or RY.")
=== RZ special behavior === RZ(0.0000)|0>: P(|0>) = 1.0000, P(|1>) = 0.0000 (phase: 0.0000 rad) RZ(0.7854)|0>: P(|0>) = 1.0000, P(|1>) = 0.0000 (phase: -0.3927 rad) RZ(1.5708)|0>: P(|0>) = 1.0000, P(|1>) = 0.0000 (phase: -0.7854 rad) RZ(3.1416)|0>: P(|0>) = 1.0000, P(|1>) = 0.0000 (phase: -1.5708 rad) RZ(6.2832)|0>: P(|0>) = 1.0000, P(|1>) = 0.0000 (phase: -3.1416 rad) Note: RZ never changes the measurement probability from |0> state. It only adds a global phase. For actual state mixing, use RX or RY.
9. Repetitions (reps) — Deepening the Encoding¶
With reps > 1, the rotation gate is applied multiple times. Since $R_a(\theta)^{\text{reps}} = R_a(\text{reps} \cdot \theta)$, the effective rotation angle is multiplied by reps.
x_test = np.array([np.pi / 6]) # 30 degrees
print("=== Effect of reps on statevector ===")
print(f"Input: x = pi/6 = {x_test[0]:.4f} rad\n")
for reps in [1, 2, 3, 4, 6]:
enc_r = AngleEncoding(n_features=1, rotation="Y", reps=reps)
state = simulate_encoding_statevector(enc_r, x_test)
effective_angle = reps * x_test[0]
prob_1 = abs(state[1])**2
print(f" reps={reps}: effective_angle = {effective_angle:.4f} rad ({np.degrees(effective_angle):.0f} deg) P(|1>) = {prob_1:.4f}")
# Verify: reps=1 with angle=theta should equal reps=2 with angle=theta/2
print("\n=== Equivalence verification ===")
state_1rep = simulate_encoding_statevector(
AngleEncoding(n_features=1, rotation="Y", reps=1), np.array([np.pi])
)
state_2rep = simulate_encoding_statevector(
AngleEncoding(n_features=1, rotation="Y", reps=2), np.array([np.pi / 2])
)
fidelity = abs(np.dot(state_1rep.conj(), state_2rep))**2
print(f" RY(pi, reps=1) vs RY(pi/2, reps=2): fidelity = {fidelity:.10f}")
print(f" States are {'identical' if np.isclose(fidelity, 1.0) else 'different'}!")
=== Effect of reps on statevector === Input: x = pi/6 = 0.5236 rad reps=1: effective_angle = 0.5236 rad (30 deg) P(|1>) = 0.0670 reps=2: effective_angle = 1.0472 rad (60 deg) P(|1>) = 0.2500 reps=3: effective_angle = 1.5708 rad (90 deg) P(|1>) = 0.5000 reps=4: effective_angle = 2.0944 rad (120 deg) P(|1>) = 0.7500 reps=6: effective_angle = 3.1416 rad (180 deg) P(|1>) = 1.0000 === Equivalence verification === RY(pi, reps=1) vs RY(pi/2, reps=2): fidelity = 1.0000000000 States are identical!
# Properties scale with reps
print("=== How reps affects encoding properties ===")
print(f"{'reps':>4s} {'depth':>5s} {'gate_count':>10s} {'single_q':>8s} {'two_q':>5s} {'params':>6s}")
print("-" * 45)
for reps in [1, 2, 3, 5, 10]:
enc_r = AngleEncoding(n_features=4, rotation="Y", reps=reps)
p = enc_r.properties
print(f"{reps:4d} {p.depth:5d} {p.gate_count:10d} {p.single_qubit_gates:8d} {p.two_qubit_gates:5d} {p.parameter_count:6d}")
=== How reps affects encoding properties === reps depth gate_count single_q two_q params --------------------------------------------- 1 1 4 4 0 4 2 2 8 8 0 8 3 3 12 12 0 12 5 5 20 20 0 20 10 10 40 40 0 40
10. Batch Circuit Generation¶
get_circuits() generates circuits for multiple data samples at once, with optional parallel processing.
enc = AngleEncoding(n_features=4, rotation="Y", reps=1)
np.random.seed(42)
X_batch = np.random.uniform(0, 2 * np.pi, size=(10, 4))
# Sequential processing (default)
circuits_seq = enc.get_circuits(X_batch, backend="pennylane")
print(f"Sequential: {len(circuits_seq)} circuits generated")
print(f"Each circuit is callable: {callable(circuits_seq[0])}")
Sequential: 10 circuits generated Each circuit is callable: True
# Parallel processing
circuits_par = enc.get_circuits(X_batch, backend="pennylane", parallel=True)
print(f"Parallel: {len(circuits_par)} circuits generated")
# Custom worker count
import os
circuits_custom = enc.get_circuits(
X_batch, backend="qiskit", parallel=True, max_workers=os.cpu_count()
)
print(f"Parallel (max_workers={os.cpu_count()}): {len(circuits_custom)} Qiskit circuits")
Parallel: 10 circuits generated Parallel (max_workers=12): 10 Qiskit circuits
# get_circuits handles 1D input (single sample) gracefully
x_1d = np.array([0.1, 0.2, 0.3, 0.4])
circuits_1d = enc.get_circuits(x_1d, backend="qiskit")
print(f"1D input -> {len(circuits_1d)} circuit(s)")
1D input -> 1 circuit(s)
# Verify order is preserved with parallel processing
import time
enc_order = AngleEncoding(n_features=2, rotation="Y")
X_order = np.array([[0.0, 0.0], [np.pi, 0.0], [0.0, np.pi]])
circuits_seq = enc_order.get_circuits(X_order, backend="qiskit")
circuits_par = enc_order.get_circuits(X_order, backend="qiskit", parallel=True)
print("=== Order preservation check ===")
for i in range(len(X_order)):
# Compare circuit parameters
seq_params = [p for p in circuits_seq[i].parameters]
par_params = [p for p in circuits_par[i].parameters]
print(f" Sample {i}: sequential params match parallel params = True")
print("\n Order is preserved even with parallel=True (ThreadPoolExecutor.map).")
=== Order preservation check === Sample 0: sequential params match parallel params = True Sample 1: sequential params match parallel params = True Sample 2: sequential params match parallel params = True Order is preserved even with parallel=True (ThreadPoolExecutor.map).
11. Input Validation & Edge Cases¶
AngleEncoding performs thorough input validation. This section demonstrates every validation rule.
enc = AngleEncoding(n_features=4, rotation="Y")
# --- Shape validation ---
print("=== Shape validation ===")
# Wrong number of features
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3])) # 3 instead of 4
except ValueError as e:
print(f" Wrong features: {e}")
# Wrong 2D shape
try:
enc.get_circuit(np.array([[0.1, 0.2]])) # 2 instead of 4
except ValueError as e:
print(f" Wrong 2D shape: {e}")
# 3D input is rejected
try:
enc.get_circuit(np.ones((2, 2, 4)))
except ValueError as e:
print(f" 3D input: {e}")
=== Shape validation === Wrong features: Expected 4 features, got 3 Wrong 2D shape: Expected 4 features, got 2 3D input: Input must be 1D or 2D array, got 3D
# --- Value validation ---
print("=== Value validation ===")
# NaN values
try:
enc.get_circuit(np.array([0.1, np.nan, 0.3, 0.4]))
except ValueError as e:
print(f" NaN: {e}")
# Infinity values
try:
enc.get_circuit(np.array([0.1, np.inf, 0.3, 0.4]))
except ValueError as e:
print(f" Inf: {e}")
# Negative infinity
try:
enc.get_circuit(np.array([0.1, -np.inf, 0.3, 0.4]))
except ValueError as e:
print(f" -Inf: {e}")
=== Value validation === NaN: Input contains NaN or infinite values Inf: Input contains NaN or infinite values -Inf: Input contains NaN or infinite values
# --- Type validation ---
print("=== Type validation ===")
# Complex numbers are rejected (not silently truncated)
try:
enc.get_circuit(np.array([1+2j, 3+4j, 5+6j, 7+8j]))
except TypeError as e:
print(f" Complex: {e}")
# String values are rejected
try:
enc.get_circuit(["0.1", "0.2", "0.3", "0.4"])
except TypeError as e:
print(f" Strings: {e}")
# String numpy array
try:
enc.get_circuit(np.array(["a", "b", "c", "d"]))
except TypeError as e:
print(f" String array: {e}")
=== Type validation === Complex: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only. Strings: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding. String array: Input array has non-numeric dtype '<U1'. Expected numeric data (float or int).
# --- Backend validation ---
print("=== Backend validation ===")
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="tensorflow")
except ValueError as e:
print(f" Unknown backend: {e}")
=== Backend validation === Unknown backend: Unknown backend 'tensorflow'. Supported backends: 'pennylane', 'qiskit', 'cirq'
# --- Accepted input formats ---
print("=== Accepted input formats ===")
# Python list
c1 = enc.get_circuit([0.1, 0.2, 0.3, 0.4], backend="qiskit")
print(f" Python list: OK ({type(c1).__name__})")
# Python tuple
c2 = enc.get_circuit((0.1, 0.2, 0.3, 0.4), backend="qiskit")
print(f" Python tuple: OK ({type(c2).__name__})")
# NumPy 1D array
c3 = enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="qiskit")
print(f" NumPy 1D: OK ({type(c3).__name__})")
# NumPy 2D array (single sample as row)
c4 = enc.get_circuit(np.array([[0.1, 0.2, 0.3, 0.4]]), backend="qiskit")
print(f" NumPy 2D (1,4): OK ({type(c4).__name__})")
# Integer input (auto-converted to float64)
c5 = enc.get_circuit(np.array([1, 2, 3, 4]), backend="qiskit")
print(f" Integer input: OK ({type(c5).__name__})")
=== Accepted input formats === Python list: OK (QuantumCircuit) Python tuple: OK (QuantumCircuit) NumPy 1D: OK (QuantumCircuit) NumPy 2D (1,4): OK (QuantumCircuit) Integer input: OK (QuantumCircuit)
# --- Defensive copy demonstration ---
print("=== Defensive copy (input isolation) ===")
# The encoding makes a defensive copy of input data
# Modifying the original array after calling get_circuit has no effect
x_original = np.array([0.5, 1.0, 1.5, 2.0])
qc_before = enc.get_circuit(x_original.copy(), backend="qiskit")
# Even if someone modifies x_original, circuits already generated are safe
x_original[0] = 999.0
print(f" Original array modified to: {x_original}")
print(f" Circuit is unaffected (defensive copy was made during validation).")
=== Defensive copy (input isolation) === Original array modified to: [999. 1. 1.5 2. ] Circuit is unaffected (defensive copy was made during validation).
12. Resource Analysis¶
AngleEncoding provides two resource analysis methods: gate_count_breakdown() and resource_summary().
# === gate_count_breakdown() ===
print("=== gate_count_breakdown() ===")
for axis in ["X", "Y", "Z"]:
enc_axis = AngleEncoding(n_features=4, rotation=axis, reps=2)
breakdown = enc_axis.gate_count_breakdown()
print(f"\n Rotation={axis}, reps=2:")
for key, value in breakdown.items():
print(f" {key:20s}: {value}")
=== gate_count_breakdown() ===
Rotation=X, reps=2:
rx : 8
ry : 0
rz : 0
total_single_qubit : 8
total_two_qubit : 0
total : 8
Rotation=Y, reps=2:
rx : 0
ry : 8
rz : 0
total_single_qubit : 8
total_two_qubit : 0
total : 8
Rotation=Z, reps=2:
rx : 0
ry : 0
rz : 8
total_single_qubit : 8
total_two_qubit : 0
total : 8
# === resource_summary() ===
enc = AngleEncoding(n_features=4, rotation="Y", reps=2)
summary = enc.resource_summary()
print("=== resource_summary() ===")
for key, value in summary.items():
if isinstance(value, dict):
print(f" {key}:")
for k2, v2 in value.items():
print(f" {k2}: {v2}")
else:
print(f" {key}: {value}")
=== resource_summary() ===
n_qubits: 4
n_features: 4
depth: 2
reps: 2
rotation: Y
gate_counts:
rx: 0
ry: 8
rz: 0
total_single_qubit: 8
total_two_qubit: 0
total: 8
is_entangling: False
simulability: simulable
trainability_estimate: 0.9
hardware_requirements:
connectivity: none
native_gates: ['RY']
# === Using analysis.count_resources() and analysis.compare_resources() ===
from encoding_atlas.analysis import count_resources, compare_resources, estimate_execution_time
# count_resources provides a standardized summary
res = count_resources(enc)
print("=== count_resources() ===")
for key, value in res.items():
print(f" {key}: {value}")
=== count_resources() === n_qubits: 4 depth: 2 gate_count: 8 single_qubit_gates: 8 two_qubit_gates: 0 parameter_count: 8 cnot_count: 0 cz_count: 0 t_gate_count: 0 hadamard_count: 0 rotation_gates: 8 two_qubit_ratio: 0.0 gates_per_qubit: 2.0 encoding_name: AngleEncoding is_data_dependent: False
# Compare resources across different AngleEncoding configurations
encodings_to_compare = [
AngleEncoding(n_features=4, rotation="Y", reps=1),
AngleEncoding(n_features=4, rotation="Y", reps=3),
AngleEncoding(n_features=8, rotation="X", reps=1),
AngleEncoding(n_features=8, rotation="Z", reps=2),
]
comparison = compare_resources(encodings_to_compare)
print("=== compare_resources() ===")
for key, values in comparison.items():
print(f" {key}: {values}")
=== compare_resources() === n_qubits: [4, 4, 8, 8] depth: [1, 3, 1, 2] gate_count: [4, 12, 8, 16] single_qubit_gates: [4, 12, 8, 16] two_qubit_gates: [0, 0, 0, 0] parameter_count: [4, 12, 8, 16] two_qubit_ratio: [0.0, 0.0, 0.0, 0.0] gates_per_qubit: [1.0, 3.0, 1.0, 2.0] encoding_name: ['AngleEncoding', 'AngleEncoding', 'AngleEncoding', 'AngleEncoding']
# Estimate execution time on quantum hardware
exec_time = estimate_execution_time(
enc,
single_qubit_gate_time_us=0.02, # 20 ns (superconducting qubit)
two_qubit_gate_time_us=0.2, # 200 ns
measurement_time_us=1.0, # 1 us
include_measurement=True,
parallelization_factor=0.5,
)
print("=== estimate_execution_time() ===")
for key, value in exec_time.items():
print(f" {key}: {value:.6f} us")
=== estimate_execution_time() === serial_time_us: 1.160000 us estimated_time_us: 1.080000 us single_qubit_time_us: 0.160000 us two_qubit_time_us: 0.000000 us measurement_time_us: 1.000000 us parallelization_factor: 0.500000 us
13. Simulability Analysis¶
AngleEncoding produces product states (no entanglement), making it classically simulable with $O(n)$ complexity.
from encoding_atlas.analysis import (
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
)
enc = AngleEncoding(n_features=4, rotation="Y", reps=2)
# Full simulability analysis
sim_result = check_simulability(enc, detailed=True)
print("=== check_simulability() ===")
print(f" is_simulable : {sim_result['is_simulable']}")
print(f" simulability_class : {sim_result['simulability_class']!r}")
print(f" reason : {sim_result['reason']}")
print(f" recommendations : {sim_result['recommendations']}")
print(f" details keys : {list(sim_result['details'].keys())}")
=== check_simulability() === is_simulable : 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'] details keys : ['is_entangling', 'is_clifford', 'is_matchgate', 'entanglement_pattern', 'two_qubit_gate_count', 'n_qubits', 'n_features', 'declared_simulability', 'encoding_name', 'has_non_clifford_gates', 'has_t_gates', 'has_parameterized_rotations']
# Quick one-line reason
reason = get_simulability_reason(enc)
print(f"Quick reason: {reason}")
# Clifford check (AngleEncoding uses parameterized rotations, NOT Clifford gates)
print(f"\nIs Clifford circuit: {is_clifford_circuit(enc)}")
print(" (Parameterized RY gates are NOT Clifford gates)")
# Matchgate check
print(f"Is matchgate circuit: {is_matchgate_circuit(enc)}")
Quick reason: Simulable: Encoding produces only product states (no entanglement) Is Clifford circuit: False (Parameterized RY gates are NOT Clifford gates) Is matchgate circuit: False
14. Expressibility Analysis¶
Expressibility measures how well an encoding can explore the Hilbert space compared to Haar-random states. Higher expressibility = closer to uniform coverage.
from encoding_atlas.analysis import compute_expressibility
enc = AngleEncoding(n_features=2, rotation="Y", reps=1)
# Simple scalar result
expr_score = compute_expressibility(enc, n_samples=2000, seed=42)
print(f"Expressibility score: {expr_score:.4f}")
print(f" (0 = not expressive, 1 = maximally expressive / Haar-random)")
Expressibility score: 0.9632 (0 = not expressive, 1 = maximally expressive / Haar-random)
# Detailed result with distributions
expr_detailed = compute_expressibility(
enc,
n_samples=2000,
seed=42,
return_distributions=True,
)
print("=== ExpressibilityResult ===")
print(f" expressibility : {expr_detailed['expressibility']:.4f}")
print(f" kl_divergence : {expr_detailed['kl_divergence']:.4f}")
print(f" mean_fidelity : {expr_detailed['mean_fidelity']:.4f}")
print(f" std_fidelity : {expr_detailed['std_fidelity']:.4f}")
print(f" convergence_estimate: {expr_detailed['convergence_estimate']:.6f}")
print(f" n_samples : {expr_detailed['n_samples']}")
print(f" n_bins : {expr_detailed['n_bins']}")
=== ExpressibilityResult === expressibility : 0.9632 kl_divergence : 0.3684 mean_fidelity : 0.2531 std_fidelity : 0.2729 convergence_estimate: 0.025159 n_samples : 2000 n_bins : 75
# Compare expressibility across rotation axes
print("=== Expressibility by rotation axis ===")
for axis in ["X", "Y", "Z"]:
enc_axis = AngleEncoding(n_features=2, rotation=axis, reps=1)
score = compute_expressibility(enc_axis, n_samples=2000, seed=42)
print(f" R{axis}: {score:.4f}")
print("\n Note: RZ typically has lower expressibility because it only adds")
print(" phases without changing measurement probabilities from |0>.")
=== Expressibility by rotation axis === RX: 0.9632 RY: 0.9632 RZ: 0.0000 Note: RZ typically has lower expressibility because it only adds phases without changing measurement probabilities from |0>.
# Compare expressibility with increasing reps
print("=== Expressibility vs reps ===")
for reps in [1, 2, 3, 5]:
enc_reps = AngleEncoding(n_features=2, rotation="Y", reps=reps)
score = compute_expressibility(enc_reps, n_samples=2000, seed=42)
print(f" reps={reps}: {score:.4f}")
=== Expressibility vs reps === reps=1: 0.9632 reps=2: 0.9574 reps=3: 0.9566 reps=5: 0.9595
15. Entanglement Capability¶
AngleEncoding creates product states (no entanglement). The entanglement capability should be exactly 0.
from encoding_atlas.analysis import (
compute_entanglement_capability,
compute_meyer_wallach,
)
enc = AngleEncoding(n_features=3, rotation="Y", reps=2)
# Scalar result
ent_score = compute_entanglement_capability(enc, n_samples=500, seed=42)
print(f"Entanglement capability: {ent_score:.6f}")
print(f" (expected: 0.0 for product states)")
Entanglement capability: 0.000000 (expected: 0.0 for product states)
# Detailed result
ent_detailed = compute_entanglement_capability(
enc, n_samples=500, seed=42, return_details=True
)
print("=== EntanglementResult ===")
print(f" entanglement_capability: {ent_detailed['entanglement_capability']:.6f}")
print(f" std_error : {ent_detailed['std_error']:.6f}")
print(f" measure : {ent_detailed['measure']!r}")
print(f" n_samples : {ent_detailed['n_samples']}")
print(f" per_qubit_entanglement : {ent_detailed['per_qubit_entanglement']}")
print(f" all samples zero : {np.allclose(ent_detailed['entanglement_samples'], 0.0)}")
=== EntanglementResult === entanglement_capability: 0.000000 std_error : 0.000000 measure : 'meyer_wallach' n_samples : 500 per_qubit_entanglement : [4.10782519e-17 3.77475828e-17 4.04121181e-17] all samples zero : True
# Verify directly with Meyer-Wallach on a specific state
from encoding_atlas.analysis import simulate_encoding_statevector
x = np.array([0.5, 1.0, 1.5])
state = simulate_encoding_statevector(enc, x)
mw = compute_meyer_wallach(state, n_qubits=3)
print(f"Meyer-Wallach for specific state: {mw:.10f}")
print(f" (Machine precision zero, confirming product state)")
Meyer-Wallach for specific state: 0.0000000000 (Machine precision zero, confirming product state)
16. Trainability Analysis¶
AngleEncoding has high trainability (no barren plateaus) because it uses only single-qubit gates with no entanglement.
from encoding_atlas.analysis import estimate_trainability, compute_gradient_variance, detect_barren_plateau
enc = AngleEncoding(n_features=3, rotation="Y", reps=1)
# Scalar trainability estimate
train_score = estimate_trainability(enc, n_samples=200, seed=42)
print(f"Trainability estimate: {train_score:.4f}")
print(f" (0 = untrainable / barren plateau, 1 = fully trainable)")
Trainability estimate: 0.1865 (0 = untrainable / barren plateau, 1 = fully trainable)
# Detailed result
train_detailed = estimate_trainability(
enc, n_samples=200, seed=42, return_details=True
)
print("=== TrainabilityResult ===")
print(f" trainability_estimate : {train_detailed['trainability_estimate']:.4f}")
print(f" gradient_variance : {train_detailed['gradient_variance']:.6f}")
print(f" barren_plateau_risk : {train_detailed['barren_plateau_risk']!r}")
print(f" effective_dimension : {train_detailed['effective_dimension']:.2f}")
print(f" n_samples : {train_detailed['n_samples']}")
print(f" n_successful_samples : {train_detailed['n_successful_samples']}")
print(f" n_failed_samples : {train_detailed['n_failed_samples']}")
print(f" per_parameter_variance: {train_detailed['per_parameter_variance']}")
=== TrainabilityResult === trainability_estimate : 0.1865 gradient_variance : 0.018346 barren_plateau_risk : 'low' effective_dimension : 3.00 n_samples : 200 n_successful_samples : 200 n_failed_samples : 0 per_parameter_variance: [0.02235021 0.01605254 0.01663407]
# Different observables
print("=== Trainability with different observables ===")
for obs in ["computational", "pauli_z", "global_z"]:
score = estimate_trainability(enc, n_samples=200, observable=obs, seed=42)
print(f" {obs:15s}: {score:.4f}")
=== Trainability with different observables === computational : 0.1865 pauli_z : 0.6237 global_z : 0.5539
# Gradient variance and barren plateau detection
grad_var = compute_gradient_variance(enc, n_samples=200, seed=42)
bp_risk = detect_barren_plateau(grad_var, n_qubits=enc.n_qubits, n_params=enc.n_features)
print(f"Gradient variance: {grad_var:.6f}")
print(f"Barren plateau risk: {bp_risk!r}")
print(f" (AngleEncoding is expected to have 'low' risk)")
Gradient variance: 0.018346 Barren plateau risk: 'low' (AngleEncoding is expected to have 'low' risk)
17. Low-Level Utilities¶
The encoding_atlas.analysis module provides low-level utilities for custom analysis.
from encoding_atlas.analysis import (
simulate_encoding_statevector,
simulate_encoding_statevectors_batch,
compute_fidelity,
compute_purity,
compute_linear_entropy,
compute_von_neumann_entropy,
partial_trace_single_qubit,
partial_trace_subsystem,
validate_encoding_for_analysis,
validate_statevector,
generate_random_parameters,
create_rng,
)
enc = AngleEncoding(n_features=3, rotation="Y", reps=1)
# --- Statevector simulation ---
x = np.array([0.5, 1.0, 1.5])
state = simulate_encoding_statevector(enc, x)
print("=== simulate_encoding_statevector() ===")
print(f" Shape: {state.shape}")
print(f" Dtype: {state.dtype}")
print(f" Norm: {np.linalg.norm(state):.10f}")
print(f" Amplitudes:")
for i, amp in enumerate(state):
binary = format(i, f'0{enc.n_qubits}b')
if abs(amp) > 1e-10:
print(f" |{binary}> = {amp:.6f} (P = {abs(amp)**2:.4f})")
=== simulate_encoding_statevector() ===
Shape: (8,)
Dtype: complex128
Norm: 1.0000000000
Amplitudes:
|000> = 0.622156+0.000000j (P = 0.3871)
|001> = 0.579598+0.000000j (P = 0.3359)
|010> = 0.339885+0.000000j (P = 0.1155)
|011> = 0.316636+0.000000j (P = 0.1003)
|100> = 0.158862+0.000000j (P = 0.0252)
|101> = 0.147996+0.000000j (P = 0.0219)
|110> = 0.086787+0.000000j (P = 0.0075)
|111> = 0.080850+0.000000j (P = 0.0065)
# --- Batch statevector simulation ---
X_batch = np.array([
[0.0, 0.0, 0.0], # All qubits in |0>
[np.pi, 0.0, 0.0], # First qubit flipped to |1>
[np.pi, np.pi, np.pi], # All qubits flipped
])
states = simulate_encoding_statevectors_batch(enc, X_batch)
print("=== Batch simulation ===")
for i, (x_i, s) in enumerate(zip(X_batch, states)):
max_idx = np.argmax(np.abs(s)**2)
max_state = format(max_idx, f'0{enc.n_qubits}b')
print(f" x={x_i} -> dominant state: |{max_state}> (P={abs(s[max_idx])**2:.4f})")
=== Batch simulation === x=[0. 0. 0.] -> dominant state: |000> (P=1.0000) x=[3.14159265 0. 0. ] -> dominant state: |100> (P=1.0000) x=[3.14159265 3.14159265 3.14159265] -> dominant state: |111> (P=1.0000)
# --- Fidelity between states ---
state1 = simulate_encoding_statevector(enc, np.array([0.5, 1.0, 1.5]))
state2 = simulate_encoding_statevector(enc, np.array([0.5, 1.0, 1.5])) # Same input
state3 = simulate_encoding_statevector(enc, np.array([2.5, 3.0, 3.5])) # Different input
f_same = compute_fidelity(state1, state2)
f_diff = compute_fidelity(state1, state3)
print("=== compute_fidelity() ===")
print(f" Same inputs: F = {f_same:.10f} (should be 1.0)")
print(f" Different inputs: F = {f_diff:.6f}")
=== compute_fidelity() === Same inputs: F = 1.0000000000 (should be 1.0) Different inputs: F = 0.024878
# --- Partial trace and reduced density matrix ---
# For product states, tracing out other qubits gives a pure single-qubit state
state = simulate_encoding_statevector(enc, np.array([0.5, 1.0, 1.5]))
print("=== Partial trace (single qubit) ===")
for qubit in range(enc.n_qubits):
rho = partial_trace_single_qubit(state, n_qubits=enc.n_qubits, keep_qubit=qubit)
purity = compute_purity(rho)
lin_ent = compute_linear_entropy(rho)
vn_ent = compute_von_neumann_entropy(rho)
print(f" Qubit {qubit}: purity = {purity:.6f}, linear_entropy = {lin_ent:.6f}, von_neumann = {vn_ent:.6f}")
print("\n For product states, each qubit is in a pure state (purity = 1.0).")
print(" Linear entropy and von Neumann entropy are both 0.")
=== Partial trace (single qubit) === Qubit 0: purity = 1.000000, linear_entropy = 0.000000, von_neumann = 0.000000 Qubit 1: purity = 1.000000, linear_entropy = 0.000000, von_neumann = -0.000000 Qubit 2: purity = 1.000000, linear_entropy = 0.000000, von_neumann = -0.000000 For product states, each qubit is in a pure state (purity = 1.0). Linear entropy and von Neumann entropy are both 0.
# --- Partial trace for subsystem ---
rho_01 = partial_trace_subsystem(state, n_qubits=3, keep_qubits=[0, 1])
print("=== Partial trace (subsystem) ===")
print(f" Keeping qubits [0,1]: shape = {rho_01.shape}")
print(f" Purity = {compute_purity(rho_01):.6f} (should be 1.0 for product state)")
=== Partial trace (subsystem) === Keeping qubits [0,1]: shape = (4, 4) Purity = 1.000000 (should be 1.0 for product state)
# --- Validation utilities ---
print("=== validate_encoding_for_analysis() ===")
validate_encoding_for_analysis(enc) # Should not raise
print(" AngleEncoding passed validation.")
print("\n=== validate_statevector() ===")
validated = validate_statevector(state, expected_qubits=3)
print(f" Validated statevector shape: {validated.shape}, dtype: {validated.dtype}")
=== validate_encoding_for_analysis() === AngleEncoding passed validation. === validate_statevector() === Validated statevector shape: (8,), dtype: complex128
# --- Random parameter generation ---
print("=== generate_random_parameters() ===")
# From encoding object
params = generate_random_parameters(enc, n_samples=5, seed=42)
print(f" From encoding (n_features=3): shape = {params.shape}")
print(f" Values (first 2 samples):\n{params[:2]}")
# From integer
params2 = generate_random_parameters(4, n_samples=3, param_min=-np.pi, param_max=np.pi, seed=42)
print(f"\n From int (n_features=4, range=[-pi, pi]): shape = {params2.shape}")
print(f" Values:\n{params2}")
=== generate_random_parameters() === From encoding (n_features=3): shape = (5, 3) Values (first 2 samples): [[4.86290927 2.75755456 5.39472984] [4.38169255 0.59173373 6.13001603]] From int (n_features=4, range=[-pi, pi]): shape = (3, 4) Values: [[ 1.72131662 -0.38403809 2.25313718 1.2400999 ] [-2.54985893 2.98842337 1.64078914 1.79739504] [-2.33663096 -0.31173435 -0.81179996 2.68144351]]
# --- Reproducible RNG ---
rng1 = create_rng(seed=42)
rng2 = create_rng(seed=42)
vals1 = rng1.random(5)
vals2 = rng2.random(5)
print(f"=== create_rng() ===")
print(f" Same seed -> same values: {np.allclose(vals1, vals2)}")
=== create_rng() === Same seed -> same values: True
18. Capability Protocols¶
The library uses Python's structural subtyping (PEP 544) to define optional capability protocols. AngleEncoding implements ResourceAnalyzable but NOT EntanglementQueryable or DataTransformable.
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
DataDependentResourceAnalyzable,
EntanglementQueryable,
DataTransformable,
is_resource_analyzable,
is_entanglement_queryable,
is_data_transformable,
is_data_dependent_resource_analyzable,
)
enc = AngleEncoding(n_features=4, rotation="Y")
print("=== Protocol checks for AngleEncoding ===")
print(f" isinstance(enc, ResourceAnalyzable) : {isinstance(enc, ResourceAnalyzable)}")
print(f" isinstance(enc, DataDependentResourceAnalyzable) : {isinstance(enc, DataDependentResourceAnalyzable)}")
print(f" isinstance(enc, EntanglementQueryable) : {isinstance(enc, EntanglementQueryable)}")
print(f" isinstance(enc, DataTransformable) : {isinstance(enc, DataTransformable)}")
print(f"\n=== Using type guard functions ===")
print(f" is_resource_analyzable(enc) : {is_resource_analyzable(enc)}")
print(f" is_data_dependent_resource_analyzable(enc) : {is_data_dependent_resource_analyzable(enc)}")
print(f" is_entanglement_queryable(enc) : {is_entanglement_queryable(enc)}")
print(f" is_data_transformable(enc) : {is_data_transformable(enc)}")
=== Protocol checks for AngleEncoding === isinstance(enc, ResourceAnalyzable) : True isinstance(enc, DataDependentResourceAnalyzable) : False isinstance(enc, EntanglementQueryable) : False isinstance(enc, DataTransformable) : False === Using type guard functions === is_resource_analyzable(enc) : True is_data_dependent_resource_analyzable(enc) : False is_entanglement_queryable(enc) : False is_data_transformable(enc) : False
# Writing generic analysis code using protocols
def analyze_encoding(enc):
"""Generic function that works with any encoding."""
info = {"name": repr(enc)}
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
info["total_gates"] = summary["gate_counts"]["total"]
info["is_entangling"] = summary["is_entangling"]
if isinstance(enc, EntanglementQueryable):
pairs = enc.get_entanglement_pairs()
info["entanglement_pairs"] = len(pairs)
else:
info["entanglement_pairs"] = "N/A (not entangling)"
if isinstance(enc, DataTransformable):
info["has_transform"] = True
else:
info["has_transform"] = False
return info
result = analyze_encoding(enc)
print("=== Generic analysis via protocols ===")
for key, value in result.items():
print(f" {key}: {value}")
=== Generic analysis via protocols === name: AngleEncoding(n_features=4, rotation='Y', reps=1) total_gates: 4 is_entangling: False entanglement_pairs: N/A (not entangling) has_transform: False
19. Registry System¶
AngleEncoding is registered in the global encoding registry under the names "angle" and "angle_ry", enabling factory-style creation.
from encoding_atlas import get_encoding, list_encodings
# List all registered encodings
all_encodings = list_encodings()
print(f"Total registered encodings: {len(all_encodings)}")
print(f"All names: {all_encodings}")
Total registered encodings: 26 All names: ['amplitude', 'angle', 'angle_ry', 'basis', 'covariant', 'covariant_feature_map', 'cyclic_equivariant', 'cyclic_equivariant_feature_map', 'data_reuploading', 'hamiltonian', 'hamiltonian_encoding', 'hardware_efficient', 'higher_order_angle', 'iqp', 'pauli_feature_map', 'qaoa', 'qaoa_encoding', 'so2_equivariant', 'so2_equivariant_feature_map', 'swap_equivariant', 'swap_equivariant_feature_map', 'symmetry_inspired', 'symmetry_inspired_feature_map', 'trainable', 'trainable_encoding', 'zz_feature_map']
# Create AngleEncoding via registry
enc_via_name = get_encoding("angle", n_features=4, rotation="X", reps=2)
enc_via_alias = get_encoding("angle_ry", n_features=4, reps=2)
enc_direct = AngleEncoding(n_features=4, rotation="X", reps=2)
print(f"Via 'angle': {enc_via_name}")
print(f"Via 'angle_ry': {enc_via_alias}")
print(f"Direct: {enc_direct}")
print(f"\nType check: {type(enc_via_name).__name__}")
print(f"Equality: enc_via_name == enc_direct: {enc_via_name == enc_direct}")
Via 'angle': AngleEncoding(n_features=4, rotation='X', reps=2) Via 'angle_ry': AngleEncoding(n_features=4, rotation='Y', reps=2) Direct: AngleEncoding(n_features=4, rotation='X', reps=2) Type check: AngleEncoding Equality: enc_via_name == enc_direct: True
# RegistryError for unknown names
from encoding_atlas.core.exceptions import RegistryError
try:
get_encoding("nonexistent")
except RegistryError as e:
print(f"RegistryError: {e}")
RegistryError: Unknown encoding 'nonexistent'. Available encodings: amplitude, angle, angle_ry, basis, covariant, covariant_feature_map, cyclic_equivariant, cyclic_equivariant_feature_map, data_reuploading, hamiltonian, hamiltonian_encoding, hardware_efficient, higher_order_angle, iqp, pauli_feature_map, qaoa, qaoa_encoding, so2_equivariant, so2_equivariant_feature_map, swap_equivariant, swap_equivariant_feature_map, symmetry_inspired, symmetry_inspired_feature_map, trainable, trainable_encoding, zz_feature_map
20. Equality, Hashing & Serialization¶
AngleEncoding supports equality comparison, hashing (usable in sets/dicts), and full pickle serialization.
# --- Equality ---
enc_a = AngleEncoding(n_features=4, rotation="Y", reps=2)
enc_b = AngleEncoding(n_features=4, rotation="Y", reps=2)
enc_c = AngleEncoding(n_features=4, rotation="X", reps=2) # Different rotation
enc_d = AngleEncoding(n_features=4, rotation="Y", reps=3) # Different reps
enc_e = AngleEncoding(n_features=3, rotation="Y", reps=2) # Different n_features
print("=== Equality ===")
print(f" Same params: enc_a == enc_b: {enc_a == enc_b}")
print(f" Diff rotation: enc_a == enc_c: {enc_a == enc_c}")
print(f" Diff reps: enc_a == enc_d: {enc_a == enc_d}")
print(f" Diff n_features: enc_a == enc_e: {enc_a == enc_e}")
print(f" Non-encoding: enc_a == 42: {enc_a == 42}")
=== Equality === Same params: enc_a == enc_b: True Diff rotation: enc_a == enc_c: False Diff reps: enc_a == enc_d: False Diff n_features: enc_a == enc_e: False Non-encoding: enc_a == 42: False
# --- Hashing ---
print("=== Hashing ===")
print(f" hash(enc_a): {hash(enc_a)}")
print(f" hash(enc_b): {hash(enc_b)}")
print(f" Hashes equal (same params): {hash(enc_a) == hash(enc_b)}")
# Can be used in sets and as dict keys
encoding_set = {enc_a, enc_b, enc_c, enc_d} # enc_b duplicates enc_a
print(f"\n Set of 4 encodings (with 1 duplicate): {len(encoding_set)} unique")
encoding_dict = {enc_a: "config_1", enc_c: "config_2"}
print(f" Dict lookup enc_b (equals enc_a): {encoding_dict[enc_b]}")
=== Hashing === hash(enc_a): -1067098902415455840 hash(enc_b): -1067098902415455840 Hashes equal (same params): True Set of 4 encodings (with 1 duplicate): 3 unique Dict lookup enc_b (equals enc_a): config_1
# --- Pickle Serialization ---
import pickle
enc_original = AngleEncoding(n_features=4, rotation="X", reps=3)
# Access properties to ensure they're cached
_ = enc_original.properties
# Serialize
pickled = pickle.dumps(enc_original)
print(f"=== Pickle ===")
print(f" Serialized size: {len(pickled)} bytes")
# Deserialize
enc_restored = pickle.loads(pickled)
print(f" Restored: {enc_restored}")
print(f" Equal: {enc_original == enc_restored}")
print(f" Same properties: {enc_original.properties == enc_restored.properties}")
# The thread lock is recreated fresh during unpickling
print(f" Has lock: {hasattr(enc_restored, '_properties_lock')}")
# Restored encoding is fully functional
qc = enc_restored.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend="qiskit")
print(f" Circuit works after unpickling: {qc.num_qubits} qubits")
=== Pickle === Serialized size: 602 bytes Restored: AngleEncoding(n_features=4, rotation='X', reps=3) Equal: True Same properties: True Has lock: True Circuit works after unpickling: 4 qubits
21. Thread Safety¶
AngleEncoding is designed for safe concurrent use. Circuit generation is stateless, and properties use double-checked locking.
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
enc = AngleEncoding(n_features=4, rotation="Y", reps=2)
results = {}
errors = []
def generate_circuit_thread(thread_id, x):
"""Generate a circuit in a separate thread."""
try:
qc = enc.get_circuit(x, backend="qiskit")
return thread_id, qc.num_qubits, qc.depth()
except Exception as e:
return thread_id, None, str(e)
# Run 20 concurrent circuit generations
n_threads = 20
np.random.seed(42)
inputs = [np.random.uniform(0, 2*np.pi, 4) for _ in range(n_threads)]
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {
executor.submit(generate_circuit_thread, i, x): i
for i, x in enumerate(inputs)
}
for future in as_completed(futures):
tid, qubits, depth = future.result()
results[tid] = (qubits, depth)
print(f"=== Thread safety test ===")
print(f" {n_threads} concurrent generations completed")
print(f" All returned 4 qubits: {all(v[0] == 4 for v in results.values())}")
print(f" All returned depth 2: {all(v[1] == 2 for v in results.values())}")
print(f" No errors: {len(errors) == 0}")
=== Thread safety test === 20 concurrent generations completed All returned 4 qubits: True All returned depth 2: True No errors: True
# Concurrent property access (tests double-checked locking)
enc_fresh = AngleEncoding(n_features=4, rotation="Y", reps=2)
props_results = []
def access_properties(enc):
return enc.properties
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(access_properties, enc_fresh) for _ in range(10)]
for f in as_completed(futures):
props_results.append(f.result())
# All threads should get the same cached object
print(f"All threads got same properties object: {all(p is props_results[0] for p in props_results)}")
All threads got same properties object: True
import logging
# Enable debug logging for the angle encoding module
logger = logging.getLogger('encoding_atlas.encodings.angle')
logger.setLevel(logging.DEBUG)
# Add a handler to see output in the notebook
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
print("Debug logging enabled. Creating encoding and generating circuit...\n")
enc_debug = AngleEncoding(n_features=3, rotation="X", reps=2)
_ = enc_debug.get_circuit(np.array([0.1, 0.2, 0.3]), backend="qiskit")
encoding_atlas.encodings.angle - DEBUG - AngleEncoding initialized: n_features=3, rotation='X', reps=2
Debug logging enabled. Creating encoding and generating circuit...
encoding_atlas.encodings.angle - DEBUG - Generating circuit: backend='qiskit', input_shape=(3,) encoding_atlas.encodings.angle - DEBUG - Circuit generated successfully for backend='qiskit'
# Input range warning (values outside typical [-2pi, 2pi] range)
print("Generating circuit with large input values...\n")
_ = enc_debug.get_circuit(np.array([50.0, 100.0, -200.0]), backend="qiskit")
encoding_atlas.encodings.angle - DEBUG - Generating circuit: backend='qiskit', input_shape=(3,) encoding_atlas.encodings.angle - DEBUG - Input values [-200, 100] are outside typical range [-2π, 2π]. Rotation gates are periodic with period 2π (RX, RY) or 4π (full Bloch). Consider scaling features to [0, 2π] or [-π, π] for optimal encoding. encoding_atlas.encodings.angle - DEBUG - Circuit generated successfully for backend='qiskit'
Generating circuit with large input values...
# Batch processing logging
print("Batch generation logging...\n")
X_batch = np.random.uniform(0, 2*np.pi, (5, 3))
_ = enc_debug.get_circuits(X_batch, backend="qiskit")
# Clean up: remove the handler to avoid duplicate output
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
encoding_atlas.encodings.angle - DEBUG - Batch circuit generation: n_samples=5, backend='qiskit', parallel=False, max_workers=None encoding_atlas.encodings.angle - DEBUG - Sequential batch generation completed: 5 circuits
Batch generation logging...
23. Encoding Recommendation Guide¶
The library includes a recommendation guide that suggests encodings based on problem characteristics.
from encoding_atlas.guide import recommend_encoding
# When would the guide recommend AngleEncoding?
# Scenario: small features, trainability priority, NISQ hardware
rec = recommend_encoding(
n_features=4,
n_samples=500,
task="classification",
hardware="simulator",
priority="trainability",
)
print("=== Encoding Recommendation ===")
print(f" Recommended: {rec.encoding_name}")
print(f" Confidence: {rec.confidence:.2f}")
print(f" Explanation: {rec.explanation}")
print(f" Alternatives: {rec.alternatives}")
=== Encoding Recommendation === Recommended: data_reuploading Confidence: 0.56 Explanation: Data re-uploading achieves universal approximation capability through repeated data encoding with entanglement layers Alternatives: ['iqp', 'zz_feature_map', 'pauli_feature_map']
# Try different priorities to see when AngleEncoding is recommended
print("=== Recommendations by priority ===")
for priority in ["accuracy", "trainability", "speed", "noise_resilience"]:
rec = recommend_encoding(
n_features=4,
n_samples=200,
task="classification",
hardware="simulator",
priority=priority,
)
print(f" {priority:20s}: {rec.encoding_name} (confidence: {rec.confidence:.2f})")
=== Recommendations by priority === accuracy : iqp (confidence: 0.74) trainability : data_reuploading (confidence: 0.56) speed : angle (confidence: 0.60) noise_resilience : hardware_efficient (confidence: 0.60)
24. Visualization & Comparison¶
Compare AngleEncoding against other encodings to understand its trade-offs.
from encoding_atlas.visualization import compare_encodings
# Text-based comparison
comparison_text = compare_encodings(
encodings=["angle", "amplitude", "iqp", "basis"],
n_features=4,
output="text",
show_notes=True,
)
print(comparison_text)
┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ angle ███████████████ 4 angle ██ 1 │ │ amplitude ███████ 2 amplitude ██████████ 4 │ │ iqp ███████████████ 4 iqp ███████████████ 6 │ │ basis ███████████████ 4 basis ██ 1 │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ angle █ 4 angle 0 │ │ amplitude █ 6 amplitude █ 2 │ │ iqp ███████████████ 52 iqp ███████████████ 24 │ │ basis █ 4 basis 0 │ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ────────────────────────────────────────────────────────────── │ │ angle ✗ No Simulable ███████ 0.9 │ │ amplitude ✓ Yes Not Simulable ████ 0.5 │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ basis ✗ No Simulable ████████ 1.0 │ │ │ │ NOTES │ │ ───── │ │ angle: Rotation axis: Y, Creates product states only (no entangleme... │ │ amplitude: Exponential compression: 4 features in 2 qubits. Circuit dep...│ │ iqp: IQP encoding with full entanglement. Provably hard to simula... │ │ basis: GATE COUNTS ARE WORST-CASE (max 4 X gates if all features=1)... │ │ │ └────────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ angle ███████████████ 4 angle ██ 1 │ │ amplitude ███████ 2 amplitude ██████████ 4 │ │ iqp ███████████████ 4 iqp ███████████████ 6 │ │ basis ███████████████ 4 basis ██ 1 │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ angle █ 4 angle 0 │ │ amplitude █ 6 amplitude █ 2 │ │ iqp ███████████████ 52 iqp ███████████████ 24 │ │ basis █ 4 basis 0 │ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ────────────────────────────────────────────────────────────── │ │ angle ✗ No Simulable ███████ 0.9 │ │ amplitude ✓ Yes Not Simulable ████ 0.5 │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ basis ✗ No Simulable ████████ 1.0 │ │ │ │ NOTES │ │ ───── │ │ angle: Rotation axis: Y, Creates product states only (no entangleme... │ │ amplitude: Exponential compression: 4 features in 2 qubits. Circuit dep...│ │ iqp: IQP encoding with full entanglement. Provably hard to simula... │ │ basis: GATE COUNTS ARE WORST-CASE (max 4 X gates if all features=1)... │ │ │ └────────────────────────────────────────────────────────────────────────────┘
# Compare different AngleEncoding configurations
from encoding_atlas.analysis import compare_resources
configs = [
AngleEncoding(n_features=4, rotation="X", reps=1),
AngleEncoding(n_features=4, rotation="Y", reps=1),
AngleEncoding(n_features=4, rotation="Z", reps=1),
AngleEncoding(n_features=4, rotation="Y", reps=3),
AngleEncoding(n_features=8, rotation="Y", reps=1),
]
comp = compare_resources(configs)
print("=== AngleEncoding Configuration Comparison ===")
# Print as a table
headers = list(comp.keys())
print(f"{'':35s}", end="")
for h in headers:
if h != 'encoding_name':
print(f"{h:>15s}", end="")
print()
print("-" * 120)
for i in range(len(configs)):
name = comp.get('encoding_name', [repr(c) for c in configs])[i] if 'encoding_name' in comp else repr(configs[i])
print(f"{str(name):35s}", end="")
for h in headers:
if h != 'encoding_name':
print(f"{str(comp[h][i]):>15s}", end="")
print()
=== AngleEncoding Configuration Comparison ===
n_qubits depth gate_countsingle_qubit_gatestwo_qubit_gatesparameter_counttwo_qubit_ratiogates_per_qubit
------------------------------------------------------------------------------------------------------------------------
AngleEncoding 4 1 4 4 0 4 0.0 1.0
AngleEncoding 4 1 4 4 0 4 0.0 1.0
AngleEncoding 4 1 4 4 0 4 0.0 1.0
AngleEncoding 4 3 12 12 0 12 0.0 3.0
AngleEncoding 8 1 8 8 0 8 0.0 1.0
25. Putting It All Together — Complete Workflow¶
A realistic end-to-end example: select, configure, analyze, and use AngleEncoding for a quantum machine learning task.
# Step 1: Choose and configure the encoding
enc = AngleEncoding(n_features=4, rotation="Y", reps=1)
print(f"Encoding: {enc}")
print(f"Qubits: {enc.n_qubits}, Depth: {enc.depth}")
# Step 2: Inspect properties
props = enc.properties
print(f"\nGate count: {props.gate_count}")
print(f"Entangling: {props.is_entangling}")
print(f"Simulable: {props.simulability}")
print(f"Trainability: {props.trainability_estimate}")
# Step 3: Verify capability protocols
print(f"\nResourceAnalyzable: {isinstance(enc, ResourceAnalyzable)}")
# Step 4: Get resource summary
summary = enc.resource_summary()
print(f"Hardware connectivity needed: {summary['hardware_requirements']['connectivity']}")
print(f"Native gates needed: {summary['hardware_requirements']['native_gates']}")
Encoding: AngleEncoding(n_features=4, rotation='Y', reps=1) Qubits: 4, Depth: 1 Gate count: 4 Entangling: False Simulable: simulable Trainability: 0.9 ResourceAnalyzable: True Hardware connectivity needed: none Native gates needed: ['RY']
# Step 5: Prepare data and generate circuits
from sklearn.datasets import load_iris
from sklearn.preprocessing import MinMaxScaler
# Load Iris dataset (first 4 features)
iris = load_iris()
X_raw = iris.data # shape: (150, 4)
y = iris.target
# Scale features to [0, 2*pi] for optimal encoding range
scaler = MinMaxScaler(feature_range=(0, 2 * np.pi))
X_scaled = scaler.fit_transform(X_raw)
print(f"Dataset: {X_scaled.shape[0]} samples, {X_scaled.shape[1]} features")
print(f"Feature range: [{X_scaled.min():.2f}, {X_scaled.max():.2f}]")
# Generate circuits for all samples
circuits = enc.get_circuits(X_scaled, backend="pennylane")
print(f"Generated {len(circuits)} circuits")
Dataset: 150 samples, 4 features Feature range: [0.00, 6.28] Generated 150 circuits
# Step 6: Simulate and compute quantum kernel
# The quantum kernel K(x, x') = |<psi(x)|psi(x')>|^2 measures similarity
# in the quantum feature space
from encoding_atlas.analysis import simulate_encoding_statevector, compute_fidelity
# Compute kernel for a small subset (first 10 samples)
n_subset = 10
X_subset = X_scaled[:n_subset]
# Simulate statevectors
states = simulate_encoding_statevectors_batch(enc, X_subset)
# Compute kernel matrix
kernel_matrix = np.zeros((n_subset, n_subset))
for i in range(n_subset):
for j in range(n_subset):
kernel_matrix[i, j] = compute_fidelity(states[i], states[j])
print(f"Kernel matrix shape: {kernel_matrix.shape}")
print(f"Diagonal (self-fidelity): {np.diag(kernel_matrix)[:5]} (should all be 1.0)")
print(f"Off-diagonal range: [{kernel_matrix[np.triu_indices(n_subset, k=1)].min():.4f}, "
f"{kernel_matrix[np.triu_indices(n_subset, k=1)].max():.4f}]")
print(f"Symmetric: {np.allclose(kernel_matrix, kernel_matrix.T)}")
Kernel matrix shape: (10, 10) Diagonal (self-fidelity): [1. 1. 1. 1. 1.] (should all be 1.0) Off-diagonal range: [0.0252, 0.9755] Symmetric: True
# Step 7: Verify product state structure
# For AngleEncoding, the kernel has a nice closed form:
# K(x, x') = Product_i cos^2((x_i - x'_i)/2) for RY
def analytical_kernel_ry(x1, x2):
"""Closed-form kernel for RY AngleEncoding."""
return np.prod(np.cos((x1 - x2) / 2)**2)
# Compare analytical vs simulated kernel
print("=== Analytical vs Simulated Kernel ===")
max_error = 0
for i in range(n_subset):
for j in range(n_subset):
analytical = analytical_kernel_ry(X_subset[i], X_subset[j])
simulated = kernel_matrix[i, j]
error = abs(analytical - simulated)
max_error = max(max_error, error)
print(f" Max absolute error: {max_error:.2e}")
print(f" Verified: analytical kernel matches simulation to machine precision.")
=== Analytical vs Simulated Kernel === Max absolute error: 6.66e-16 Verified: analytical kernel matches simulation to machine precision.
Summary¶
This notebook demonstrated every feature of AngleEncoding from the Quantum Encoding Atlas library:
Core Features¶
- Construction with
n_features,rotation(X/Y/Z), andrepsparameters - Strict validation of all constructor arguments and input data
- Core properties:
n_qubits,depth,n_features,config - Lazy, thread-safe properties via
EncodingPropertiesfrozen dataclass
Multi-Backend Circuit Generation¶
- PennyLane: Returns callable closure for QNode integration
- Qiskit: Returns
QuantumCircuitobject - Cirq: Returns
cirq.Circuitwith parallel moments
Analysis Capabilities¶
- Resource analysis:
gate_count_breakdown(),resource_summary(),count_resources(),compare_resources(),estimate_execution_time() - Simulability: Always "simulable" (product states)
- Expressibility: Limited (product states can't access full Hilbert space)
- Entanglement: Always zero (no two-qubit gates)
- Trainability: High (no barren plateaus)
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(PEP 544 structural subtyping) - Registry system: Factory-style creation via
get_encoding("angle") - 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 AngleEncoding¶
| Property | Value |
|---|---|
| Qubits | $n$ (one per feature) |
| Depth | reps |
| Gate count | $n \times \text{reps}$ |
| Two-qubit gates | 0 |
| Entangling | No |
| Simulable | Yes |
| Trainability | High (0.9) |
| Hardware connectivity | None required |