CyclicEquivariantFeatureMap: Complete Feature Demonstration¶
This notebook provides a comprehensive, hands-on guide to every feature of the
CyclicEquivariantFeatureMap from the
encoding-atlas library.
The Cyclic Equivariant Feature Map encodes classical data into quantum states while preserving Z_n cyclic shift symmetry:
$$\text{SWAP}_{\sigma}|\psi(x)\rangle = |\psi(\sigma \cdot x)\rangle$$
where $\sigma$ is a cyclic permutation and $\text{SWAP}_{\sigma}$ permutes qubits accordingly. Cyclically shifting the input features is exactly equivalent to cyclically permuting the qubits in the output state.
What This Notebook Covers¶
| # | Section | Description |
|---|---|---|
| 1 | Installation & Setup | Install the library and check backends |
| 2 | Creating an Encoding | Constructor parameters and defaults |
| 3 | Constructor Validation | Type checks, value checks, edge cases |
| 4 | Core Properties | n_qubits, depth, n_features |
| 5 | Encoding Properties | Lazy, thread-safe EncodingProperties dataclass |
| 6 | Configuration | The config property |
| 7 | Circuit Generation — PennyLane | Generate and simulate PennyLane circuits |
| 8 | Circuit Generation — Qiskit | Generate and simulate Qiskit circuits |
| 9 | Circuit Generation — Cirq | Generate and simulate Cirq circuits |
| 10 | Group Action | Cyclic shifts of input data |
| 11 | Unitary Representation | Permutation unitaries on qubits |
| 12 | Group Generators | Generators of Z_n |
| 13 | Equivariance Verification (Exact) | verify_equivariance and verify_equivariance_detailed |
| 14 | Equivariance on Generators | Verify on group generators |
| 15 | Statistical Verification | Scalable measurement-based verification |
| 16 | Auto Verification | Automatic method selection |
| 17 | Entanglement Pairs | Ring topology connectivity |
| 18 | Gate Count Breakdown | Detailed gate counts by type |
| 19 | Resource Summary | Comprehensive resource analysis |
| 20 | Batch Circuit Generation | Sequential and parallel batch processing |
| 21 | Input Validation & Edge Cases | Shape, value, type, and backend validation |
| 22 | Resource Analysis Tools | count_resources, compare_resources, estimate_execution_time |
| 23 | Simulability Analysis | Classical simulability classification |
| 24 | Expressibility Analysis | Hilbert space coverage measurement |
| 25 | Entanglement Capability | Entanglement generation measurement |
| 26 | Trainability Analysis | Barren plateau detection |
| 27 | Low-Level Utilities | Statevector simulation, fidelity, partial trace |
| 28 | Capability Protocols | ResourceAnalyzable, EntanglementQueryable |
| 29 | Registry System | get_encoding, list_encodings |
| 30 | Equality, Hashing & Serialization | ==, hash, pickle |
| 31 | Thread Safety | Concurrent property and circuit access |
| 32 | Logging & Debugging | Debug logging support |
| 33 | Encoding Recommendation Guide | When to use this encoding |
| 34 | Visualization & Comparison | Compare with other encodings |
| 35 | Complete Workflow | End-to-end quantum kernel example |
| 36 | Summary | Recap of all features |
Mathematical Background¶
Cyclic Group Z_n: The cyclic group of order n consists of elements {0, 1, 2, ..., n-1} with group operation (k₁ + k₂) mod n. It is generated by a single element: the shift-by-one operator σ.
Equivariance Property: For an n-dimensional feature vector x = (x₀, x₁, ..., x_{n-1}), a cyclic shift by k positions transforms:
$$\text{Input: } (x_0, x_1, \ldots, x_{n-1}) \rightarrow (x_k, x_{k+1}, \ldots, x_{k+n-1 \bmod n})$$
$$\text{State: } |\psi(x)\rangle \rightarrow |\psi(\sigma^k \cdot x)\rangle = \text{SWAP}_k |\psi(x)\rangle$$
Circuit Structure (each layer):
- RY(xᵢ) on qubit i — feature encoding
- RZZ(θ) on all nearest-neighbor pairs in a ring — entanglement with periodic boundary
- RX(π/6) on all qubits — uniform translationally-invariant rotation
The ring topology with identical gates at every position ensures translational invariance, which is the key to cyclic equivariance.
1. Installation & Setup¶
# Install the library (uncomment if not already installed)
# !pip install encoding-atlas
# For full multi-backend support:
# !pip install encoding-atlas[qiskit] # Qiskit backend
# !pip install encoding-atlas[cirq] # Cirq backend
# Or install everything:
# !pip install encoding-atlas[all]
import json
import numpy as np
import warnings
# Core imports
from encoding_atlas import CyclicEquivariantFeatureMap
from encoding_atlas.core.properties import EncodingProperties
print("encoding-atlas imported successfully!")
print(f"NumPy version: {np.__version__}")
encoding-atlas imported successfully! 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'] = None
try:
import qiskit
backends_available['qiskit'] = qiskit.__version__
except ImportError:
backends_available['qiskit'] = None
try:
import cirq
backends_available['cirq'] = cirq.__version__
except ImportError:
backends_available['cirq'] = None
print("Backend availability:")
for name, version in backends_available.items():
status = f"v{version}" if version else "NOT INSTALLED"
print(f" {name:12s}: {status}")
Backend availability: pennylane : v0.42.3 qiskit : v2.3.0 cirq : v1.5.0
2. Creating a CyclicEquivariantFeatureMap¶
Constructor Signature¶
CyclicEquivariantFeatureMap(
n_features: int, # Number of features (= number of qubits). Must be >= 2.
reps: int = 2, # Number of encoding layer repetitions. Must be >= 1.
coupling_strength: float = np.pi / 4, # RZZ entangling gate strength. Must be finite.
)
# Basic creation — only n_features is required
enc = CyclicEquivariantFeatureMap(n_features=4)
print(f"Encoding: {enc}")
print(f" n_features = {enc.n_features}")
print(f" n_qubits = {enc.n_qubits}")
print(f" reps = {enc.reps}")
print(f" coupling_strength = {enc.coupling_strength:.6f} (π/4 ≈ {np.pi/4:.6f})")
Encoding: CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483) n_features = 4 n_qubits = 4 reps = 2 coupling_strength = 0.785398 (π/4 ≈ 0.785398)
# Custom repetitions
enc_3rep = CyclicEquivariantFeatureMap(n_features=4, reps=3)
print(f"3 reps: depth = {enc_3rep.depth}")
# Custom coupling strength
enc_strong = CyclicEquivariantFeatureMap(n_features=4, coupling_strength=np.pi / 2)
print(f"Strong coupling (π/2): coupling_strength = {enc_strong.coupling_strength:.4f}")
# Minimum features (n=2)
enc_min = CyclicEquivariantFeatureMap(n_features=2)
print(f"Minimum features: n_features={enc_min.n_features}, n_qubits={enc_min.n_qubits}")
# Odd number of features (perfectly valid)
enc_odd = CyclicEquivariantFeatureMap(n_features=5)
print(f"Odd features: n_features={enc_odd.n_features}, n_qubits={enc_odd.n_qubits}")
# Large feature count
enc_large = CyclicEquivariantFeatureMap(n_features=10, reps=1)
print(f"Large: n_features={enc_large.n_features}, depth={enc_large.depth}")
3 reps: depth = 9 Strong coupling (π/2): coupling_strength = 1.5708 Minimum features: n_features=2, n_qubits=2 Odd features: n_features=5, n_qubits=5 Large: n_features=10, depth=3
3. Constructor Validation¶
The constructor performs rigorous validation on all parameters, including type checks
that reject bool (even though bool is a subclass of int in Python).
# --- n_features validation ---
# n_features=0 is rejected
try:
CyclicEquivariantFeatureMap(n_features=0)
except ValueError as e:
print(f"n_features=0: ValueError — {e}")
# Negative n_features
try:
CyclicEquivariantFeatureMap(n_features=-3)
except ValueError as e:
print(f"\nn_features=-3: ValueError — {e}")
n_features=0: ValueError — CyclicEquivariantFeatureMap requires n_features >= 2 for meaningful cyclic symmetry (Z_n group with n >= 2). Got n_features=0. For single-feature encoding, use AngleEncoding instead. n_features=-3: ValueError — CyclicEquivariantFeatureMap requires n_features >= 2 for meaningful cyclic symmetry (Z_n group with n >= 2). Got n_features=-3. For single-feature encoding, use AngleEncoding instead.
# --- Type validation for n_features ---
# Float is rejected (BaseEncoding checks isinstance(n_features, int))
try:
CyclicEquivariantFeatureMap(n_features=4.0)
except (TypeError, ValueError) as e:
print(f"n_features=4.0 (float): {type(e).__name__} — {e}")
# String is rejected
try:
CyclicEquivariantFeatureMap(n_features="4")
except (TypeError, ValueError) as e:
print(f"\nn_features='4' (str): {type(e).__name__} — {e}")
n_features=4.0 (float): TypeError — n_features must be an integer, got float n_features='4' (str): TypeError — n_features must be an integer, got str
# --- reps validation ---
# reps=0 is rejected
try:
CyclicEquivariantFeatureMap(n_features=4, reps=0)
except ValueError as e:
print(f"reps=0: ValueError — {e}")
# Negative reps
try:
CyclicEquivariantFeatureMap(n_features=4, reps=-1)
except ValueError as e:
print(f"\nreps=-1: ValueError — {e}")
# Bool reps
try:
CyclicEquivariantFeatureMap(n_features=4, reps=True)
except ValueError as e:
print(f"\nreps=True (bool): ValueError — {e}")
# Float reps
try:
CyclicEquivariantFeatureMap(n_features=4, reps=2.5)
except ValueError as e:
print(f"\nreps=2.5 (float): ValueError — {e}")
reps=0: ValueError — reps must be a positive integer, got 0 reps=-1: ValueError — reps must be a positive integer, got -1 reps=True (bool): ValueError — reps must be a positive integer, got True reps=2.5 (float): ValueError — reps must be a positive integer, got 2.5
# --- coupling_strength validation ---
# Infinity rejected
try:
CyclicEquivariantFeatureMap(n_features=4, coupling_strength=float('inf'))
except (TypeError, ValueError) as e:
print(f"coupling_strength=inf: {type(e).__name__} — {e}")
# Negative infinity rejected
try:
CyclicEquivariantFeatureMap(n_features=4, coupling_strength=float('-inf'))
except (TypeError, ValueError) as e:
print(f"\ncoupling_strength=-inf: {type(e).__name__} — {e}")
# NaN rejected
try:
CyclicEquivariantFeatureMap(n_features=4, coupling_strength=float('nan'))
except (TypeError, ValueError) as e:
print(f"\ncoupling_strength=NaN: {type(e).__name__} — {e}")
coupling_strength=inf: ValueError — coupling_strength must be finite, got inf coupling_strength=-inf: ValueError — coupling_strength must be finite, got -inf coupling_strength=NaN: ValueError — coupling_strength must be finite, got nan
# --- Valid edge cases for coupling_strength ---
# Zero coupling is valid (disables entanglement)
enc_zero = CyclicEquivariantFeatureMap(n_features=4, coupling_strength=0.0)
print(f"Zero coupling: coupling_strength = {enc_zero.coupling_strength}")
# Negative coupling is valid
enc_neg = CyclicEquivariantFeatureMap(n_features=4, coupling_strength=-np.pi / 4)
print(f"Negative coupling: coupling_strength = {enc_neg.coupling_strength:.4f}")
# Integer coupling is valid (auto-converted)
enc_int = CyclicEquivariantFeatureMap(n_features=4, coupling_strength=1)
print(f"Integer coupling: coupling_strength = {enc_int.coupling_strength}")
Zero coupling: coupling_strength = 0.0 Negative coupling: coupling_strength = -0.7854 Integer coupling: coupling_strength = 1
# --- Large n_features warning ---
# n_features > 20 emits a UserWarning about hardware connectivity
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_warn = CyclicEquivariantFeatureMap(n_features=21)
if w:
print(f"Warning emitted for n_features=21:")
print(f" {w[0].category.__name__}: {w[0].message}")
else:
print("No warning emitted")
# n_features=20 does NOT emit a warning (at the threshold)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_thresh = CyclicEquivariantFeatureMap(n_features=20)
print(f"\nn_features=20: warnings emitted = {len(w)}")
Large feature count (21) with ring topology may have hardware compatibility issues
Warning emitted for n_features=21: UserWarning: Large n_features (21) with ring topology may have limited connectivity on some quantum hardware. Linear chains are more common than circular topologies on superconducting devices. n_features=20: warnings emitted = 0
4. Core Properties¶
| Property | Description | Formula |
|---|---|---|
n_features |
Number of classical features | Set at construction |
n_qubits |
Number of qubits (always equals n_features) | n_qubits = n_features |
depth |
Circuit depth | depth = 3 × reps |
for n in [2, 3, 4, 6, 8]:
enc = CyclicEquivariantFeatureMap(n_features=n, reps=2)
print(f"n_features={n}: n_qubits={enc.n_qubits}, depth={enc.depth}")
assert enc.n_qubits == enc.n_features, "n_qubits must equal n_features"
assert enc.depth == 3 * enc.reps, "depth must equal 3 * reps"
print("\nAll assertions passed: n_qubits == n_features, depth == 3 * reps")
n_features=2: n_qubits=2, depth=6 n_features=3: n_qubits=3, depth=6 n_features=4: n_qubits=4, depth=6 n_features=6: n_qubits=6, depth=6 n_features=8: n_qubits=8, depth=6 All assertions passed: n_qubits == n_features, depth == 3 * reps
# Depth scales with reps
for reps in [1, 2, 3, 4, 5]:
enc = CyclicEquivariantFeatureMap(n_features=4, reps=reps)
print(f"reps={reps}: depth={enc.depth} (= 3 × {reps})")
reps=1: depth=3 (= 3 × 1) reps=2: depth=6 (= 3 × 2) reps=3: depth=9 (= 3 × 3) reps=4: depth=12 (= 3 × 4) reps=5: depth=15 (= 3 × 5)
5. Encoding Properties (Lazy, Thread-Safe)¶
The properties attribute returns an EncodingProperties frozen dataclass. It is
lazily computed on first access and cached using a thread-safe double-checked
locking pattern.
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
props = enc.properties
print(f"EncodingProperties for {enc}")
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" trainability_est. = {props.trainability_estimate}")
EncodingProperties for CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483) n_qubits = 4 depth = 6 gate_count = 24 single_qubit_gates = 16 two_qubit_gates = 8 parameter_count = 0 is_entangling = True simulability = not_simulable trainability_est. = None
# The properties object is frozen (immutable)
try:
enc.properties.n_qubits = 10
except AttributeError as e:
print(f"Cannot modify frozen properties: {e}")
# to_dict() for easy serialization
props_dict = enc.properties.to_dict()
print(f"\nProperties as dict: {json.dumps({k: str(v) for k, v in props_dict.items()}, indent=2)}")
Cannot modify frozen properties: cannot assign to field 'n_qubits'
Properties as dict: {
"n_qubits": "4",
"depth": "6",
"gate_count": "24",
"single_qubit_gates": "16",
"two_qubit_gates": "8",
"parameter_count": "0",
"is_entangling": "True",
"simulability": "not_simulable",
"expressibility": "None",
"entanglement_capability": "None",
"trainability_estimate": "None",
"noise_resilience_estimate": "None",
"notes": ""
}
# Verify properties are cached (same object returned on second access)
props1 = enc.properties
props2 = enc.properties
print(f"Properties cached (same object): {props1 is props2}")
# Verify key invariants
assert props.single_qubit_gates + props.two_qubit_gates == props.gate_count
assert props.is_entangling is True # Cyclic encoding always entangles (has RZZ gates)
assert props.simulability == "not_simulable"
print("All property invariants verified!")
Properties cached (same object): True All property invariants verified!
6. Configuration¶
The config property returns a defensive copy of the encoding-specific parameters
passed to the constructor (excluding n_features).
enc = CyclicEquivariantFeatureMap(n_features=4, reps=3, coupling_strength=0.5)
config = enc.config
print(f"Config: {config}")
# It's a defensive copy — modifying it doesn't affect the encoding
config['reps'] = 999
print(f"\nModified copy: {config}")
print(f"Original config: {enc.config}")
assert enc.config['reps'] == 3, "Original config must be unchanged"
print("Defensive copy verified!")
Config: {'reps': 3, 'coupling_strength': 0.5}
Modified copy: {'reps': 999, 'coupling_strength': 0.5}
Original config: {'reps': 3, 'coupling_strength': 0.5}
Defensive copy verified!
7. Circuit Generation — PennyLane Backend¶
get_circuit(x, backend='pennylane') returns a callable (quantum function) that can be
used inside a PennyLane QNode.
import pennylane as qml
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
# get_circuit returns a callable
circuit_fn = enc.get_circuit(x, backend='pennylane')
print(f"Type: {type(circuit_fn)}")
print(f"Callable: {callable(circuit_fn)}")
Type: <class '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 get_state(x_input):
circuit_fn = enc.get_circuit(x_input, backend='pennylane')
circuit_fn()
return qml.state()
state = get_state(x)
print(f"State vector (first 8 amplitudes):")
for i, amp in enumerate(state):
if abs(amp) > 1e-10:
print(f" |{i:04b}⟩: {amp:.6f}")
# Verify normalization
norm = np.sum(np.abs(state) ** 2)
print(f"\nNorm: {norm:.10f}")
assert np.isclose(norm, 1.0), "State must be normalized"
print("State is properly normalized!")
State vector (first 8 amplitudes): |0000⟩: -0.904545+0.083533j |0001⟩: -0.057640+0.072844j |0010⟩: -0.106875+0.119711j |0011⟩: -0.026576+0.005566j |0100⟩: -0.124846+0.159525j |0101⟩: 0.002758+0.016528j |0110⟩: -0.016456+0.062181j |0111⟩: -0.007014+0.005862j |1000⟩: -0.168015+0.209972j |1001⟩: -0.026612+0.065312j |1010⟩: 0.003998+0.044108j |1011⟩: -0.005387+0.019199j |1100⟩: 0.010320+0.110355j |1101⟩: 0.021906+0.021881j |1110⟩: 0.025984+0.021488j |1111⟩: 0.007731+0.008627j Norm: 1.0000000000 State is properly normalized!
# Visualize the circuit using PennyLane's drawer
@qml.qnode(dev)
def draw_circuit():
circuit_fn = enc.get_circuit(x, backend='pennylane')
circuit_fn()
return qml.state()
print(qml.draw(draw_circuit)())
0: ──RY(0.10)─╭IsingZZ(0.79)───────────────────────────────╭IsingZZ(0.79)──RX(0.52)──RY(0.10) ··· 1: ──RY(0.20)─╰IsingZZ(0.79)─╭IsingZZ(0.79)────────────────│───────────────RX(0.52)──RY(0.20) ··· 2: ──RY(0.30)────────────────╰IsingZZ(0.79)─╭IsingZZ(0.79)─│───────────────RX(0.52)──RY(0.30) ··· 3: ──RY(0.40)───────────────────────────────╰IsingZZ(0.79)─╰IsingZZ(0.79)──RX(0.52)──RY(0.40) ··· 0: ··· ─╭IsingZZ(0.79)───────────────────────────────╭IsingZZ(0.79)──RX(0.52)─┤ State 1: ··· ─╰IsingZZ(0.79)─╭IsingZZ(0.79)────────────────│───────────────RX(0.52)─┤ State 2: ··· ────────────────╰IsingZZ(0.79)─╭IsingZZ(0.79)─│───────────────RX(0.52)─┤ State 3: ··· ───────────────────────────────╰IsingZZ(0.79)─╰IsingZZ(0.79)──RX(0.52)─┤ State
# Verify different inputs produce different states
x1 = np.array([0.1, 0.2, 0.3, 0.4])
x2 = np.array([0.5, 0.6, 0.7, 0.8])
state1 = get_state(x1)
state2 = get_state(x2)
fidelity = np.abs(np.vdot(state1, state2)) ** 2
print(f"Fidelity between different inputs: {fidelity:.6f}")
assert fidelity < 1.0, "Different inputs should produce different states"
print("Different inputs produce different states!")
Fidelity between different inputs: 0.732719 Different inputs produce different states!
8. Circuit Generation — Qiskit Backend¶
try:
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
HAS_QISKIT = True
except ImportError:
HAS_QISKIT = False
print("Qiskit not installed — skipping this section")
if HAS_QISKIT:
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
qiskit_circuit = enc.get_circuit(x, backend='qiskit')
print(f"Type: {type(qiskit_circuit).__name__}")
print(f"Qubits: {qiskit_circuit.num_qubits}")
print(f"Depth: {qiskit_circuit.depth()}")
print(f"\nCircuit diagram:")
print(qiskit_circuit.draw())
Type: QuantumCircuit
Qubits: 4
Depth: 12
Circuit diagram:
┌─────────┐ ┌─────────┐┌─────────┐»
q_0: ┤ Ry(0.1) ├─■──────────────────────────────■────────┤ Rx(π/6) ├┤ Ry(0.1) ├»
├─────────┤ │ZZ(π/4) ┌─────────┐ │ ├─────────┤└─────────┘»
q_1: ┤ Ry(0.2) ├─■─────────■────────┤ Rx(π/6) ├─┼────────┤ Ry(0.2) ├───────────»
├─────────┤ │ZZ(π/4) └─────────┘ │ ├─────────┤┌─────────┐»
q_2: ┤ Ry(0.3) ├───────────■──────────■─────────┼────────┤ Rx(π/6) ├┤ Ry(0.3) ├»
├─────────┤ │ZZ(π/4) │ZZ(π/4) ├─────────┤├─────────┤»
q_3: ┤ Ry(0.4) ├──────────────────────■─────────■────────┤ Rx(π/6) ├┤ Ry(0.4) ├»
└─────────┘ └─────────┘└─────────┘»
« ┌─────────┐
«q_0: ─■──────────────────────────────■────────┤ Rx(π/6) ├
« │ZZ(π/4) ┌─────────┐ │ └─────────┘
«q_1: ─■─────────■────────┤ Rx(π/6) ├─┼───────────────────
« │ZZ(π/4) └─────────┘ │ ┌─────────┐
«q_2: ───────────■──────────■─────────┼────────┤ Rx(π/6) ├
« │ZZ(π/4) │ZZ(π/4) ├─────────┤
«q_3: ──────────────────────■─────────■────────┤ Rx(π/6) ├
« └─────────┘
if HAS_QISKIT:
# Simulate and get statevector
sv = Statevector(qiskit_circuit)
state_qiskit = np.array(sv)
print("Qiskit statevector (non-zero amplitudes):")
for i, amp in enumerate(state_qiskit):
if abs(amp) > 1e-10:
print(f" |{i:04b}⟩: {amp:.6f}")
print(f"\nNorm: {np.sum(np.abs(state_qiskit)**2):.10f}")
Qiskit statevector (non-zero amplitudes): |0000⟩: -0.904545+0.083533j |0001⟩: -0.168015+0.209972j |0010⟩: -0.124846+0.159525j |0011⟩: 0.010320+0.110355j |0100⟩: -0.106875+0.119711j |0101⟩: 0.003998+0.044108j |0110⟩: -0.016456+0.062181j |0111⟩: 0.025984+0.021488j |1000⟩: -0.057640+0.072844j |1001⟩: -0.026612+0.065312j |1010⟩: 0.002758+0.016528j |1011⟩: 0.021906+0.021881j |1100⟩: -0.026576+0.005566j |1101⟩: -0.005387+0.019199j |1110⟩: -0.007014+0.005862j |1111⟩: 0.007731+0.008627j Norm: 1.0000000000
9. Circuit Generation — Cirq Backend¶
try:
import cirq
HAS_CIRQ = True
except ImportError:
HAS_CIRQ = False
print("Cirq not installed — skipping this section")
if HAS_CIRQ:
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
cirq_circuit = enc.get_circuit(x, backend='cirq')
print(f"Type: {type(cirq_circuit).__name__}")
print(f"\nCircuit diagram:")
print(cirq_circuit)
Type: Circuit
Circuit diagram:
┌─────────────────┐ ┌─────────────────┐
0: ───Ry(0.032π)───ZZ────────────────────────────────ZZ───────────────────Rx(0.167π)───Ry(0.032π)───ZZ────────────────────────────────ZZ───────────────────Rx(0.167π)───
│ │ │ │
1: ───Ry(0.064π)───ZZ^0.25───ZZ────────Rx(0.167π)────┼──────Ry(0.064π)──────────────────────────────ZZ^0.25───ZZ────────Rx(0.167π)────┼─────────────────────────────────
│ │ │ │
2: ───Ry(0.095π)─────────────ZZ^0.25───ZZ────────────┼──────Rx(0.167π)────Ry(0.095π)──────────────────────────ZZ^0.25───ZZ────────────┼──────Rx(0.167π)─────────────────
│ │ │ │
3: ───Ry(0.127π)───────────────────────ZZ^0.25───────ZZ^0.25──────────────Rx(0.167π)───Ry(0.127π)───────────────────────ZZ^0.25───────ZZ^0.25──────────────Rx(0.167π)───
└─────────────────┘ └─────────────────┘
if HAS_CIRQ:
# Simulate Cirq circuit
simulator = cirq.Simulator()
result = simulator.simulate(cirq_circuit)
state_cirq = result.final_state_vector
print("Cirq statevector (non-zero amplitudes):")
for i, amp in enumerate(state_cirq):
if abs(amp) > 1e-10:
print(f" |{i:04b}⟩: {amp:.6f}")
print(f"\nNorm: {np.sum(np.abs(state_cirq)**2):.10f}")
Cirq statevector (non-zero amplitudes): |0000⟩: 0.904545-0.083533j |0001⟩: 0.057640-0.072844j |0010⟩: 0.106875-0.119711j |0011⟩: 0.026576-0.005566j |0100⟩: 0.124846-0.159525j |0101⟩: -0.002758-0.016528j |0110⟩: 0.016456-0.062181j |0111⟩: 0.007014-0.005862j |1000⟩: 0.168015-0.209972j |1001⟩: 0.026612-0.065312j |1010⟩: -0.003998-0.044108j |1011⟩: 0.005387-0.019199j |1100⟩: -0.010320-0.110355j |1101⟩: -0.021906-0.021881j |1110⟩: -0.025984-0.021488j |1111⟩: -0.007731-0.008627j Norm: 1.0000001192
10. Group Action — Cyclic Shifts¶
The group_action(k, x) method cyclically shifts features by k positions. This defines
how the cyclic group Z_n acts on the classical input space.
$$\sigma^k \cdot (x_0, x_1, \ldots, x_{n-1}) = (x_k, x_{k+1}, \ldots, x_{k+n-1 \bmod n})$$
enc = CyclicEquivariantFeatureMap(n_features=4)
x = np.array([10, 20, 30, 40])
print(f"Original: x = {x}")
print(f"Shift by 0: σ⁰·x = {enc.group_action(0, x)}")
print(f"Shift by 1: σ¹·x = {enc.group_action(1, x)}")
print(f"Shift by 2: σ²·x = {enc.group_action(2, x)}")
print(f"Shift by 3: σ³·x = {enc.group_action(3, x)}")
# Full cycle returns to original
print(f"\nShift by 4 (full cycle): σ⁴·x = {enc.group_action(4, x)}")
assert np.array_equal(enc.group_action(4, x), x), "Full cycle must return to original"
print("Full cycle identity verified!")
Original: x = [10 20 30 40] Shift by 0: σ⁰·x = [10 20 30 40] Shift by 1: σ¹·x = [20 30 40 10] Shift by 2: σ²·x = [30 40 10 20] Shift by 3: σ³·x = [40 10 20 30] Shift by 4 (full cycle): σ⁴·x = [10 20 30 40] Full cycle identity verified!
# Negative shifts work correctly
print(f"Shift by -1: σ⁻¹·x = {enc.group_action(-1, x)}")
print(f"Shift by -2: σ⁻²·x = {enc.group_action(-2, x)}")
# Negative shift is equivalent to (n - k) positive shift
assert np.array_equal(enc.group_action(-1, x), enc.group_action(3, x))
print("\nNegative shift equivalence verified: σ⁻¹ = σ³ (mod 4)")
Shift by -1: σ⁻¹·x = [40 10 20 30] Shift by -2: σ⁻²·x = [30 40 10 20] Negative shift equivalence verified: σ⁻¹ = σ³ (mod 4)
# Group composition: σ^a(σ^b(x)) = σ^(a+b)(x)
a, b = 1, 2
composed = enc.group_action(a, enc.group_action(b, x))
direct = enc.group_action(a + b, x)
assert np.array_equal(composed, direct)
print(f"Group composition: σ¹(σ²(x)) = σ³(x) = {direct}")
print("Composition law verified!")
Group composition: σ¹(σ²(x)) = σ³(x) = [40 10 20 30] Composition law verified!
11. Unitary Representation¶
The unitary_representation(k) method returns the unitary matrix U(k) that implements
the cyclic permutation on the Hilbert space. It maps computational basis states by
permuting qubit indices cyclically.
enc = CyclicEquivariantFeatureMap(n_features=3)
# Identity (k=0)
U0 = enc.unitary_representation(0)
print("U(0) — Identity permutation:")
print(U0.astype(int))
assert np.allclose(U0, np.eye(2**3)), "U(0) must be identity"
print("U(0) = I verified!")
U(0) — Identity permutation: [[1 0 0 0 0 0 0 0] [0 1 0 0 0 0 0 0] [0 0 1 0 0 0 0 0] [0 0 0 1 0 0 0 0] [0 0 0 0 1 0 0 0] [0 0 0 0 0 1 0 0] [0 0 0 0 0 0 1 0] [0 0 0 0 0 0 0 1]] U(0) = I verified!
# The unitary is a permutation matrix (each row and column has exactly one 1)
U1 = enc.unitary_representation(1)
print("U(1) — Single cyclic shift:")
print(U1.astype(int))
# Verify it's unitary: U†U = I
assert np.allclose(U1.conj().T @ U1, np.eye(2**3)), "Must be unitary"
print("\nUnitarity verified: U†U = I")
# Verify it's a permutation matrix
assert np.all(np.sum(np.abs(U1), axis=0) == 1), "Column sums must be 1"
assert np.all(np.sum(np.abs(U1), axis=1) == 1), "Row sums must be 1"
print("Permutation matrix structure verified!")
U(1) — Single cyclic shift: [[1 0 0 0 0 0 0 0] [0 0 0 0 1 0 0 0] [0 1 0 0 0 0 0 0] [0 0 0 0 0 1 0 0] [0 0 1 0 0 0 0 0] [0 0 0 0 0 0 1 0] [0 0 0 1 0 0 0 0] [0 0 0 0 0 0 0 1]] Unitarity verified: U†U = I Permutation matrix structure verified!
# Group homomorphism: U(a) @ U(b) = U((a+b) mod n)
enc = CyclicEquivariantFeatureMap(n_features=4)
U1 = enc.unitary_representation(1)
U2 = enc.unitary_representation(2)
U3 = enc.unitary_representation(3)
assert np.allclose(U1 @ U2, U3), "U(1)·U(2) must equal U(3)"
assert np.allclose(U1 @ U1, U2), "U(1)·U(1) must equal U(2)"
assert np.allclose(U1 @ U1 @ U1, U3), "U(1)³ must equal U(3)"
assert np.allclose(U1 @ U1 @ U1 @ U1, np.eye(2**4)), "U(1)⁴ must equal I"
print("Group homomorphism verified:")
print(" U(1)·U(2) = U(3)")
print(" U(1)² = U(2)")
print(" U(1)³ = U(3)")
print(" U(1)⁴ = I (cyclic order = 4)")
Group homomorphism verified: U(1)·U(2) = U(3) U(1)² = U(2) U(1)³ = U(3) U(1)⁴ = I (cyclic order = 4)
12. Group Generators¶
The cyclic group Z_n is generated by a single element: the shift by one position. Testing equivariance on this generator is sufficient to guarantee equivariance for the entire group.
for n in [2, 3, 4, 8]:
enc = CyclicEquivariantFeatureMap(n_features=n)
gens = enc.group_generators()
print(f"Z_{n} generators: {gens}")
# There is always exactly one generator: [1]
enc = CyclicEquivariantFeatureMap(n_features=6)
assert enc.group_generators() == [1]
print("\nAll cyclic groups have a single generator: [1]")
Z_2 generators: [1] Z_3 generators: [1] Z_4 generators: [1] Z_8 generators: [1] All cyclic groups have a single generator: [1]
13. Equivariance Verification (Exact)¶
The core mathematical guarantee: U(g)|ψ(x)⟩ = |ψ(g·x)⟩
verify_equivariance(x, g) computes both sides and checks that the state overlap
|⟨left|right⟩| ≈ 1 (accounting for global phase ambiguity).
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
# Verify equivariance for each possible cyclic shift
print("Verifying equivariance for all shifts:")
for k in range(4):
result = enc.verify_equivariance(x, k)
print(f" Shift k={k}: equivariant = {result}")
assert result, f"Equivariance must hold for k={k}"
print("\nAll shifts verified! Cyclic equivariance holds.")
Verifying equivariance for all shifts: Shift k=0: equivariant = True Shift k=1: equivariant = True Shift k=2: equivariant = True Shift k=3: equivariant = True All shifts verified! Cyclic equivariance holds.
# Detailed verification returns overlap values
result = enc.verify_equivariance_detailed(x, g=1)
print("Detailed verification result:")
for key, val in result.items():
print(f" {key}: {val}")
assert result['equivariant'] is True
assert result['overlap'] > 0.9999999
print(f"\nOverlap = {result['overlap']:.12f} (≈1.0)")
Detailed verification result: equivariant: True overlap: 1.0000000000000004 tolerance: 1e-10 group_element: 1 Overlap = 1.000000000000 (≈1.0)
# Test with various input patterns
test_inputs = [
("zeros", np.zeros(4)),
("ones", np.ones(4)),
("pi values", np.array([np.pi, np.pi/2, np.pi/3, np.pi/4])),
("negative", np.array([-0.5, -1.0, -1.5, -2.0])),
("large", np.array([10.0, 20.0, 30.0, 40.0])),
("small", np.array([1e-6, 2e-6, 3e-6, 4e-6])),
("mixed sign", np.array([-1.0, 0.5, -0.3, 2.0])),
]
print("Equivariance with various inputs (shift k=1):")
for name, x_test in test_inputs:
result = enc.verify_equivariance(x_test, g=1)
print(f" {name:15s}: {result}")
Equivariance with various inputs (shift k=1): zeros : True ones : True pi values : True negative : True large : True small : True mixed sign : True
14. Equivariance on Generators¶
verify_equivariance_on_generators(x) tests equivariance on all group generators.
Since Z_n has a single generator, this is equivalent to verify_equivariance(x, 1).
For more complex groups this would test multiple generators.
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
result = enc.verify_equivariance_on_generators(x)
print(f"Equivariance on generators: {result}")
assert result is True
# Also works with different configurations
for n in [2, 3, 5, 6]:
enc_n = CyclicEquivariantFeatureMap(n_features=n)
x_n = np.random.default_rng(42).random(n)
result = enc_n.verify_equivariance_on_generators(x_n)
print(f"Z_{n} equivariance on generators: {result}")
Equivariance on generators: True Z_2 equivariance on generators: True Z_3 equivariance on generators: True Z_5 equivariance on generators: True Z_6 equivariance on generators: True
15. Statistical Verification¶
For large systems where exact state vector verification requires exponential memory,
verify_equivariance_statistical uses measurement sampling and chi-squared tests
to verify equivariance statistically.
| Parameter | Default | Description |
|---|---|---|
n_shots |
10,000 | Measurement shots per circuit |
significance |
0.01 | Type I error probability |
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
result = enc.verify_equivariance_statistical(x, g=1, n_shots=10000)
print("Statistical verification result:")
for key, val in result.items():
print(f" {key:20s}: {val}")
Statistical verification result: equivariant : True p_value : 0.9040226770387134 test_statistic : 5.509273101576799 significance : 0.01 n_shots : 10000 group_element : 1 method : chi_squared confidence_level : 0.99
# Higher confidence with more shots
result_high = enc.verify_equivariance_statistical(
x, g=1, n_shots=50000, significance=0.001
)
print(f"High-confidence verification:")
print(f" equivariant: {result_high['equivariant']}")
print(f" p_value: {result_high['p_value']:.6f}")
print(f" confidence_level: {result_high['confidence_level']}")
print(f" method: {result_high['method']}")
High-confidence verification: equivariant: True p_value: 0.461908 confidence_level: 0.999 method: chi_squared
# Validation: n_shots must be >= 100
try:
enc.verify_equivariance_statistical(x, g=1, n_shots=50)
except ValueError as e:
print(f"n_shots=50: ValueError — {e}")
# Validation: significance must be in (0, 1)
try:
enc.verify_equivariance_statistical(x, g=1, significance=0.0)
except ValueError as e:
print(f"\nsignificance=0: ValueError — {e}")
try:
enc.verify_equivariance_statistical(x, g=1, significance=1.0)
except ValueError as e:
print(f"\nsignificance=1: ValueError — {e}")
n_shots=50: ValueError — n_shots must be at least 100 for meaningful statistics, got 50 significance=0: ValueError — significance must be in (0, 1), got 0.0 significance=1: ValueError — significance must be in (0, 1), got 1.0
16. Auto Verification¶
verify_equivariance_auto automatically selects the best verification method based
on system size:
- n_qubits ≤ 12: Exact verification (precise)
- n_qubits > 12: Statistical verification (scalable)
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
# For small systems, uses exact verification
result = enc.verify_equivariance_auto(x, g=1)
print(f"Auto verification (4 qubits → exact): {result}")
# Also supports all group elements
for k in range(4):
r = enc.verify_equivariance_auto(x, k)
print(f" k={k}: {r}")
Auto verification (4 qubits → exact): True k=0: True k=1: True k=2: True k=3: True
17. Entanglement Pairs — Ring Topology¶
The get_entanglement_pairs() method returns the qubit pairs connected by RZZ gates.
The cyclic encoding uses a ring topology with periodic boundary conditions:
qubit (n-1) connects back to qubit 0.
for n in [2, 3, 4, 6, 8]:
enc = CyclicEquivariantFeatureMap(n_features=n)
pairs = enc.get_entanglement_pairs()
print(f"n={n}: {pairs}")
# Verify ring: last pair wraps around
assert pairs[-1] == (n - 1, 0), f"Last pair must be ({n-1}, 0)"
# Verify count
assert len(pairs) == n, f"Ring topology must have exactly n={n} pairs"
print("\nAll ring topologies verified! (includes periodic boundary)")
n=2: [(0, 1), (1, 0)] n=3: [(0, 1), (1, 2), (2, 0)] n=4: [(0, 1), (1, 2), (2, 3), (3, 0)] n=6: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)] n=8: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 0)] All ring topologies verified! (includes periodic boundary)
18. Gate Count Breakdown¶
gate_count_breakdown() returns a CyclicGateCountBreakdown TypedDict with counts for
each gate type.
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
breakdown = enc.gate_count_breakdown()
print(f"Gate count breakdown for n_features=4, reps=2:")
print(f" RY gates: {breakdown['ry']}")
print(f" RZZ gates: {breakdown['rzz']}")
print(f" RX gates: {breakdown['rx']}")
print(f" Total single-qubit: {breakdown['total_single_qubit']}")
print(f" Total two-qubit: {breakdown['total_two_qubit']}")
print(f" Total: {breakdown['total']}")
# Verify formulas
n, reps = 4, 2
assert breakdown['ry'] == n * reps
assert breakdown['rzz'] == n * reps # Ring: n pairs per layer
assert breakdown['rx'] == n * reps
assert breakdown['total_single_qubit'] == breakdown['ry'] + breakdown['rx']
assert breakdown['total_two_qubit'] == breakdown['rzz']
assert breakdown['total'] == breakdown['total_single_qubit'] + breakdown['total_two_qubit']
print("\nAll gate count formulas verified!")
Gate count breakdown for n_features=4, reps=2: RY gates: 8 RZZ gates: 8 RX gates: 8 Total single-qubit: 16 Total two-qubit: 8 Total: 24 All gate count formulas verified!
# Gate counts scale linearly with n_features and reps
print("Gate scaling with n_features (reps=2):")
print(f"{'n':>4s} {'RY':>5s} {'RZZ':>5s} {'RX':>5s} {'1Q':>5s} {'2Q':>5s} {'Total':>6s}")
print("-" * 38)
for n in [2, 4, 6, 8, 10]:
enc = CyclicEquivariantFeatureMap(n_features=n, reps=2)
b = enc.gate_count_breakdown()
print(f"{n:4d} {b['ry']:5d} {b['rzz']:5d} {b['rx']:5d} {b['total_single_qubit']:5d} {b['total_two_qubit']:5d} {b['total']:6d}")
Gate scaling with n_features (reps=2): n RY RZZ RX 1Q 2Q Total -------------------------------------- 2 4 4 4 8 4 12 4 8 8 8 16 8 24 6 12 12 12 24 12 36 8 16 16 16 32 16 48 10 20 20 20 40 20 60
19. Resource Summary¶
resource_summary() provides a comprehensive view of the encoding's resources,
characteristics, and hardware requirements in a single call.
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
summary = enc.resource_summary()
print("=== Resource Summary ===")
print(f"\n--- Circuit 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" coupling_strength: {summary['coupling_strength']:.6f}")
print(f"\n--- Encoding Characteristics ---")
print(f" is_entangling: {summary['is_entangling']}")
print(f" simulability: {summary['simulability']}")
print(f" trainability_estimate: {summary['trainability_estimate']}")
print(f"\n--- Symmetry Information ---")
print(f" symmetry_group: {summary['symmetry_group']}")
print(f" cyclic_order: {summary['cyclic_order']}")
print(f"\n--- Hardware Requirements ---")
print(f" connectivity: {summary['hardware_requirements']['connectivity']}")
print(f" native_gates: {summary['hardware_requirements']['native_gates']}")
print(f"\n--- Entanglement ---")
print(f" n_entanglement_pairs: {summary['n_entanglement_pairs']}")
print(f" entanglement_pairs: {summary['entanglement_pairs']}")
print(f"\n--- Verification Methods ---")
for method in summary['verification_methods']:
print(f" - {method}")
print(f"\n--- Verification Cost ---")
for method, cost in summary['verification_cost'].items():
print(f" {method}: {cost}")
=== Resource Summary ===
--- Circuit Structure ---
n_qubits: 4
n_features: 4
depth: 6
reps: 2
coupling_strength: 0.785398
--- Encoding Characteristics ---
is_entangling: True
simulability: not_simulable
trainability_estimate: None
--- Symmetry Information ---
symmetry_group: Z_4
cyclic_order: 4
--- Hardware Requirements ---
connectivity: ring
native_gates: ['RY', 'RZZ', 'RX']
--- Entanglement ---
n_entanglement_pairs: 4
entanglement_pairs: [(0, 1), (1, 2), (2, 3), (3, 0)]
--- Verification Methods ---
- verify_equivariance (exact)
- verify_equivariance_statistical (scalable)
- verify_equivariance_auto (automatic selection)
--- Verification Cost ---
exact: {'memory': 'O(2^4)', 'time': 'O(2^4)', 'recommended_max_qubits': 12}
statistical: {'memory': 'O(n_shots)', 'time': 'O(n_shots × circuit_depth)', 'default_shots': 10000, 'scalable': True}
20. Batch Circuit Generation¶
get_circuits(X, backend, parallel=False, max_workers=None) generates circuits for
multiple data samples. Supports both sequential and parallel processing.
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
X = np.array([
[0.1, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8],
[1.0, 1.1, 1.2, 1.3],
])
# Sequential (default)
circuits_seq = enc.get_circuits(X, backend='pennylane')
print(f"Sequential: {len(circuits_seq)} circuits generated")
print(f"All callable: {all(callable(c) for c in circuits_seq)}")
# Parallel
circuits_par = enc.get_circuits(X, backend='pennylane', parallel=True)
print(f"\nParallel: {len(circuits_par)} circuits generated")
print(f"All callable: {all(callable(c) for c in circuits_par)}")
# Custom worker count
circuits_cust = enc.get_circuits(X, backend='pennylane', parallel=True, max_workers=2)
print(f"\nParallel (2 workers): {len(circuits_cust)} circuits generated")
Sequential: 3 circuits generated All callable: True Parallel: 3 circuits generated All callable: True Parallel (2 workers): 3 circuits generated
# Single sample as 1D array is handled gracefully
x_single = np.array([0.1, 0.2, 0.3, 0.4])
circuits_1d = enc.get_circuits(x_single, backend='pennylane')
print(f"1D input: {len(circuits_1d)} circuit(s) generated")
1D input: 1 circuit(s) generated
21. Input Validation & Edge Cases¶
The encoding validates input data rigorously before circuit generation.
enc = CyclicEquivariantFeatureMap(n_features=4)
# --- Shape validation ---
# Wrong number of features
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3]), backend='pennylane')
except ValueError as e:
print(f"Wrong features (3 instead of 4): {e}")
# Batch input in get_circuit (must be single sample)
try:
enc.get_circuit(np.array([[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8]]), backend='pennylane')
except ValueError as e:
print(f"\nBatch in get_circuit: {e}")
Wrong features (3 instead of 4): Expected 4 features, got 3 Batch in get_circuit: get_circuit requires a single sample
# --- Value validation ---
# NaN values
try:
enc.get_circuit(np.array([0.1, float('nan'), 0.3, 0.4]), backend='pennylane')
except ValueError as e:
print(f"NaN input: {e}")
# Infinity values
try:
enc.get_circuit(np.array([0.1, float('inf'), 0.3, 0.4]), backend='pennylane')
except ValueError as e:
print(f"\nInf input: {e}")
NaN input: Input contains NaN or infinite values Inf input: Input contains NaN or infinite values
# --- Type validation ---
# Complex numbers rejected
try:
enc.get_circuit(np.array([0.1 + 0.2j, 0.3, 0.4, 0.5]), backend='pennylane')
except (TypeError, ValueError) as e:
print(f"Complex input: {type(e).__name__} — {e}")
# --- Backend validation ---
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3, 0.4]), backend='unknown')
except ValueError as e:
print(f"\nUnknown backend: {e}")
Complex input: TypeError — Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only. Unknown backend: Unknown backend: unknown
# --- Accepted input formats ---
x_list = [0.1, 0.2, 0.3, 0.4] # Python list
x_tuple = (0.1, 0.2, 0.3, 0.4) # Python tuple
x_1d = np.array([0.1, 0.2, 0.3, 0.4]) # NumPy 1D
x_2d = np.array([[0.1, 0.2, 0.3, 0.4]]) # NumPy 2D (single sample)
x_int = np.array([1, 2, 3, 4]) # Integer (auto-converted)
for name, x_input in [("list", x_list), ("tuple", x_tuple), ("1D", x_1d),
("2D single", x_2d), ("int", x_int)]:
circuit = enc.get_circuit(x_input, backend='pennylane')
print(f"{name:12s}: callable={callable(circuit)}")
print("\nAll accepted formats work correctly!")
list : callable=True tuple : callable=True 1D : callable=True 2D single : callable=True int : callable=True All accepted formats work correctly!
# --- Defensive copy ---
# The encoding makes a copy; modifying the original array has no effect
x_original = np.array([0.1, 0.2, 0.3, 0.4])
circuit = enc.get_circuit(x_original.copy(), backend='pennylane')
# Even if we modify x_original, already-generated circuits are safe
x_original[0] = 999.0
print(f"Original modified to: {x_original}")
print("Previously generated circuit is unaffected (defensive copy)")
Original modified to: [9.99e+02 2.00e-01 3.00e-01 4.00e-01] Previously generated circuit is unaffected (defensive copy)
22. Resource Analysis Tools¶
The encoding_atlas.analysis module provides standalone analysis functions
that work with any encoding.
from encoding_atlas.analysis import (
count_resources,
get_resource_summary,
get_gate_breakdown,
compare_resources,
estimate_execution_time,
)
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
# count_resources — standardized summary
resources = count_resources(enc)
print("count_resources():")
for key, val in resources.items():
print(f" {key}: {val}")
count_resources(): n_qubits: 4 depth: 6 gate_count: 24 single_qubit_gates: 16 two_qubit_gates: 8 parameter_count: 0 cnot_count: 0 cz_count: 0 t_gate_count: 0 hadamard_count: 0 rotation_gates: 16 two_qubit_ratio: 0.3333333333333333 gates_per_qubit: 6.0 encoding_name: CyclicEquivariantFeatureMap is_data_dependent: False
# Detailed gate breakdown
detailed = count_resources(enc, detailed=True)
print("Detailed gate breakdown:")
for key, val in detailed.items():
print(f" {key}: {val}")
Detailed gate breakdown: rx: 8 ry: 8 rz: 0 h: 0 x: 0 y: 0 z: 0 s: 0 t: 0 cnot: 0 cx: 0 cz: 0 swap: 0 total_single_qubit: 16 total_two_qubit: 8 total: 24 encoding_name: CyclicEquivariantFeatureMap
# Compare resources across encodings
from encoding_atlas import AngleEncoding, IQPEncoding
encodings = [
CyclicEquivariantFeatureMap(n_features=4, reps=2),
AngleEncoding(n_features=4, reps=2),
IQPEncoding(n_features=4, reps=2),
]
comparison = compare_resources(encodings)
print("Resource comparison:")
for key, values in comparison.items():
print(f" {key}: {values}")
Resource comparison: n_qubits: [4, 4, 4] depth: [6, 2, 6] gate_count: [24, 8, 52] single_qubit_gates: [16, 8, 28] two_qubit_gates: [8, 0, 24] parameter_count: [0, 8, 20] two_qubit_ratio: [0.3333333333333333, 0.0, 0.46153846153846156] gates_per_qubit: [6.0, 2.0, 13.0] encoding_name: ['CyclicEquivariantFeatureMap', 'AngleEncoding', 'IQPEncoding']
# Estimate execution time
exec_time = estimate_execution_time(enc)
print("Estimated execution time:")
for key, val in exec_time.items():
print(f" {key}: {val:.2f} μs" if isinstance(val, float) else f" {key}: {val}")
Estimated execution time: serial_time_us: 2.92 μs estimated_time_us: 2.20 μs single_qubit_time_us: 0.32 μs two_qubit_time_us: 1.60 μs measurement_time_us: 1.00 μs parallelization_factor: 0.50 μs
23. Simulability Analysis¶
Determines whether the encoding circuit can be efficiently simulated classically. CyclicEquivariantFeatureMap uses RZZ entangling gates, making it not classically simulable.
from encoding_atlas.analysis import (
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
)
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
sim_result = check_simulability(enc)
print(f"Simulability result:")
print(f" is_simulable: {sim_result['is_simulable']}")
print(f" simulability_class: {sim_result['simulability_class']}")
print(f" reason: {sim_result['reason']}")
print(f" details: {sim_result['details']}")
print(f" recommendations: {sim_result['recommendations']}")
print(f"\nOne-line reason: {get_simulability_reason(enc)}")
print(f"Is Clifford: {is_clifford_circuit(enc)}")
print(f"Is matchgate: {is_matchgate_circuit(enc)}")
Simulability result:
is_simulable: False
simulability_class: conditionally_simulable
reason: Circular entanglement structure may allow tensor network simulation if entanglement entropy is bounded
details: {'is_entangling': True, 'is_clifford': False, 'is_matchgate': False, 'entanglement_pattern': 'circular', 'two_qubit_gate_count': 8, 'n_qubits': 4, 'n_features': 4, 'declared_simulability': 'not_simulable', 'encoding_name': 'CyclicEquivariantFeatureMap', 'has_non_clifford_gates': False, 'has_t_gates': False, 'has_parameterized_rotations': False}
recommendations: ['Statevector simulation feasible (4 qubits, ~256 bytes memory)', 'Consider MPS with periodic boundary conditions', 'May be efficient for bounded entanglement', 'DMRG-style algorithms may be applicable']
One-line reason: Not simulable: Circular entanglement structure may allow tensor network simulation if entanglement entropy is bounded
Is Clifford: False
Is matchgate: False
24. Expressibility Analysis¶
Measures how well the encoding covers the Hilbert space. Lower KL divergence from the Haar distribution means higher expressibility.
from encoding_atlas.analysis import compute_expressibility
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
# Scalar result
expr = compute_expressibility(enc, n_samples=500, seed=42)
print(f"Expressibility (KL divergence): {expr:.6f}")
print(" (lower = more expressive)")
Expressibility (KL divergence): 0.985695 (lower = more expressive)
# Detailed result with distributions
expr_detailed = compute_expressibility(enc, n_samples=500, seed=42, return_distributions=True)
print(f"Expressibility (detailed):")
print(f" expressibility: {expr_detailed['expressibility']:.6f}")
print(f" KL divergence: {expr_detailed['kl_divergence']:.6f}")
print(f" n_samples: {expr_detailed['n_samples']}")
print(f" mean_fidelity: {expr_detailed['mean_fidelity']:.6f}")
Expressibility (detailed): expressibility: 0.985695 KL divergence: 0.143050 n_samples: 500 mean_fidelity: 0.076360
# Compare expressibility across reps
print("Expressibility vs. reps:")
for reps in [1, 2, 3]:
enc_r = CyclicEquivariantFeatureMap(n_features=4, reps=reps)
e = compute_expressibility(enc_r, n_samples=300, seed=42)
print(f" reps={reps}: KL divergence = {e:.6f}")
Expressibility vs. reps: reps=1: KL divergence = 0.934434 reps=2: KL divergence = 0.989835 reps=3: KL divergence = 0.991928
25. Entanglement Capability¶
Measures how much entanglement the encoding generates using the Meyer-Wallach measure (0 = product state, 1 = maximally entangled).
from encoding_atlas.analysis import compute_entanglement_capability
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
ent = compute_entanglement_capability(enc, n_samples=300, seed=42)
print(f"Entanglement capability (Meyer-Wallach): {ent:.6f}")
print(" 0 = no entanglement, 1 = maximal entanglement")
Entanglement capability (Meyer-Wallach): 0.454090 0 = no entanglement, 1 = maximal entanglement
# Detailed result
ent_detailed = compute_entanglement_capability(enc, n_samples=300, seed=42, return_details=True)
print(f"Entanglement (detailed):")
print(f" entanglement_capability: {ent_detailed['entanglement_capability']:.6f}")
print(f" measure: {ent_detailed['measure']}")
print(f" n_samples: {ent_detailed['n_samples']}")
print(f" std_error: {ent_detailed['std_error']:.6f}")
Entanglement (detailed): entanglement_capability: 0.454090 measure: meyer_wallach n_samples: 300 std_error: 0.010711
# Zero coupling → no entanglement (product state)
enc_zero = CyclicEquivariantFeatureMap(n_features=4, coupling_strength=0.0)
ent_zero = compute_entanglement_capability(enc_zero, n_samples=300, seed=42)
print(f"Zero coupling entanglement: {ent_zero:.6f}")
print(" (Should be ~0: no RZZ entanglement)")
Zero coupling entanglement: 0.000000 (Should be ~0: no RZZ entanglement)
26. Trainability Analysis¶
Estimates the risk of barren plateaus — vanishing gradients that make optimization difficult in variational quantum circuits.
from encoding_atlas.analysis import estimate_trainability
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
# Scalar trainability estimate (0 = barren plateau, 1 = easy to train)
train = estimate_trainability(enc, n_samples=200, seed=42)
print(f"Trainability estimate: {train:.6f}")
Trainability estimate: 0.052790
# Detailed result
train_detailed = estimate_trainability(enc, n_samples=200, seed=42, return_details=True)
print(f"Trainability (detailed):")
print(f" trainability_estimate: {train_detailed['trainability_estimate']:.6f}")
print(f" gradient_variance: {train_detailed['gradient_variance']:.8f}")
print(f" barren_plateau_risk: {train_detailed['barren_plateau_risk']}")
print(f" n_samples: {train_detailed['n_samples']}")
print(f" n_successful_samples: {train_detailed['n_successful_samples']}")
Trainability (detailed): trainability_estimate: 0.052790 gradient_variance: 0.00360077 barren_plateau_risk: low n_samples: 200 n_successful_samples: 200
27. 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,
partial_trace_single_qubit,
validate_encoding_for_analysis,
)
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
# Statevector simulation
state = simulate_encoding_statevector(enc, x)
print(f"Statevector shape: {state.shape}")
print(f"Norm: {np.sum(np.abs(state)**2):.10f}")
Statevector shape: (16,) Norm: 1.0000000000
# Batch simulation
X = np.array([[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8]])
states = simulate_encoding_statevectors_batch(enc, X)
print(f"Batch: {len(states)} states, each shape {states[0].shape}")
Batch: 2 states, each shape (16,)
# Fidelity between states
f = compute_fidelity(states[0], states[1])
print(f"Fidelity between two inputs: {f:.6f}")
# Self-fidelity should be 1
f_self = compute_fidelity(states[0], states[0])
print(f"Self-fidelity: {f_self:.10f}")
Fidelity between two inputs: 0.732719 Self-fidelity: 1.0000000000
# Partial trace — reduced density matrix for a single qubit
rho_0 = partial_trace_single_qubit(state, n_qubits=4, keep_qubit=0)
print(f"Reduced density matrix for qubit 0:")
print(rho_0)
print(f"Purity: {compute_purity(rho_0):.6f}")
print(" (Purity < 1 means qubit 0 is entangled with others)")
Reduced density matrix for qubit 0: [[0.90583495-4.28422505e-18j 0.19855423+2.01194311e-01j] [0.19855423-2.01194311e-01j 0.09416505-1.08492533e-18j]] Purity: 0.989210 (Purity < 1 means qubit 0 is entangled with others)
# Validate encoding for analysis
validate_encoding_for_analysis(enc)
print("Encoding validated for analysis (no errors)")
Encoding validated for analysis (no errors)
28. Capability Protocols¶
The library uses structural subtyping (PEP 544 Protocols) to check encoding
capabilities at runtime. CyclicEquivariantFeatureMap implements both
ResourceAnalyzable and EntanglementQueryable.
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
EntanglementQueryable,
DataDependentResourceAnalyzable,
DataTransformable,
is_resource_analyzable,
is_entanglement_queryable,
)
enc = CyclicEquivariantFeatureMap(n_features=4)
print("Protocol checks:")
print(f" ResourceAnalyzable: {isinstance(enc, ResourceAnalyzable)}")
print(f" EntanglementQueryable: {isinstance(enc, EntanglementQueryable)}")
print(f" DataDependentResourceAnalyzable: {isinstance(enc, DataDependentResourceAnalyzable)}")
print(f" DataTransformable: {isinstance(enc, DataTransformable)}")
print(f"\nType guard functions:")
print(f" is_resource_analyzable(enc): {is_resource_analyzable(enc)}")
print(f" is_entanglement_queryable(enc): {is_entanglement_queryable(enc)}")
Protocol checks: ResourceAnalyzable: True EntanglementQueryable: True DataDependentResourceAnalyzable: False DataTransformable: False Type guard functions: is_resource_analyzable(enc): True is_entanglement_queryable(enc): True
# Generic function using protocols
def analyze_encoding(enc):
"""Analyze any encoding using capability protocols."""
info = {"name": enc.__class__.__name__}
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
info["total_gates"] = summary["gate_counts"]["total"]
if isinstance(enc, EntanglementQueryable):
pairs = enc.get_entanglement_pairs()
info["n_entanglement_pairs"] = len(pairs)
info["connectivity"] = "ring" if pairs[-1][1] == 0 else "linear"
return info
result = analyze_encoding(enc)
print(f"Generic analysis result: {result}")
Generic analysis result: {'name': 'CyclicEquivariantFeatureMap', 'total_gates': 24, 'n_entanglement_pairs': 4, 'connectivity': 'ring'}
29. Registry System¶
from encoding_atlas import get_encoding, list_encodings
# List all registered encodings
all_encodings = list_encodings()
print(f"Registered encodings ({len(all_encodings)}):")
for name in all_encodings:
print(f" - {name}")
Registered encodings (26): - amplitude - angle - angle_ry - basis - covariant - covariant_feature_map - cyclic_equivariant - cyclic_equivariant_feature_map - data_reuploading - hamiltonian - hamiltonian_encoding - hardware_efficient - higher_order_angle - iqp - pauli_feature_map - qaoa - qaoa_encoding - so2_equivariant - so2_equivariant_feature_map - swap_equivariant - swap_equivariant_feature_map - symmetry_inspired - symmetry_inspired_feature_map - trainable - trainable_encoding - zz_feature_map
# Create CyclicEquivariantFeatureMap via registry
# First check if it's registered
if 'cyclic_equivariant' in all_encodings:
enc_reg = get_encoding('cyclic_equivariant', n_features=4)
print(f"Created via registry: {enc_reg}")
else:
print("CyclicEquivariantFeatureMap is imported directly, not via registry name.")
print("Use: from encoding_atlas import CyclicEquivariantFeatureMap")
Created via registry: CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483)
30. Equality, Hashing & Serialization¶
# --- Equality ---
enc1 = CyclicEquivariantFeatureMap(n_features=4, reps=2)
enc2 = CyclicEquivariantFeatureMap(n_features=4, reps=2)
enc3 = CyclicEquivariantFeatureMap(n_features=4, reps=3)
print(f"Same params: enc1 == enc2 → {enc1 == enc2}")
print(f"Diff params: enc1 == enc3 → {enc1 == enc3}")
print(f"Diff n_feat: enc1 == CyclicEquivariantFeatureMap(3) → {enc1 == CyclicEquivariantFeatureMap(n_features=3)}")
Same params: enc1 == enc2 → True Diff params: enc1 == enc3 → False Diff n_feat: enc1 == CyclicEquivariantFeatureMap(3) → False
# --- Hashing ---
print(f"hash(enc1) = {hash(enc1)}")
print(f"hash(enc2) = {hash(enc2)}")
print(f"Equal objects, same hash: {hash(enc1) == hash(enc2)}")
# Can be used in sets and dicts
encoding_set = {enc1, enc2, enc3}
print(f"\nSet with enc1, enc2 (equal), enc3: {len(encoding_set)} unique encodings")
hash(enc1) = 9189668551494177461 hash(enc2) = 9189668551494177461 Equal objects, same hash: True Set with enc1, enc2 (equal), enc3: 2 unique encodings
import pickle
# --- Pickle serialization ---
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
_ = enc.properties # Ensure properties are cached
# Serialize
data = pickle.dumps(enc)
print(f"Serialized size: {len(data)} bytes")
# Deserialize
enc_restored = pickle.loads(data)
print(f"Restored: {enc_restored}")
print(f"Equal to original: {enc_restored == enc}")
print(f"Properties preserved: {enc_restored.properties == enc.properties}")
# The restored encoding is fully functional
x = np.array([0.1, 0.2, 0.3, 0.4])
circuit = enc_restored.get_circuit(x, backend='pennylane')
print(f"Circuit generation works: {callable(circuit)}")
# Equivariance still holds
result = enc_restored.verify_equivariance(x, g=1)
print(f"Equivariance still holds: {result}")
Serialized size: 593 bytes Restored: CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483) Equal to original: True Properties preserved: True Circuit generation works: True Equivariance still holds: True
31. Thread Safety¶
The encoding is designed for concurrent access. Properties use double-checked locking, and circuit generation is thread-safe.
from concurrent.futures import ThreadPoolExecutor, as_completed
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
# Concurrent circuit generation
X_batch = np.random.default_rng(42).random((20, 4))
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(enc.get_circuit, X_batch[i], 'pennylane'): i
for i in range(20)
}
results = {}
for future in as_completed(futures):
idx = futures[future]
results[idx] = future.result()
print(f"20 concurrent circuit generations: all successful = {len(results) == 20}")
print(f"All callable: {all(callable(c) for c in results.values())}")
20 concurrent circuit generations: all successful = True All callable: True
# Concurrent property access (tests double-checked locking)
enc_fresh = CyclicEquivariantFeatureMap(n_features=4, reps=2)
with ThreadPoolExecutor(max_workers=8) as executor:
futures = [executor.submit(lambda: enc_fresh.properties) for _ in range(50)]
props_list = [f.result() for f in futures]
# All threads should get the same cached object
assert all(p is props_list[0] for p in props_list)
print(f"50 concurrent property accesses: all returned same cached object = True")
50 concurrent property accesses: all returned same cached object = True
32. Logging & Debugging¶
The module supports Python's standard logging framework for debugging.
import logging
# Set up a handler to capture log output
logger = logging.getLogger('encoding_atlas.encodings.equivariant_feature_map')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
# Creating an encoding generates debug logs
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
# Equivariance verification generates debug logs
x = np.array([0.1, 0.2, 0.3, 0.4])
_ = enc.verify_equivariance(x, g=1)
# Clean up
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
print("\n(Logging output shown above)")
DEBUG - Initialized CyclicEquivariantFeatureMap with n_features=4, reps=2 DEBUG - Verifying equivariance for group element 1 DEBUG - Equivariance check: overlap = 1.0000000000, threshold = 0.9999999999, result = True
(Logging output shown above)
33. Encoding Recommendation Guide¶
The library includes a recommendation system that suggests encodings based on problem characteristics.
from encoding_atlas.guide import recommend_encoding
# When would CyclicEquivariantFeatureMap be the best choice?
# It excels for data with cyclic/periodic structure
rec = recommend_encoding(n_features=4, task="classification", priority="accuracy")
print(f"Recommendation for n_features=4, classification, accuracy:")
print(f" Encoding: {rec.encoding_name}")
print(f" Explanation: {rec.explanation}")
print(f" Alternatives: {rec.alternatives}")
print(f" Confidence: {rec.confidence}")
# CyclicEquivariantFeatureMap is best when your data has known cyclic symmetry
# (e.g., periodic signals, ring-structured data, rotational features)
print("\nNote: CyclicEquivariantFeatureMap is ideal when you have:")
print(" - Periodic signals (time series with cyclical patterns)")
print(" - Ring-structured data (molecular rings, circular sensor arrays)")
print(" - Features where cyclic permutation is a meaningful symmetry")
Recommendation for n_features=4, classification, accuracy: Encoding: iqp Explanation: IQP encoding creates highly entangled states with provable classical simulation hardness, well-suited for kernel methods Alternatives: ['data_reuploading', 'zz_feature_map', 'pauli_feature_map'] Confidence: 0.74 Note: CyclicEquivariantFeatureMap is ideal when you have: - Periodic signals (time series with cyclical patterns) - Ring-structured data (molecular rings, circular sensor arrays) - Features where cyclic permutation is a meaningful symmetry
34. Visualization & Comparison¶
from encoding_atlas.visualization import compare_encodings
# Text-based comparison of cyclic encoding with other encodings
compare_encodings(['angle', 'iqp', 'hardware_efficient'], n_features=4)
┌────────────────────────────────────────────────────────────────────────────┐ │ ENCODING COMPARISON (n_features=4) │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ QUBITS CIRCUIT DEPTH │ │ ────── ───────────── │ │ angle ███████████████ 4 angle ██ │ │ iqp ███████████████ 4 iqp █████████████│ │ hardware_efficient ███████████████ 4 hardware_efficient ██████████ │ │ │ │ GATE COUNT TWO-QUBIT GATES │ │ ────────── ─────────────── │ │ angle █ 4 angle │ │ iqp ███████████████ 52 iqp █████████████│ │ hardware_efficient ████ 14 hardware_efficient ███ │ │ │ │ PROPERTIES │ │ ────────── │ │ Encoding Entangling Simulability Trainability │ │ ─────────────────────────────────────────────────────────────────────── │ │ angle ✗ No Simulable ███████ 0.9 │ │ iqp ✓ Yes Not Simulable █████ 0.7 │ │ hardware_efficient ✓ Yes Not Simulable ██████ 0.8 │ │ │ └────────────────────────────────────────────────────────────────────────────┘
'┌────────────────────────────────────────────────────────────────────────────┐\n│ ENCODING COMPARISON (n_features=4) │\n├────────────────────────────────────────────────────────────────────────────┤\n│ │\n│ QUBITS CIRCUIT DEPTH │\n│ ────── ───────────── │\n│ angle ███████████████ 4 angle ██ │\n│ iqp ███████████████ 4 iqp █████████████│\n│ hardware_efficient ███████████████ 4 hardware_efficient ██████████ │\n│ │\n│ GATE COUNT TWO-QUBIT GATES │\n│ ────────── ─────────────── │\n│ angle █ 4 angle │\n│ iqp ███████████████ 52 iqp █████████████│\n│ hardware_efficient ████ 14 hardware_efficient ███ │\n│ │\n│ PROPERTIES │\n│ ────────── │\n│ Encoding Entangling Simulability Trainability │\n│ ─────────────────────────────────────────────────────────────────────── │\n│ angle ✗ No Simulable ███████ 0.9 │\n│ iqp ✓ Yes Not Simulable █████ 0.7 │\n│ hardware_efficient ✓ Yes Not Simulable ██████ 0.8 │\n│ │\n└────────────────────────────────────────────────────────────────────────────┘'
# Compare different CyclicEquivariant configurations
configs = []
for reps in [1, 2, 3]:
enc = CyclicEquivariantFeatureMap(n_features=4, reps=reps)
configs.append({
"name": f"Cyclic(reps={reps})",
"n_qubits": enc.n_qubits,
"depth": enc.depth,
"total_gates": enc.gate_count_breakdown()['total'],
"two_qubit_gates": enc.gate_count_breakdown()['total_two_qubit'],
"is_entangling": enc.properties.is_entangling,
})
print(f"{'Name':<20s} {'Qubits':>7s} {'Depth':>6s} {'Gates':>6s} {'2Q Gates':>9s}")
print("-" * 52)
for c in configs:
print(f"{c['name']:<20s} {c['n_qubits']:>7d} {c['depth']:>6d} {c['total_gates']:>6d} {c['two_qubit_gates']:>9d}")
Name Qubits Depth Gates 2Q Gates ---------------------------------------------------- Cyclic(reps=1) 4 3 12 4 Cyclic(reps=2) 4 6 24 8 Cyclic(reps=3) 4 9 36 12
35. String Representation¶
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=np.pi/4)
print(f"repr: {repr(enc)}")
print(f"str: {str(enc)}")
# Repr contains all configuration parameters
r = repr(enc)
assert "CyclicEquivariantFeatureMap" in r
assert "n_features=4" in r
assert "reps=2" in r
print("\nRepr contains class name and all parameters!")
repr: CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483) str: CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483) Repr contains class name and all parameters!
36. Complete Workflow — Quantum Kernel with Cyclic Equivariance¶
This section demonstrates a complete end-to-end workflow: encoding data, computing a quantum kernel, and verifying equivariance guarantees.
# Step 1: Choose and configure the encoding
enc = CyclicEquivariantFeatureMap(n_features=4, reps=2)
print(f"Encoding: {enc}")
print(f"Properties: {enc.properties.to_dict()}")
Encoding: CyclicEquivariantFeatureMap(n_features=4, reps=2, coupling_strength=0.7853981633974483)
Properties: {'n_qubits': 4, 'depth': 6, 'gate_count': 24, 'single_qubit_gates': 16, 'two_qubit_gates': 8, 'parameter_count': 0, 'is_entangling': True, 'simulability': 'not_simulable', 'expressibility': None, 'entanglement_capability': None, 'trainability_estimate': None, 'noise_resilience_estimate': None, 'notes': ''}
# Step 2: Verify capability protocols
from encoding_atlas.core.protocols import ResourceAnalyzable, EntanglementQueryable
assert isinstance(enc, ResourceAnalyzable), "Must support resource analysis"
assert isinstance(enc, EntanglementQueryable), "Must support entanglement queries"
print("Capability protocols verified!")
# Step 3: Get resource summary
summary = enc.resource_summary()
print(f"\nSymmetry group: {summary['symmetry_group']}")
print(f"Ring connectivity: {summary['entanglement_pairs']}")
print(f"Total gates: {summary['gate_counts']['total']}")
Capability protocols verified! Symmetry group: Z_4 Ring connectivity: [(0, 1), (1, 2), (2, 3), (3, 0)] Total gates: 24
# Step 4: Prepare synthetic data with cyclic structure
# Simulate 4 periodic signals (like sensors around a ring)
rng = np.random.default_rng(42)
n_samples = 30
t = np.linspace(0, 2 * np.pi, n_samples)
# Data with inherent cyclic structure
X = np.column_stack([
np.sin(t),
np.sin(t + np.pi/2), # Phase-shifted
np.sin(t + np.pi), # Phase-shifted
np.sin(t + 3*np.pi/2), # Phase-shifted
]) + rng.normal(0, 0.1, (n_samples, 4))
# Scale to [0, 2pi] for optimal encoding
X_scaled = 2 * np.pi * (X - X.min()) / (X.max() - X.min())
print(f"Data shape: {X_scaled.shape}")
print(f"Data range: [{X_scaled.min():.2f}, {X_scaled.max():.2f}]")
Data shape: (30, 4) Data range: [0.00, 6.28]
# Step 5: Verify equivariance on a sample
x_test = X_scaled[0]
for k in range(4):
result = enc.verify_equivariance(x_test, k)
print(f"Equivariance for shift k={k}: {result}")
print("\nEquivariance verified for all cyclic shifts!")
Equivariance for shift k=0: True Equivariance for shift k=1: True Equivariance for shift k=2: True Equivariance for shift k=3: True Equivariance verified for all cyclic shifts!
# Step 6: Compute quantum kernel
# K(x, x') = |<psi(x)|psi(x')>|^2
# Use a small subset for demonstration
n_subset = 10
X_sub = X_scaled[:n_subset]
# Simulate statevectors
states = simulate_encoding_statevectors_batch(enc, X_sub)
# Compute kernel matrix
kernel = np.zeros((n_subset, n_subset))
for i in range(n_subset):
for j in range(n_subset):
kernel[i, j] = np.abs(np.vdot(states[i], states[j])) ** 2
print(f"Quantum kernel matrix ({n_subset}x{n_subset}):")
print(np.array2string(kernel, precision=3, suppress_small=True))
# Verify kernel properties
assert np.allclose(np.diag(kernel), 1.0), "Diagonal must be 1"
assert np.allclose(kernel, kernel.T), "Kernel must be symmetric"
print("\nKernel is symmetric with unit diagonal!")
Quantum kernel matrix (10x10): [[1. 0.727 0.274 0.145 0.046 0.021 0.047 0.09 0.292 0.22 ] [0.727 1. 0.399 0.15 0.036 0.022 0.054 0.05 0.095 0.11 ] [0.274 0.399 1. 0.668 0.152 0.023 0.015 0.015 0.012 0.01 ] [0.145 0.15 0.668 1. 0.276 0.066 0.053 0.044 0.016 0.03 ] [0.046 0.036 0.152 0.276 1. 0.701 0.271 0.16 0.012 0.032] [0.021 0.022 0.023 0.066 0.701 1. 0.645 0.369 0.054 0.061] [0.047 0.054 0.015 0.053 0.271 0.645 1. 0.783 0.156 0.065] [0.09 0.05 0.015 0.044 0.16 0.369 0.783 1. 0.34 0.137] [0.292 0.095 0.012 0.016 0.012 0.054 0.156 0.34 1. 0.777] [0.22 0.11 0.01 0.03 0.032 0.061 0.065 0.137 0.777 1. ]] Kernel is symmetric with unit diagonal!
# Step 7: Demonstrate equivariance in the kernel
# If encoding is equivariant, then K(sigma*x, sigma*y) = K(x, y)
# (the kernel is invariant under simultaneous cyclic shifts of both inputs)
x_a = X_scaled[0]
x_b = X_scaled[1]
k_original = np.abs(np.vdot(
simulate_encoding_statevector(enc, x_a),
simulate_encoding_statevector(enc, x_b)
)) ** 2
# Apply cyclic shift to both inputs
x_a_shifted = enc.group_action(1, x_a)
x_b_shifted = enc.group_action(1, x_b)
k_shifted = np.abs(np.vdot(
simulate_encoding_statevector(enc, x_a_shifted),
simulate_encoding_statevector(enc, x_b_shifted)
)) ** 2
print(f"K(x_a, x_b): {k_original:.10f}")
print(f"K(σ·x_a, σ·x_b): {k_shifted:.10f}")
print(f"Difference: {abs(k_original - k_shifted):.2e}")
assert np.isclose(k_original, k_shifted, atol=1e-8)
print("\nKernel invariance under simultaneous cyclic shift verified!")
print("This is the practical consequence of encoding equivariance.")
K(x_a, x_b): 0.7265384027 K(σ·x_a, σ·x_b): 0.7265384027 Difference: 0.00e+00 Kernel invariance under simultaneous cyclic shift verified! This is the practical consequence of encoding equivariance.
Summary¶
Core Features¶
- Constructor:
CyclicEquivariantFeatureMap(n_features, reps=2, coupling_strength=π/4) - Properties:
n_features,n_qubits,depth,reps,coupling_strength - Encoding Properties: Lazy, thread-safe
EncodingPropertiesdataclass - Configuration:
configreturns a defensive copy
Multi-Backend Circuit Generation¶
- PennyLane:
get_circuit(x, backend='pennylane')→ callable - Qiskit:
get_circuit(x, backend='qiskit')→QuantumCircuit - Cirq:
get_circuit(x, backend='cirq')→cirq.Circuit - Batch:
get_circuits(X, parallel=True, max_workers=N)
Group Theory & Equivariance¶
- Group Action:
group_action(k, x)— cyclic shift by k positions - Unitary Representation:
unitary_representation(k)— permutation matrix - Group Generators:
group_generators()→[1] - Exact Verification:
verify_equivariance(x, g),verify_equivariance_detailed(x, g) - Generator Verification:
verify_equivariance_on_generators(x) - Statistical Verification:
verify_equivariance_statistical(x, g, n_shots, significance) - Auto Verification:
verify_equivariance_auto(x, g)— selects best method
Resource Analysis¶
- Gate Count Breakdown:
gate_count_breakdown()→CyclicGateCountBreakdown - Resource Summary:
resource_summary()— comprehensive hardware planning info - Entanglement Pairs:
get_entanglement_pairs()— ring topology - Analysis Tools:
count_resources,compare_resources,estimate_execution_time
Analysis Capabilities¶
- Simulability:
check_simulability(enc)— "not_simulable" - Expressibility:
compute_expressibility(enc)— Hilbert space coverage - Entanglement:
compute_entanglement_capability(enc)— Meyer-Wallach measure - Trainability:
estimate_trainability(enc)— barren plateau detection
Software Engineering Features¶
- Capability Protocols:
ResourceAnalyzable,EntanglementQueryable - Registry:
get_encoding,list_encodings - Equality & Hashing:
==,hash()— can be used in sets/dicts - Serialization: Full pickle support with thread lock recreation
- Thread Safety: Double-checked locking for properties, safe concurrent access
- Logging: Standard Python logging at DEBUG level
- Input Validation: Shape, type, value, backend — with clear error messages
- Defensive Copies: Input arrays and config dict are copied
Key Mathematical Properties¶
| Property | Value |
|---|---|
| Symmetry group | Z_n (cyclic group of order n) |
| Equivariance | SWAP_σ|ψ(x)⟩ = |ψ(σ·x)⟩ |
| n_qubits | = n_features |
| Circuit depth | 3 × reps |
| Entanglement topology | Ring (periodic boundary) |
| Gate types | RY, RZZ, RX |
| Simulability | Not classically simulable |
| Is entangling | Yes (RZZ gates) |