Swap Equivariant Feature Map — Comprehensive Usage Guide¶
This notebook provides a complete, hands-on guide to the SwapEquivariantFeatureMap from the Quantum Encoding Atlas library. It covers every feature, method, property, and edge case — everything you need to fully understand and use this encoding in your quantum machine learning projects.
Table of Contents¶
- Installation & Setup
- What is Swap Equivariance?
- Creating the Encoding
- Properties & Configuration
- Circuit Generation (PennyLane / Qiskit / Cirq)
- Batch Circuit Generation
- Understanding the Group Action
- Unitary Representation
- Group Generators
- Equivariance Verification — Exact
- Equivariance Verification — Detailed
- Equivariance Verification — On Generators
- Equivariance Verification — Statistical (Scalable)
- Equivariance Verification — Auto Selection
- Gate Count Breakdown
- Resource Summary
- Entanglement Pairs
- EncodingProperties Object
- Analysis Tools — Simulability
- Analysis Tools — Resource Counting
- Analysis Tools — Expressibility
- Analysis Tools — Entanglement Capability
- Analysis Tools — Trainability
- Capability Protocols (isinstance checks)
- Registry & Discovery
- Equality, Hashing & Collections
- Serialization (Pickle)
- Thread Safety
- Data Preprocessing Recommendations
- Scaling Behavior
- Input Validation & Edge Cases
- Comparing with Other Encodings
- Practical Example — Particle Physics Feature Pairs
- Debugging with Logging
- Summary
# Install encoding-atlas (uncomment if not already installed)
# !pip install encoding-atlas
import numpy as np
import encoding_atlas
print(f"encoding-atlas version: {encoding_atlas.__version__}")
encoding-atlas version: 0.2.0
# Core imports used throughout this notebook
from encoding_atlas import SwapEquivariantFeatureMap
from encoding_atlas import EncodingProperties, BaseEncoding
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
EntanglementQueryable,
)
from encoding_atlas import analysis
from encoding_atlas.analysis import (
check_simulability,
get_simulability_reason,
is_clifford_circuit,
is_matchgate_circuit,
count_resources,
get_resource_summary,
get_gate_breakdown,
compare_resources,
estimate_execution_time,
compute_expressibility,
compute_entanglement_capability,
compute_meyer_wallach,
estimate_trainability,
simulate_encoding_statevector,
compute_fidelity,
compute_purity,
partial_trace_single_qubit,
)
import warnings
warnings.filterwarnings("ignore") # Keep notebook output clean
2. What is Swap Equivariance? ¶
The Core Idea¶
A quantum feature map $|\psi(x)\rangle$ is swap-equivariant if swapping features within a pair in the classical input is exactly equivalent to applying a quantum SWAP gate on the corresponding qubits:
$$ (\text{SWAP}_{01} \otimes \text{SWAP}_{23} \otimes \ldots) |\psi(x)\rangle = |\psi(\text{swap} \cdot x)\rangle $$
where swap exchanges $(x_0, x_1) \to (x_1, x_0)$, $(x_2, x_3) \to (x_3, x_2)$, etc.
Why Does This Matter?¶
| Benefit | Explanation |
|---|---|
| Guaranteed symmetry preservation | The quantum state transforms predictably under pair swaps — mathematically provable |
| Reduced hypothesis space | Learning is constrained to symmetric functions, improving sample efficiency |
| Provable generalization | Theoretical guarantees on learning performance from symmetry constraints |
| Algebraic verification | Can mathematically verify the equivariance property on any input |
When To Use It¶
Use SwapEquivariantFeatureMap when your data has natural pair structure where order within each pair doesn't matter:
- Particle physics: particle-antiparticle features (energy, momentum)
- Bidirectional measurements: forward/backward sensor readings
- Symmetric feature pairs: (latitude, longitude), (real part, imaginary part)
- Graphs/networks: source-destination node features
Symmetry Group¶
The symmetry group is $S_2^n$ — the direct product of $n$ copies of the symmetric group $S_2$ (one per feature pair). For $n$ pairs, there are $2^n$ group elements (every combination of swap/no-swap for each pair).
3. Creating the Encoding ¶
# Basic construction: 4 features (2 pairs), default reps=2
enc = SwapEquivariantFeatureMap(n_features=4)
print(enc)
SwapEquivariantFeatureMap(n_features=4, n_pairs=2, reps=2)
# Specifying the number of repetition layers
enc_1rep = SwapEquivariantFeatureMap(n_features=4, reps=1)
enc_3rep = SwapEquivariantFeatureMap(n_features=4, reps=3)
print(f"reps=1: {enc_1rep}")
print(f"reps=3: {enc_3rep}")
reps=1: SwapEquivariantFeatureMap(n_features=4, n_pairs=2, reps=1) reps=3: SwapEquivariantFeatureMap(n_features=4, n_pairs=2, reps=3)
# Different sizes — n_features must be even
enc_2 = SwapEquivariantFeatureMap(n_features=2) # Minimum: 1 pair
enc_6 = SwapEquivariantFeatureMap(n_features=6) # 3 pairs
enc_8 = SwapEquivariantFeatureMap(n_features=8) # 4 pairs
for e in [enc_2, enc_6, enc_8]:
print(f"n_features={e.n_features}, n_pairs={e.n_pairs}, n_qubits={e.n_qubits}")
n_features=2, n_pairs=1, n_qubits=2 n_features=6, n_pairs=3, n_qubits=6 n_features=8, n_pairs=4, n_qubits=8
# Validation: n_features must be even
try:
bad = SwapEquivariantFeatureMap(n_features=3)
except ValueError as e:
print(f"Caught expected error: {e}")
Caught expected error: n_features must be even, got 3
# Validation: reps must be a positive integer (not bool, not zero, not negative)
for bad_reps in [0, -1, True, False, 2.5, "2"]:
try:
SwapEquivariantFeatureMap(n_features=4, reps=bad_reps)
print(f" reps={bad_reps!r} — unexpectedly accepted")
except (ValueError, TypeError) as e:
print(f" reps={bad_reps!r} — rejected: {e}")
reps=0 — rejected: reps must be a positive integer, got 0 reps=-1 — rejected: reps must be a positive integer, got -1 reps=True — rejected: reps must be a positive integer, got True reps=False — rejected: reps must be a positive integer, got False reps=2.5 — rejected: reps must be a positive integer, got 2.5 reps='2' — rejected: reps must be a positive integer, got '2'
4. Properties & Configuration ¶
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
print("=== Core Attributes ===")
print(f"n_features : {enc.n_features}") # Number of classical features
print(f"n_qubits : {enc.n_qubits}") # Qubits = n_features (1 qubit per feature)
print(f"n_pairs : {enc.n_pairs}") # n_features // 2
print(f"reps : {enc.reps}") # Repetition layers
print(f"depth : {enc.depth}") # 3 * reps (RY + H + CZ per layer)
print()
print("=== Configuration ===")
print(f"config : {enc.config}") # Returns a copy of internal config dict
=== Core Attributes ===
n_features : 4
n_qubits : 4
n_pairs : 2
reps : 2
depth : 6
=== Configuration ===
config : {'reps': 2}
# The config property returns a copy — modifying it doesn't affect the encoding
config = enc.config
config['reps'] = 999
print(f"Modified copy: {config}")
print(f"Original config: {enc.config}") # Unchanged
Modified copy: {'reps': 999}
Original config: {'reps': 2}
5. Circuit Generation ¶
The encoding supports three quantum backends: PennyLane, Qiskit, and Cirq.
Circuit Structure (per repetition layer)¶
Each layer consists of three sub-layers:
- RY gates — Direct feature encoding:
RY(x[i])on qubit $i$ - Hadamard gates — Basis mixing:
Hon every qubit - CZ gates — Symmetric pair entanglement:
CZbetween qubits $(2k, 2k+1)$ for each pair $k$
Why these specific gates?
- RY for encoding: Direct correspondence between feature positions and qubit positions means swapping features ↔ swapping qubits
- H⊗H for mixing: $H \otimes H$ commutes with SWAP (tensor product of identical operators)
- CZ (not CNOT) for entanglement: CZ is symmetric under qubit exchange:
SWAP · CZ(q0,q1) · SWAP = CZ(q0,q1). CNOT would break equivariance becauseCNOT(q0,q1) ≠ CNOT(q1,q0)
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
5.1 PennyLane Backend¶
import pennylane as qml
# get_circuit returns a callable (a PennyLane quantum function)
circuit_fn = enc.get_circuit(x, backend="pennylane")
print(f"Type: {type(circuit_fn)}")
# Use it inside a QNode to execute
dev = qml.device("default.qubit", wires=enc.n_qubits)
@qml.qnode(dev)
def run_circuit():
circuit_fn()
return qml.state()
state = run_circuit()
print(f"State vector (first 8 of {len(state)} amplitudes):")
for i in range(min(8, len(state))):
print(f" |{i:04b}⟩ : {state[i]:.6f}")
Type: <class 'function'> State vector (first 8 of 16 amplitudes): |0000⟩ : 0.501832+0.000000j |0001⟩ : 0.207422+0.000000j |0010⟩ : 0.186465+0.000000j |0011⟩ : 0.281283+0.000000j |0100⟩ : 0.346241+0.000000j |0101⟩ : 0.143112+0.000000j |0110⟩ : 0.128653+0.000000j |0111⟩ : 0.194072+0.000000j
# Visualize the circuit using PennyLane's drawer
@qml.qnode(dev)
def draw_circuit():
circuit_fn()
return qml.state()
print(qml.draw(draw_circuit, max_length=120)())
0: ──RY(0.10)──H─╭●──RY(0.10)──H─╭●─┤ State 1: ──RY(0.20)──H─╰Z──RY(0.20)──H─╰Z─┤ State 2: ──RY(0.30)──H─╭●──RY(0.30)──H─╭●─┤ State 3: ──RY(0.40)──H─╰Z──RY(0.40)──H─╰Z─┤ State
5.2 Qiskit Backend¶
# get_circuit returns a Qiskit QuantumCircuit
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()
print(qiskit_circuit.draw(output='text'))
Type: QuantumCircuit
Qubits: 4
Depth: 6
┌─────────┐┌───┐ ┌─────────┐┌───┐
q_0: ┤ Ry(0.1) ├┤ H ├─■─┤ Ry(0.1) ├┤ H ├─■─
├─────────┤├───┤ │ ├─────────┤├───┤ │
q_1: ┤ Ry(0.2) ├┤ H ├─■─┤ Ry(0.2) ├┤ H ├─■─
├─────────┤├───┤ ├─────────┤├───┤
q_2: ┤ Ry(0.3) ├┤ H ├─■─┤ Ry(0.3) ├┤ H ├─■─
├─────────┤├───┤ │ ├─────────┤├───┤ │
q_3: ┤ Ry(0.4) ├┤ H ├─■─┤ Ry(0.4) ├┤ H ├─■─
└─────────┘└───┘ └─────────┘└───┘
5.3 Cirq Backend¶
import cirq
# get_circuit returns a Cirq Circuit
cirq_circuit = enc.get_circuit(x, backend="cirq")
print(f"Type: {type(cirq_circuit).__name__}")
print(cirq_circuit)
Type: Circuit
0: ───Ry(0.032π)───H───@───Ry(0.032π)───H───@───
│ │
1: ───Ry(0.064π)───H───@───Ry(0.064π)───H───@───
2: ───Ry(0.095π)───H───@───Ry(0.095π)───H───@───
│ │
3: ───Ry(0.127π)───H───@───Ry(0.127π)───H───@───
# Invalid backend raises ValueError
try:
enc.get_circuit(x, backend="invalid")
except ValueError as e:
print(f"Caught expected error: {e}")
Caught expected error: Unknown backend: invalid
5.4 Cross-Backend Consistency¶
All three backends produce the same quantum state for the same input.
# Get the state vector using the library's simulation utility
state_pl = simulate_encoding_statevector(enc, x, backend="pennylane")
print(f"State vector shape: {state_pl.shape}")
print(f"Normalization: {np.abs(state_pl) @ np.abs(state_pl):.10f}")
print(f"||state||² = {np.vdot(state_pl, state_pl).real:.10f}")
State vector shape: (16,) Normalization: 1.0000000000 ||state||² = 1.0000000000
6. Batch Circuit Generation ¶
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Generate a batch of random input data
rng = np.random.default_rng(42)
X_batch = rng.uniform(0, np.pi, size=(5, 4))
print(f"Batch shape: {X_batch.shape}")
# Sequential batch generation
circuits = enc.get_circuits(X_batch, backend="pennylane")
print(f"Generated {len(circuits)} circuits (sequential)")
# Parallel batch generation (useful for large batches)
circuits_par = enc.get_circuits(X_batch, backend="pennylane", parallel=True)
print(f"Generated {len(circuits_par)} circuits (parallel)")
# Control parallelism with max_workers
circuits_par2 = enc.get_circuits(X_batch, backend="pennylane", parallel=True, max_workers=2)
print(f"Generated {len(circuits_par2)} circuits (parallel, 2 workers)")
Batch shape: (5, 4) Generated 5 circuits (sequential) Generated 5 circuits (parallel) Generated 5 circuits (parallel, 2 workers)
# get_circuit() rejects batch input — use get_circuits() for batches
try:
enc.get_circuit(X_batch, backend="pennylane")
except ValueError as e:
print(f"Caught expected error: {e}")
Caught expected error: get_circuit requires a single sample
# Single sample passed to get_circuits() also works (1D or 2D)
x_single = np.array([0.1, 0.2, 0.3, 0.4])
circuits_single = enc.get_circuits(x_single, backend="pennylane")
print(f"Single sample → {len(circuits_single)} circuit(s)")
Single sample → 1 circuit(s)
7. Understanding the Group Action ¶
The group action defines how the symmetry group acts on classical input data. For swap equivariance, a group element is a list[bool] — one boolean per pair, indicating whether to swap that pair.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([1.0, 2.0, 3.0, 4.0])
print(f"Original features: {x}")
print(f" Pair 0: ({x[0]}, {x[1]})")
print(f" Pair 1: ({x[2]}, {x[3]})")
print()
Original features: [1. 2. 3. 4.] Pair 0: (1.0, 2.0) Pair 1: (3.0, 4.0)
# Swap first pair only
swaps_first = [True, False]
result = enc.group_action(swaps_first, x)
print(f"Swap first pair: {x} → {result}")
# Swap second pair only
swaps_second = [False, True]
result = enc.group_action(swaps_second, x)
print(f"Swap second pair: {x} → {result}")
# Swap both pairs
swaps_both = [True, True]
result = enc.group_action(swaps_both, x)
print(f"Swap both pairs: {x} → {result}")
# Identity (no swaps)
swaps_none = [False, False]
result = enc.group_action(swaps_none, x)
print(f"No swaps: {x} → {result}")
Swap first pair: [1. 2. 3. 4.] → [2. 1. 3. 4.] Swap second pair: [1. 2. 3. 4.] → [1. 2. 4. 3.] Swap both pairs: [1. 2. 3. 4.] → [2. 1. 4. 3.] No swaps: [1. 2. 3. 4.] → [1. 2. 3. 4.]
# Key property: applying the same swap twice returns to original (involution)
x_swapped = enc.group_action([True, True], x)
x_double = enc.group_action([True, True], x_swapped)
print(f"Original: {x}")
print(f"Swapped once: {x_swapped}")
print(f"Swapped twice: {x_double}")
print(f"Matches original: {np.allclose(x, x_double)}")
Original: [1. 2. 3. 4.] Swapped once: [2. 1. 4. 3.] Swapped twice: [1. 2. 3. 4.] Matches original: True
# Enumerate all group elements for 2 pairs (2² = 4 elements)
print("All group elements for 2 pairs:")
for swap_0 in [False, True]:
for swap_1 in [False, True]:
g = [swap_0, swap_1]
result = enc.group_action(g, x)
print(f" {g} → {result}")
All group elements for 2 pairs: [False, False] → [1. 2. 3. 4.] [False, True] → [1. 2. 4. 3.] [True, False] → [2. 1. 3. 4.] [True, True] → [2. 1. 4. 3.]
8. Unitary Representation ¶
The unitary representation maps each group element to a unitary matrix $U(g)$ that acts on the quantum Hilbert space. For swap equivariance, $U(g)$ is a tensor product of SWAP and Identity gates.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Get the unitary for swapping the first pair
U_swap_first = enc.unitary_representation([True, False])
print(f"U([True, False]) shape: {U_swap_first.shape}")
print(f"Is unitary: {np.allclose(U_swap_first @ U_swap_first.conj().T, np.eye(16))}")
U([True, False]) shape: (16, 16) Is unitary: True
# Identity element → identity matrix
U_identity = enc.unitary_representation([False, False])
print(f"U([False, False]) = Identity: {np.allclose(U_identity, np.eye(16))}")
U([False, False]) = Identity: True
# Involution property: U(g)² = I for all swaps (since each swap is its own inverse)
U_both = enc.unitary_representation([True, True])
print(f"U([True, True])² = I: {np.allclose(U_both @ U_both, np.eye(16))}")
U([True, True])² = I: True
# Visualize the SWAP gate (2-qubit) for a single pair (n_features=2)
enc_small = SwapEquivariantFeatureMap(n_features=2)
SWAP_matrix = enc_small.unitary_representation([True])
print("SWAP gate matrix (4x4):")
print(np.real(SWAP_matrix).astype(int))
print()
print("Action: |00⟩→|00⟩, |01⟩→|10⟩, |10⟩→|01⟩, |11⟩→|11⟩")
SWAP gate matrix (4x4): [[1 0 0 0] [0 0 1 0] [0 1 0 0] [0 0 0 1]] Action: |00⟩→|00⟩, |01⟩→|10⟩, |10⟩→|01⟩, |11⟩→|11⟩
9. Group Generators ¶
Generators are a minimal set of group elements that can produce all other elements through composition. For $S_2^n$, each pair swap is an independent generator.
enc = SwapEquivariantFeatureMap(n_features=6, reps=2) # 3 pairs
generators = enc.group_generators()
print(f"Number of generators: {len(generators)}")
print(f"Number of pairs: {enc.n_pairs}")
print()
for i, gen in enumerate(generators):
print(f" Generator {i}: {gen} (swaps pair {i} only)")
Number of generators: 3 Number of pairs: 3 Generator 0: [True, False, False] (swaps pair 0 only) Generator 1: [False, True, False] (swaps pair 1 only) Generator 2: [False, False, True] (swaps pair 2 only)
# For S_2^n, every element can be expressed as a product of generators.
# Since pair swaps are independent and self-inverse, composition is just OR.
# For example, [True, True, False] = generator[0] ∘ generator[1]
# But more importantly, verifying equivariance on generators alone
# is sufficient to guarantee equivariance for ALL group elements!
print(f"Total group elements: {2**enc.n_pairs}")
print(f"Generators to test: {len(generators)}")
print(f"Savings: {2**enc.n_pairs - len(generators)} fewer verifications")
Total group elements: 8 Generators to test: 3 Savings: 5 fewer verifications
10. Equivariance Verification — Exact ¶
Exact verification checks:
$$U(g)|\psi(x)\rangle = |\psi(g \cdot x)\rangle$$
by computing full state vectors and comparing them (up to global phase).
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.1, 0.2, 0.3, 0.4])
# Verify for each possible group element
group_elements = [
[False, False], # Identity
[True, False], # Swap pair 0
[False, True], # Swap pair 1
[True, True], # Swap both pairs
]
print("Equivariance verification (exact):")
for g in group_elements:
result = enc.verify_equivariance(x, g)
print(f" g={g} → equivariant: {result}")
Equivariance verification (exact): g=[False, False] → equivariant: True g=[True, False] → equivariant: True g=[False, True] → equivariant: True g=[True, True] → equivariant: True
# Verify with different input values
test_inputs = [
np.array([0.0, 0.0, 0.0, 0.0]), # All zeros
np.array([np.pi, 0.0, 0.0, np.pi]), # Extreme angles
np.array([0.5, 1.5, 2.5, 3.5]), # Larger values
np.array([-1.0, 1.0, -0.5, 0.5]), # Negative values
]
swaps = [True, True] # Swap both pairs
print(f"Testing swap={swaps} across different inputs:")
for x_test in test_inputs:
result = enc.verify_equivariance(x_test, swaps)
print(f" x={x_test} → equivariant: {result}")
Testing swap=[True, True] across different inputs: x=[0. 0. 0. 0.] → equivariant: True x=[3.14159265 0. 0. 3.14159265] → equivariant: True x=[0.5 1.5 2.5 3.5] → equivariant: True x=[-1. 1. -0.5 0.5] → equivariant: True
# The tolerance can be adjusted (default: 1e-10)
result_tight = enc.verify_equivariance(x, [True, False], atol=1e-12)
result_loose = enc.verify_equivariance(x, [True, False], atol=1e-6)
print(f"Tight tolerance (1e-12): {result_tight}")
print(f"Loose tolerance (1e-6): {result_loose}")
Tight tolerance (1e-12): True Loose tolerance (1e-6): True
11. Equivariance Verification — Detailed ¶
Returns a dictionary with detailed metrics instead of just a boolean.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.5, 1.0, 1.5, 2.0])
result = enc.verify_equivariance_detailed(x, [True, False])
print("Detailed verification result:")
for key, value in result.items():
if isinstance(value, float):
print(f" {key}: {value:.12f}")
else:
print(f" {key}: {value}")
Detailed verification result: equivariant: True overlap: 1.000000000000 tolerance: 0.000000000100 group_element: [True, False]
# The overlap |⟨U(g)ψ(x)|ψ(g·x)⟩| should be ~1.0 for equivariant encodings
# An overlap of 1.0 means the states are identical (up to global phase)
print(f"Overlap value: {result['overlap']:.15f}")
print(f"Distance from 1.0: {abs(1.0 - result['overlap']):.2e}")
Overlap value: 0.999999999999999 Distance from 1.0: 1.44e-15
12. Equivariance Verification — On Generators ¶
Testing equivariance on generators is sufficient to guarantee equivariance for the entire group. This is more efficient than testing all $2^n$ group elements.
enc = SwapEquivariantFeatureMap(n_features=6, reps=2) # 3 pairs → 8 group elements
x = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6])
# Instead of testing all 2³=8 elements, test only 3 generators
result = enc.verify_equivariance_on_generators(x)
print(f"Equivariant on all generators: {result}")
print(f" → Guarantees equivariance for all {2**enc.n_pairs} group elements")
Equivariant on all generators: True → Guarantees equivariance for all 8 group elements
13. Equivariance Verification — Statistical (Scalable) ¶
For large systems where exact state vector computation becomes infeasible ($O(2^n)$ memory), statistical verification uses measurement sampling. This scales polynomially and works on real quantum hardware.
It compares probability distributions from two circuits:
- Prepare $|\psi(x)\rangle$, apply $U(g)$, measure
- Prepare $|\psi(g \cdot x)\rangle$, measure
If equivariance holds, both distributions should be identical. A chi-squared test is used to compare them.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
x = np.array([0.5, 1.0, 1.5, 2.0])
# Statistical verification with default parameters
stat_result = enc.verify_equivariance_statistical(x, [True, False])
print("Statistical verification result:")
for key, value in stat_result.items():
if isinstance(value, float):
print(f" {key}: {value:.6f}")
else:
print(f" {key}: {value}")
Statistical verification result: equivariant: True p_value: 0.118487 test_statistic: 5.862662 significance: 0.010000 n_shots: 10000 group_element: [True, False] method: chi_squared confidence_level: 0.990000
# Tuning statistical verification parameters
# Quick check (faster, less confident)
result_quick = enc.verify_equivariance_statistical(
x, [True, True], n_shots=1000, significance=0.05
)
print(f"Quick check: equivariant={result_quick['equivariant']}, "
f"p_value={result_quick['p_value']:.4f}, "
f"confidence={result_quick['confidence_level']:.2f}")
# High confidence (slower, more confident)
result_precise = enc.verify_equivariance_statistical(
x, [True, True], n_shots=50000, significance=0.001
)
print(f"High confidence: equivariant={result_precise['equivariant']}, "
f"p_value={result_precise['p_value']:.4f}, "
f"confidence={result_precise['confidence_level']:.3f}")
Quick check: equivariant=True, p_value=0.4193, confidence=0.95 High confidence: equivariant=True, p_value=0.9454, confidence=0.999
# n_shots < 100 is rejected as insufficient for statistics
try:
enc.verify_equivariance_statistical(x, [True, False], n_shots=50)
except ValueError as e:
print(f"Caught expected error: {e}")
Caught expected error: n_shots must be at least 100 for meaningful statistics, got 50
14. Equivariance Verification — Auto Selection ¶
Automatically chooses between exact and statistical verification based on system size:
- $n_{\text{qubits}} \le 12$: exact verification (more precise)
- $n_{\text{qubits}} > 12$: statistical verification (scalable)
# Small system → exact verification is used
enc_small = SwapEquivariantFeatureMap(n_features=4, reps=2)
x_small = np.array([0.1, 0.2, 0.3, 0.4])
result = enc_small.verify_equivariance_auto(x_small, [True, False])
print(f"Small system (n_qubits={enc_small.n_qubits}): equivariant={result}")
print(f" → Used exact verification (n_qubits ≤ 12)")
Small system (n_qubits=4): equivariant=True → Used exact verification (n_qubits ≤ 12)
15. Gate Count Breakdown ¶
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
breakdown = enc.gate_count_breakdown()
print("Gate Count Breakdown:")
print(f" RY gates: {breakdown['ry']}")
print(f" Hadamard gates: {breakdown['hadamard']}")
print(f" CZ gates: {breakdown['cz']}")
print(f" Total single-qubit: {breakdown['total_single_qubit']}")
print(f" Total two-qubit: {breakdown['total_two_qubit']}")
print(f" Total gates: {breakdown['total']}")
Gate Count Breakdown: RY gates: 8 Hadamard gates: 8 CZ gates: 4 Total single-qubit: 16 Total two-qubit: 4 Total gates: 20
# Gate count formulas:
# RY = n_qubits × reps
# Hadamard = n_qubits × reps
# CZ = n_pairs × reps
# Total = (2 × n_qubits + n_pairs) × reps
n, r, p = enc.n_qubits, enc.reps, enc.n_pairs
print("Verify formulas:")
print(f" RY = n×r = {n}×{r} = {n*r} ✓" if n*r == breakdown['ry'] else " RY ✗")
print(f" H = n×r = {n}×{r} = {n*r} ✓" if n*r == breakdown['hadamard'] else " H ✗")
print(f" CZ = p×r = {p}×{r} = {p*r} ✓" if p*r == breakdown['cz'] else " CZ ✗")
print(f" Total = (2n+p)×r = ({2*n}+{p})×{r} = {(2*n+p)*r} ✓"
if (2*n+p)*r == breakdown['total'] else " Total ✗")
Verify formulas: RY = n×r = 4×2 = 8 ✓ H = n×r = 4×2 = 8 ✓ CZ = p×r = 2×2 = 4 ✓ Total = (2n+p)×r = (8+2)×2 = 20 ✓
# Compare gate counts across different configurations
print(f"{'n_features':>10} {'reps':>6} {'RY':>6} {'H':>6} {'CZ':>6} {'Total':>8}")
print("-" * 50)
for nf in [2, 4, 6, 8]:
for r in [1, 2, 3]:
e = SwapEquivariantFeatureMap(n_features=nf, reps=r)
b = e.gate_count_breakdown()
print(f"{nf:>10} {r:>6} {b['ry']:>6} {b['hadamard']:>6} {b['cz']:>6} {b['total']:>8}")
n_features reps RY H CZ Total
--------------------------------------------------
2 1 2 2 1 5
2 2 4 4 2 10
2 3 6 6 3 15
4 1 4 4 2 10
4 2 8 8 4 20
4 3 12 12 6 30
6 1 6 6 3 15
6 2 12 12 6 30
6 3 18 18 9 45
8 1 8 8 4 20
8 2 16 16 8 40
8 3 24 24 12 60
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
summary = enc.resource_summary()
# Pretty-print the full summary
import json
def format_summary(d, indent=0):
"""Pretty print nested dictionary."""
for k, v in d.items():
if isinstance(v, dict):
print(f"{' ' * indent}{k}:")
format_summary(v, indent + 1)
elif isinstance(v, list) and len(v) > 0 and isinstance(v[0], (tuple, list)):
print(f"{' ' * indent}{k}: {v}")
else:
print(f"{' ' * indent}{k}: {v}")
format_summary(summary)
n_qubits: 4
n_features: 4
depth: 6
reps: 2
n_pairs: 2
gate_counts:
ry: 8
hadamard: 8
cz: 4
total_single_qubit: 16
total_two_qubit: 4
total: 20
is_entangling: True
simulability: not_simulable
trainability_estimate: None
symmetry_group: S_2^2
n_pair_swaps: 2
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
hardware_requirements:
connectivity: pairs
native_gates: ['RY', 'H', 'CZ']
n_entanglement_pairs: 2
entanglement_pairs: [(0, 1), (2, 3)]
verification_methods: ['verify_equivariance (exact)', 'verify_equivariance_statistical (scalable)', 'verify_equivariance_auto (automatic selection)']
# Key fields explained
print("=== Symmetry Information ===")
print(f"Symmetry group: {summary['symmetry_group']}")
print(f" → Direct product of {summary['n_pair_swaps']} copies of S₂")
print(f" → {2**summary['n_pair_swaps']} group elements total")
print()
print("=== Hardware Requirements ===")
hw = summary['hardware_requirements']
print(f"Connectivity: {hw['connectivity']}")
print(f"Native gates: {hw['native_gates']}")
print()
print("=== Verification Methods ===")
for method in summary['verification_methods']:
print(f" • {method}")
print()
print("=== Verification Cost ===")
vc = summary['verification_cost']
print(f"Exact: memory={vc['exact']['memory']}, time={vc['exact']['time']}")
print(f"Statistical: memory={vc['statistical']['memory']}, scalable={vc['statistical']['scalable']}")
=== Symmetry Information === Symmetry group: S_2^2 → Direct product of 2 copies of S₂ → 4 group elements total === Hardware Requirements === Connectivity: pairs Native gates: ['RY', 'H', 'CZ'] === 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) Statistical: memory=O(n_shots), scalable=True
17. Entanglement Pairs ¶
# Entanglement pairs are the qubit indices connected by CZ gates
for nf in [2, 4, 6, 8]:
enc = SwapEquivariantFeatureMap(n_features=nf)
pairs = enc.get_entanglement_pairs()
print(f"n_features={nf}: entanglement pairs = {pairs}")
print()
print("Notice: CZ gates connect qubits within each feature pair")
print(" Pair 0 → CZ(0,1), Pair 1 → CZ(2,3), Pair 2 → CZ(4,5), ...")
n_features=2: entanglement pairs = [(0, 1)] n_features=4: entanglement pairs = [(0, 1), (2, 3)] n_features=6: entanglement pairs = [(0, 1), (2, 3), (4, 5)] n_features=8: entanglement pairs = [(0, 1), (2, 3), (4, 5), (6, 7)] Notice: CZ gates connect qubits within each feature pair Pair 0 → CZ(0,1), Pair 1 → CZ(2,3), Pair 2 → CZ(4,5), ...
# CZ is symmetric: CZ(q0,q1) = CZ(q1,q0), which is WHY equivariance works
# The returned pairs are a copy — modifying them doesn't affect the encoding
enc = SwapEquivariantFeatureMap(n_features=4)
pairs = enc.get_entanglement_pairs()
pairs.append((99, 100)) # Modify the returned copy
print(f"Modified copy: {pairs}")
print(f"Original pairs: {enc.get_entanglement_pairs()}") # Unaffected
Modified copy: [(0, 1), (2, 3), (99, 100)] Original pairs: [(0, 1), (2, 3)]
18. EncodingProperties Object ¶
The properties attribute returns a frozen (immutable) dataclass with computed encoding properties.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
props = enc.properties
print(f"Type: {type(props).__name__}")
print()
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}") # 0 — no trainable params
print(f"is_entangling: {props.is_entangling}")
print(f"simulability: {props.simulability}")
print(f"expressibility: {props.expressibility}") # None until computed
print(f"trainability_estimate: {props.trainability_estimate}")
Type: EncodingProperties n_qubits: 4 depth: 6 gate_count: 20 single_qubit_gates: 16 two_qubit_gates: 4 parameter_count: 0 is_entangling: True simulability: not_simulable expressibility: None trainability_estimate: None
# Properties can be converted to a dictionary
props_dict = props.to_dict()
print("Properties as dict:")
for k, v in props_dict.items():
print(f" {k}: {v}")
Properties as dict: n_qubits: 4 depth: 6 gate_count: 20 single_qubit_gates: 16 two_qubit_gates: 4 parameter_count: 0 is_entangling: True simulability: not_simulable expressibility: None entanglement_capability: None trainability_estimate: None noise_resilience_estimate: None notes:
# Properties are frozen (immutable)
try:
props.depth = 999
except AttributeError as e:
print(f"Cannot modify frozen dataclass: {e}")
Cannot modify frozen dataclass: cannot assign to field 'depth'
# Properties are lazily computed (thread-safe) and cached
# Accessing .properties multiple times returns the same object
props1 = enc.properties
props2 = enc.properties
print(f"Same object (cached): {props1 is props2}")
Same object (cached): True
19. Analysis Tools — Simulability ¶
Checks whether the encoding can be efficiently simulated on a classical computer.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Full simulability analysis
sim_result = check_simulability(enc)
print("Simulability Analysis:")
print(f" Is simulable: {sim_result['is_simulable']}")
print(f" Simulability class: {sim_result['simulability_class']}")
print(f" Reason: {sim_result['reason']}")
print("\nDetails:")
for k, v in sim_result['details'].items():
print(f" {k}: {v}")
Simulability Analysis: Is simulable: False Simulability class: not_simulable Reason: Entangling circuit with 4 two-qubit gates and non-Clifford operations Details: is_entangling: True is_clifford: False is_matchgate: False entanglement_pattern: partial two_qubit_gate_count: 4 n_qubits: 4 n_features: 4 declared_simulability: not_simulable encoding_name: SwapEquivariantFeatureMap has_non_clifford_gates: False has_t_gates: False has_parameterized_rotations: False
# Quick simulability reason
reason = get_simulability_reason(enc)
print(f"Quick reason: {reason}")
Quick reason: Not simulable: Entangling circuit with 4 two-qubit gates and non-Clifford operations
# Check specific simulability conditions
print(f"Is Clifford circuit: {is_clifford_circuit(enc)}")
print(f"Is matchgate circuit: {is_matchgate_circuit(enc)}")
# SwapEquivariant uses RY (non-Clifford) + CZ → NOT simulable
# This is desirable for quantum advantage!
Is Clifford circuit: False Is matchgate circuit: False
20. Analysis Tools — Resource Counting ¶
The analysis module provides additional resource analysis beyond the encoding's own methods.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Quick resource summary from cached properties
res_summary = get_resource_summary(enc)
print("Resource Summary:")
for k, v in res_summary.items():
print(f" {k}: {v}")
Resource Summary: n_qubits: 4 depth: 6 gate_count: 20 single_qubit_gates: 16 two_qubit_gates: 4 parameter_count: 0 cnot_count: 4 cz_count: 0 t_gate_count: 0 hadamard_count: 0 rotation_gates: 16 two_qubit_ratio: 0.2 gates_per_qubit: 5.0 encoding_name: SwapEquivariantFeatureMap is_data_dependent: False
# Detailed gate breakdown
detailed = get_gate_breakdown(enc)
print("Detailed Gate Breakdown:")
for k, v in detailed.items():
print(f" {k}: {v}")
Detailed Gate Breakdown: rx: 0 ry: 8 rz: 0 h: 8 x: 0 y: 0 z: 0 s: 0 t: 0 cnot: 0 cx: 0 cz: 4 swap: 0 total_single_qubit: 16 total_two_qubit: 4 total: 20 encoding_name: SwapEquivariantFeatureMap
# Estimate execution time on typical quantum hardware
exec_time = estimate_execution_time(enc)
print("Estimated Execution Time:")
for k, v in exec_time.items():
if isinstance(v, float):
print(f" {k}: {v:.4f} μs")
else:
print(f" {k}: {v}")
Estimated Execution Time: serial_time_us: 2.1200 μs estimated_time_us: 2.2000 μs single_qubit_time_us: 0.3200 μs two_qubit_time_us: 0.8000 μs measurement_time_us: 1.0000 μs parallelization_factor: 0.5000 μs
# Custom hardware timing parameters
exec_time_custom = estimate_execution_time(
enc,
single_qubit_gate_time_us=0.05, # 50 ns single-qubit
two_qubit_gate_time_us=0.5, # 500 ns two-qubit
measurement_time_us=2.0, # 2 μs measurement
)
print(f"Custom hardware estimated time: {exec_time_custom['estimated_time_us']:.4f} μs")
Custom hardware estimated time: 5.0000 μs
21. Analysis Tools — Expressibility ¶
Expressibility measures how well the encoding can explore the Hilbert space, compared to Haar-random states. Higher expressibility → better coverage → more expressive encoding.
Note: This computation requires sampling many random inputs and computing fidelities. It may take a few seconds.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Compute expressibility (scalar value between 0 and 1)
expr_score = compute_expressibility(enc, n_samples=500, seed=42)
print(f"Expressibility score: {expr_score:.4f}")
print(f" 0.0 = poor coverage, 1.0 = approaches Haar-random")
Expressibility score: 0.8304 0.0 = poor coverage, 1.0 = approaches Haar-random
# Get detailed expressibility results including distributions
expr_detailed = compute_expressibility(
enc, n_samples=500, seed=42, return_distributions=True
)
print("Detailed Expressibility:")
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" n_samples: {expr_detailed['n_samples']}")
print(f" n_bins: {expr_detailed['n_bins']}")
Detailed Expressibility: Expressibility: 0.8304 KL divergence: 1.6960 Mean fidelity: 0.1667 Std fidelity: 0.2217 n_samples: 500 n_bins: 75
22. Analysis Tools — Entanglement Capability ¶
Measures how much entanglement the encoding produces on average across random inputs.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Compute entanglement capability (Meyer-Wallach measure)
ent_score = compute_entanglement_capability(enc, n_samples=200, seed=42)
print(f"Entanglement capability (Meyer-Wallach): {ent_score:.4f}")
print(f" 0.0 = separable (no entanglement), 1.0 = maximally entangled")
Entanglement capability (Meyer-Wallach): 0.1372 0.0 = separable (no entanglement), 1.0 = maximally entangled
# Detailed results with per-qubit breakdown
ent_detailed = compute_entanglement_capability(
enc, n_samples=200, seed=42, return_details=True
)
print("Detailed Entanglement:")
print(f" Capability: {ent_detailed['entanglement_capability']:.4f}")
print(f" Std error: {ent_detailed['std_error']:.4f}")
print(f" Measure: {ent_detailed['measure']}")
print(f" n_samples: {ent_detailed['n_samples']}")
if ent_detailed.get('per_qubit_entanglement') is not None:
print(f" Per-qubit entanglement:")
for i, val in enumerate(ent_detailed['per_qubit_entanglement']):
print(f" Qubit {i}: {val:.4f}")
Detailed Entanglement:
Capability: 0.1372
Std error: 0.0090
Measure: meyer_wallach
n_samples: 200
Per-qubit entanglement:
Qubit 0: 0.0727
Qubit 1: 0.0727
Qubit 2: 0.0646
Qubit 3: 0.0646
# Compute Meyer-Wallach for a specific input state
x = np.array([0.5, 1.0, 1.5, 2.0])
state = simulate_encoding_statevector(enc, x)
mw = compute_meyer_wallach(state, enc.n_qubits)
print(f"Meyer-Wallach for x={x}: {mw:.4f}")
Meyer-Wallach for x=[0.5 1. 1.5 2. ]: 0.0951
# Compute purity of each qubit's reduced density matrix
# Purity < 1 indicates entanglement with the rest of the system
print(f"Per-qubit purity (lower = more entangled with rest):")
for q in range(enc.n_qubits):
rho = partial_trace_single_qubit(state, enc.n_qubits, q)
purity = compute_purity(rho)
print(f" Qubit {q}: purity = {purity:.4f}")
Per-qubit purity (lower = more entangled with rest): Qubit 0: purity = 0.9054 Qubit 1: purity = 0.9054 Qubit 2: purity = 0.9996 Qubit 3: purity = 0.9996
23. Analysis Tools — Trainability ¶
Estimates the risk of barren plateaus (vanishing gradients) for the encoding.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Estimate trainability (scalar: 0-1, higher = more trainable)
train_score = estimate_trainability(enc, n_samples=100, seed=42)
print(f"Trainability estimate: {train_score:.4f}")
print(f" 1.0 = easily trainable, 0.0 = severe barren plateau")
Trainability estimate: 0.1925 1.0 = easily trainable, 0.0 = severe barren plateau
# Detailed trainability results
train_detailed = estimate_trainability(
enc, n_samples=100, seed=42, return_details=True
)
print("Detailed Trainability:")
print(f" Trainability estimate: {train_detailed['trainability_estimate']:.4f}")
print(f" Gradient variance: {train_detailed['gradient_variance']:.6e}")
print(f" Barren plateau risk: {train_detailed['barren_plateau_risk']}")
print(f" Effective dimension: {train_detailed['effective_dimension']:.4f}")
print(f" n_samples: {train_detailed['n_samples']}")
print(f" n_successful: {train_detailed['n_successful_samples']}")
Detailed Trainability: Trainability estimate: 0.1925 Gradient variance: 1.918924e-02 Barren plateau risk: low Effective dimension: 4.0000 n_samples: 100 n_successful: 100
24. Capability Protocols ¶
The library uses Python protocols (PEP 544) for runtime capability checking. This lets you write generic code that works with any encoding that supports specific features.
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Check what protocols SwapEquivariantFeatureMap implements
print(f"isinstance(enc, BaseEncoding): {isinstance(enc, BaseEncoding)}")
print(f"isinstance(enc, ResourceAnalyzable): {isinstance(enc, ResourceAnalyzable)}")
print(f"isinstance(enc, EntanglementQueryable): {isinstance(enc, EntanglementQueryable)}")
isinstance(enc, BaseEncoding): True isinstance(enc, ResourceAnalyzable): True isinstance(enc, EntanglementQueryable): True
# Using type guards for cleaner checks
from encoding_atlas.core.protocols import (
is_resource_analyzable,
is_entanglement_queryable,
is_data_transformable,
)
print(f"is_resource_analyzable: {is_resource_analyzable(enc)}")
print(f"is_entanglement_queryable: {is_entanglement_queryable(enc)}")
print(f"is_data_transformable: {is_data_transformable(enc)}")
is_resource_analyzable: True is_entanglement_queryable: True is_data_transformable: False
# Example: Generic function that works with any ResourceAnalyzable encoding
def analyze_encoding(enc):
"""Analyze any encoding that supports resource analysis."""
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
breakdown = enc.gate_count_breakdown()
print(f"{enc.__class__.__name__}:")
print(f" Total gates: {breakdown['total']}")
print(f" Depth: {summary['depth']}")
else:
print(f"{enc.__class__.__name__}: Resource analysis not supported")
if isinstance(enc, EntanglementQueryable):
pairs = enc.get_entanglement_pairs()
print(f" Entanglement pairs: {pairs}")
analyze_encoding(enc)
SwapEquivariantFeatureMap: Total gates: 20 Depth: 6 Entanglement pairs: [(0, 1), (2, 3)]
25. Registry & Discovery ¶
The library provides a registry system for discovering available encodings.
from encoding_atlas import list_encodings, get_encoding
# List all registered encoding names
all_encodings = list_encodings()
print(f"Registered encodings ({len(all_encodings)}):")
for name in sorted(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 encodings by name (useful for configuration-driven workflows)
enc_from_registry = get_encoding("swap_equivariant", n_features=4, reps=3)
print(f"From registry: {enc_from_registry}")
print(f"Type: {type(enc_from_registry).__name__}")
From registry: SwapEquivariantFeatureMap(n_features=4, n_pairs=2, reps=3) Type: SwapEquivariantFeatureMap
26. Equality, Hashing & Collections ¶
# Two encodings with same parameters are equal
enc1 = SwapEquivariantFeatureMap(n_features=4, reps=2)
enc2 = SwapEquivariantFeatureMap(n_features=4, reps=2)
enc3 = SwapEquivariantFeatureMap(n_features=4, reps=3) # Different reps
enc4 = SwapEquivariantFeatureMap(n_features=6, reps=2) # Different n_features
print(f"enc1 == enc2 (same params): {enc1 == enc2}")
print(f"enc1 == enc3 (diff reps): {enc1 == enc3}")
print(f"enc1 == enc4 (diff n_features): {enc1 == enc4}")
enc1 == enc2 (same params): True enc1 == enc3 (diff reps): False enc1 == enc4 (diff n_features): False
# Consistent hashing enables use in sets and as dict keys
print(f"hash(enc1) == hash(enc2): {hash(enc1) == hash(enc2)}")
# Use in sets (deduplication)
encoding_set = {enc1, enc2, enc3, enc4}
print(f"Set of 4 encodings (2 duplicates): {len(encoding_set)} unique")
# Use as dict keys
results = {enc1: "result_1", enc3: "result_3"}
print(f"Dict lookup enc2 (== enc1): {results[enc2]}")
hash(enc1) == hash(enc2): True Set of 4 encodings (2 duplicates): 3 unique Dict lookup enc2 (== enc1): result_1
# Comparison with non-encoding types
print(f"enc1 == 'not an encoding': {enc1 == 'not an encoding'}")
print(f"enc1 == 42: {enc1 == 42}")
enc1 == 'not an encoding': False enc1 == 42: False
27. Serialization (Pickle) ¶
Encodings can be serialized and deserialized with pickle, which is useful for saving/loading models.
import pickle
enc = SwapEquivariantFeatureMap(n_features=4, reps=3)
# Force property computation before pickling (to test cache preservation)
_ = enc.properties
# Pickle roundtrip
pickled = pickle.dumps(enc)
restored = pickle.loads(pickled)
print(f"Original: {enc}")
print(f"Restored: {restored}")
print(f"Equal: {enc == restored}")
print(f"Same hash: {hash(enc) == hash(restored)}")
Original: SwapEquivariantFeatureMap(n_features=4, n_pairs=2, reps=3) Restored: SwapEquivariantFeatureMap(n_features=4, n_pairs=2, reps=3) Equal: True Same hash: True
# Verify the restored encoding works correctly
x = np.array([0.1, 0.2, 0.3, 0.4])
# Verify equivariance on the restored encoding
result = restored.verify_equivariance(x, [True, False])
print(f"Restored encoding equivariance check: {result}")
# Verify properties were preserved
print(f"Restored properties: depth={restored.properties.depth}, "
f"gates={restored.properties.gate_count}")
Restored encoding equivariance check: True Restored properties: depth=9, gates=30
import threading
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
results = {}
errors = []
def worker(thread_id):
"""Concurrent access to encoding properties and circuit generation."""
try:
# Access properties (triggers lazy initialization)
props = enc.properties
# Generate circuit
x = np.array([0.1 * thread_id, 0.2, 0.3, 0.4])
circuit = enc.get_circuit(x, backend="pennylane")
results[thread_id] = {
'depth': props.depth,
'circuit_type': type(circuit).__name__,
}
except Exception as e:
errors.append((thread_id, str(e)))
# Launch 10 concurrent threads
threads = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Threads completed: {len(results)}")
print(f"Errors: {len(errors)}")
print(f"All depths consistent: {len(set(r['depth'] for r in results.values())) == 1}")
Threads completed: 10 Errors: 0 All depths consistent: True
# Preprocessing recommendation: Scale features to [0, 2π] or [-π, π]
# Example: Raw data with arbitrary scale
raw_data = np.array([100.0, 250.0, -50.0, 75.0])
print(f"Raw data: {raw_data}")
# Method 1: Standardize then scale to rotation-friendly range
standardized = (raw_data - raw_data.mean()) / raw_data.std()
scaled = standardized * np.pi / 3 # Maps ~3σ to ±π
print(f"Standardized + scaled: {scaled}")
# Method 2: Min-max scale to [0, 2π]
min_max = 2 * np.pi * (raw_data - raw_data.min()) / (raw_data.max() - raw_data.min())
print(f"Min-max to [0, 2π]: {min_max}")
Raw data: [100. 250. -50. 75.] Standardized + scaled: [ 0.06138781 1.53469519 -1.41191958 -0.18416342] Min-max to [0, 2π]: [3.14159265 6.28318531 0. 2.61799388]
# IMPORTANT: Features must be organized so that indices (2i, 2i+1) form meaningful pairs
# Example: particle physics data with (energy, momentum) pairs
# Correct pair structure:
# x = [energy_1, momentum_1, energy_2, momentum_2]
# \---- pair 0 ----/ \---- pair 1 ----/
# Wrong: features NOT organized into meaningful pairs
# x = [energy_1, energy_2, momentum_1, momentum_2] # ← breaks pair structure
# Verify that preprocessing preserves equivariance
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
x_preprocessed = np.array([0.5, 1.2, -0.3, 0.8]) # Already in reasonable range
result = enc.verify_equivariance(x_preprocessed, [True, True])
print(f"Equivariance holds after preprocessing: {result}")
Equivariance holds after preprocessing: True
print(f"{'n_features':>10} {'n_qubits':>8} {'n_pairs':>7} {'depth':>5} "
f"{'1Q gates':>8} {'2Q gates':>8} {'total':>8} {'2Q ratio':>8}")
print("-" * 75)
reps = 2
for nf in [2, 4, 6, 8, 10, 12, 16, 20]:
enc = SwapEquivariantFeatureMap(n_features=nf, reps=reps)
b = enc.gate_count_breakdown()
ratio = b['total_two_qubit'] / b['total'] if b['total'] > 0 else 0
print(f"{nf:>10} {enc.n_qubits:>8} {enc.n_pairs:>7} {enc.depth:>5} "
f"{b['total_single_qubit']:>8} {b['total_two_qubit']:>8} "
f"{b['total']:>8} {ratio:>8.2%}")
n_features n_qubits n_pairs depth 1Q gates 2Q gates total 2Q ratio
---------------------------------------------------------------------------
2 2 1 6 8 2 10 20.00%
4 4 2 6 16 4 20 20.00%
6 6 3 6 24 6 30 20.00%
8 8 4 6 32 8 40 20.00%
10 10 5 6 40 10 50 20.00%
12 12 6 6 48 12 60 20.00%
16 16 8 6 64 16 80 20.00%
20 20 10 6 80 20 100 20.00%
# Depth scaling: O(reps), independent of n_features!
# This is a desirable property for hardware execution
print("Depth scaling (fixed n_features=8):")
for r in [1, 2, 3, 4, 5]:
enc = SwapEquivariantFeatureMap(n_features=8, reps=r)
print(f" reps={r}: depth={enc.depth} (= 3 × {r})")
print()
print("Depth scaling (fixed reps=2):")
for nf in [2, 4, 8, 16, 20]:
enc = SwapEquivariantFeatureMap(n_features=nf, reps=2)
print(f" n_features={nf:>2}: depth={enc.depth} (constant!)")
Depth scaling (fixed n_features=8): 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) Depth scaling (fixed reps=2): n_features= 2: depth=6 (constant!) n_features= 4: depth=6 (constant!) n_features= 8: depth=6 (constant!) n_features=16: depth=6 (constant!) n_features=20: depth=6 (constant!)
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Wrong number of features
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3]), backend="pennylane") # 3 ≠ 4
except ValueError as e:
print(f"Wrong feature count: {e}")
Wrong feature count: Expected 4 features, got 3
# NaN values rejected
try:
enc.get_circuit(np.array([0.1, float('nan'), 0.3, 0.4]), backend="pennylane")
except ValueError as e:
print(f"NaN rejected: {e}")
NaN rejected: Input contains NaN or infinite values
# Inf values rejected
try:
enc.get_circuit(np.array([0.1, float('inf'), 0.3, 0.4]), backend="pennylane")
except ValueError as e:
print(f"Inf rejected: {e}")
Inf rejected: Input contains NaN or infinite values
# String values rejected
try:
enc.get_circuit(["0.1", "0.2", "0.3", "0.4"], backend="pennylane")
except TypeError as e:
print(f"Strings rejected: {e}")
Strings rejected: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding.
# Complex values rejected (would silently corrupt data)
try:
enc.get_circuit(np.array([0.1+0j, 0.2+0j, 0.3+0j, 0.4+0j]), backend="pennylane")
except (TypeError, ValueError) as e:
print(f"Complex rejected: {e}")
Complex rejected: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only.
# Edge case: all-zero input (valid)
x_zero = np.array([0.0, 0.0, 0.0, 0.0])
circuit = enc.get_circuit(x_zero, backend="pennylane")
print(f"All-zero input: circuit generated successfully")
# Equivariance still holds for zero input
result = enc.verify_equivariance(x_zero, [True, True])
print(f"Equivariance for zero input: {result}")
All-zero input: circuit generated successfully Equivariance for zero input: True
# Edge case: very large values
x_large = np.array([1e6, -1e6, 1e6, -1e6])
result = enc.verify_equivariance(x_large, [True, False])
print(f"Large values equivariance: {result}")
# Edge case: very small values
x_tiny = np.array([1e-15, 2e-15, 3e-15, 4e-15])
result = enc.verify_equivariance(x_tiny, [True, False])
print(f"Tiny values equivariance: {result}")
Large values equivariance: True Tiny values equivariance: True
# Edge case: minimum configuration (2 features, 1 pair, 1 rep)
enc_min = SwapEquivariantFeatureMap(n_features=2, reps=1)
x_min = np.array([0.5, 1.5])
print(f"Minimum config: {enc_min}")
print(f" n_qubits: {enc_min.n_qubits}, depth: {enc_min.depth}")
print(f" gates: {enc_min.gate_count_breakdown()}")
print(f" equivariance: {enc_min.verify_equivariance(x_min, [True])}")
Minimum config: SwapEquivariantFeatureMap(n_features=2, n_pairs=1, reps=1)
n_qubits: 2, depth: 3
gates: {'ry': 2, 'hadamard': 2, 'cz': 1, 'total_single_qubit': 4, 'total_two_qubit': 1, 'total': 5}
equivariance: True
# Inputs can be plain Python lists (automatically converted)
circuit = enc.get_circuit([0.1, 0.2, 0.3, 0.4], backend="pennylane")
print(f"Python list input: accepted")
# Inputs can be 2D with shape (1, n_features) for single samples
x_2d = np.array([[0.1, 0.2, 0.3, 0.4]])
circuit = enc.get_circuit(x_2d, backend="pennylane")
print(f"2D single-sample input: accepted")
Python list input: accepted 2D single-sample input: accepted
32. Comparing with Other Encodings ¶
See how SwapEquivariantFeatureMap compares to other encodings in the library.
from encoding_atlas import AngleEncoding, IQPEncoding, HardwareEfficientEncoding
# Create encodings with the same number of features
encodings = [
SwapEquivariantFeatureMap(n_features=4, reps=2),
AngleEncoding(n_features=4),
IQPEncoding(n_features=4, reps=2),
HardwareEfficientEncoding(n_features=4, reps=2),
]
# Side-by-side resource comparison
comparison = compare_resources(encodings)
print(f"{'Metric':<25}", end="")
for name in comparison.get('encoding_name', comparison.get('names', [])):
print(f"{name:>25}", end="")
print()
print("-" * (25 + 25 * len(encodings)))
for metric in ['n_qubits', 'depth', 'gate_count', 'single_qubit_gates',
'two_qubit_gates', 'parameter_count', 'two_qubit_ratio']:
if metric in comparison:
print(f"{metric:<25}", end="")
for val in comparison[metric]:
if isinstance(val, float):
print(f"{val:>25.3f}", end="")
else:
print(f"{val:>25}", end="")
print()
Metric SwapEquivariantFeatureMap AngleEncoding IQPEncodingHardwareEfficientEncoding ----------------------------------------------------------------------------------------------------------------------------- n_qubits 4 4 4 4 depth 6 1 6 4 gate_count 20 4 52 14 single_qubit_gates 16 4 28 8 two_qubit_gates 4 0 24 6 parameter_count 0 4 20 8 two_qubit_ratio 0.200 0.000 0.462 0.429
# Compare simulability
print("\nSimulability comparison:")
for enc in encodings:
reason = get_simulability_reason(enc)
print(f" {enc.__class__.__name__:.<40} {reason}")
Simulability comparison: SwapEquivariantFeatureMap............... Not simulable: Entangling circuit with 4 two-qubit gates and non-Clifford operations AngleEncoding........................... Simulable: Encoding produces only product states (no entanglement) IQPEncoding............................. Not simulable: IQP circuits have provable classical hardness under polynomial hierarchy assumptions HardwareEfficientEncoding............... Not simulable: Linear entanglement structure may allow tensor network simulation if entanglement entropy is bounded
# Key differences:
print("Summary of key differences:")
print()
print("SwapEquivariantFeatureMap:")
print(" ✓ Mathematically proven symmetry guarantees")
print(" ✓ Not classically simulable (potential quantum advantage)")
print(" ✓ Entangling (CZ gates within pairs)")
print(" ✓ No trainable parameters (purely data-driven)")
print(" ✓ Pair-local entanglement → efficient hardware mapping")
print()
print("vs AngleEncoding:")
print(" ✗ No entanglement (product states only)")
print(" ✗ Classically simulable")
print(" ✗ No symmetry guarantees")
print()
print("vs IQPEncoding:")
print(" ✓ Also entangling and not simulable")
print(" ✗ No symmetry guarantees")
print(" ✗ All-to-all entanglement (harder to map to hardware)")
Summary of key differences: SwapEquivariantFeatureMap: ✓ Mathematically proven symmetry guarantees ✓ Not classically simulable (potential quantum advantage) ✓ Entangling (CZ gates within pairs) ✓ No trainable parameters (purely data-driven) ✓ Pair-local entanglement → efficient hardware mapping vs AngleEncoding: ✗ No entanglement (product states only) ✗ Classically simulable ✗ No symmetry guarantees vs IQPEncoding: ✓ Also entangling and not simulable ✗ No symmetry guarantees ✗ All-to-all entanglement (harder to map to hardware)
33. Practical Example — Particle Physics Feature Pairs ¶
A realistic example where swap equivariance is physically meaningful: encoding particle-antiparticle pair features where the order within each pair doesn't matter.
# Simulate particle physics data:
# Each event has two jets, each with (pT, eta) measurements
# The two jets are interchangeable (no preferred ordering)
#
# Features: [pT_jet1, eta_jet1, pT_jet2, eta_jet2]
# \-- pair 0 --/ \-- pair 1 --/
rng = np.random.default_rng(42)
n_events = 100
# Generate synthetic jet data
pT_1 = rng.exponential(50, n_events) # Transverse momentum (GeV)
eta_1 = rng.normal(0, 2, n_events) # Pseudorapidity
pT_2 = rng.exponential(50, n_events)
eta_2 = rng.normal(0, 2, n_events)
raw_data = np.column_stack([pT_1, eta_1, pT_2, eta_2])
print(f"Raw data shape: {raw_data.shape}")
print(f"Sample event: pT1={raw_data[0,0]:.1f}, eta1={raw_data[0,1]:.2f}, "
f"pT2={raw_data[0,2]:.1f}, eta2={raw_data[0,3]:.2f}")
Raw data shape: (100, 4) Sample event: pT1=120.2, eta1=0.80, pT2=54.4, eta2=0.31
# Preprocess: standardize then scale to rotation-friendly range
mean = raw_data.mean(axis=0)
std = raw_data.std(axis=0)
standardized = (raw_data - mean) / std
scaled = standardized * np.pi / 3 # ~[-π, π] for most values
print(f"Scaled range: [{scaled.min():.2f}, {scaled.max():.2f}]")
Scaled range: [-2.60, 5.22]
# Create the encoding
enc = SwapEquivariantFeatureMap(n_features=4, reps=2)
# Encode a single event
x_event = scaled[0]
print(f"Event features: {x_event}")
print(f" Pair 0 (jet 1): pT={x_event[0]:.3f}, eta={x_event[1]:.3f}")
print(f" Pair 1 (jet 2): pT={x_event[2]:.3f}, eta={x_event[3]:.3f}")
Event features: [1.78102806 0.44240335 0.17418666 0.09107443] Pair 0 (jet 1): pT=1.781, eta=0.442 Pair 1 (jet 2): pT=0.174, eta=0.091
# The key physics insight: swapping the two jets shouldn't change the prediction.
# Swap equivariance ensures the quantum state transforms predictably.
# Original event
state_original = simulate_encoding_statevector(enc, x_event)
# Swapped event (jet1 ↔ jet2)
x_swapped = enc.group_action([True, True], x_event)
state_swapped = simulate_encoding_statevector(enc, x_swapped)
# The states are related by the unitary representation
U_swap = enc.unitary_representation([True, True])
state_transformed = U_swap @ state_original
# Verify: U(swap) |ψ(original)⟩ = |ψ(swapped)⟩
fidelity = compute_fidelity(state_transformed, state_swapped)
print(f"Fidelity between U(g)|ψ(x)⟩ and |ψ(g·x)⟩: {fidelity:.10f}")
print(f"Equivariance verified: {np.isclose(fidelity, 1.0)}")
Fidelity between U(g)|ψ(x)⟩ and |ψ(g·x)⟩: 1.0000000000 Equivariance verified: True
# Batch encode all events
circuits = enc.get_circuits(scaled[:10], backend="pennylane")
print(f"Encoded {len(circuits)} events as quantum circuits")
# Parallel encoding for large batches
circuits_all = enc.get_circuits(scaled, backend="pennylane", parallel=True)
print(f"Parallel encoding of {len(circuits_all)} events complete")
Encoded 10 events as quantum circuits Parallel encoding of 100 events complete
import logging
# Enable debug logging for the equivariant feature map module
logger = logging.getLogger('encoding_atlas.encodings.equivariant_feature_map')
logger.setLevel(logging.DEBUG)
# Add a handler to see the output
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logger.addHandler(handler)
# Now operations will produce debug output
enc_debug = SwapEquivariantFeatureMap(n_features=4, reps=1)
x_debug = np.array([0.5, 1.0, 1.5, 2.0])
_ = enc_debug.verify_equivariance(x_debug, [True, False])
DEBUG: Initialized SwapEquivariantFeatureMap with n_features=4, n_pairs=2, reps=1 DEBUG: Verifying equivariance for group element [True, False] DEBUG: Equivariance check: overlap = 1.0000000000, threshold = 0.9999999999, result = True
# Clean up: reset logging to avoid noise in subsequent cells
logger.removeHandler(handler)
logger.setLevel(logging.WARNING)
35. Summary ¶
Quick Reference¶
| Feature | Details |
|---|---|
| Class | SwapEquivariantFeatureMap |
| Import | from encoding_atlas import SwapEquivariantFeatureMap |
| Symmetry Group | $S_2^n$ (direct product of $n$ swap groups) |
| Parameters | n_features (even int, required), reps (int ≥ 1, default=2) |
| Qubits | n_features (1 qubit per feature) |
| Depth | $3 \times \text{reps}$ |
| Gates | RY + Hadamard + CZ per layer |
| Total gates | $(2n + n/2) \times \text{reps}$ |
| Entangling | Yes (CZ gates within pairs) |
| Simulability | Not classically simulable |
| Trainable params | 0 (purely data-driven) |
| Backends | PennyLane, Qiskit, Cirq |
Method Reference¶
| Method | Returns | Description |
|---|---|---|
get_circuit(x, backend) |
Circuit | Single-sample circuit |
get_circuits(X, backend, parallel, max_workers) |
list[Circuit] | Batch circuit generation |
group_action(swaps, x) |
ndarray | Apply pair swaps to features |
unitary_representation(swaps) |
ndarray | SWAP/Identity tensor product |
group_generators() |
list[list[bool]] | Generators of the swap group |
verify_equivariance(x, g, atol) |
bool | Exact equivariance check |
verify_equivariance_detailed(x, g, atol) |
dict | Detailed check with overlap |
verify_equivariance_on_generators(x, atol) |
bool | Check on all generators |
verify_equivariance_statistical(x, g, n_shots, significance) |
dict | Scalable statistical check |
verify_equivariance_auto(x, g) |
bool | Auto-select exact/statistical |
gate_count_breakdown() |
dict | Per-gate-type counts |
resource_summary() |
dict | Full resource & symmetry info |
get_entanglement_pairs() |
list[tuple] | CZ gate qubit pairs |
Property Reference¶
| Property | Type | Description |
|---|---|---|
n_features |
int | Number of classical features |
n_qubits |
int | Number of qubits (= n_features) |
n_pairs |
int | Number of feature pairs |
reps |
int | Repetition layers |
depth |
int | Circuit depth |
config |
dict | Configuration (copy) |
properties |
EncodingProperties | Frozen dataclass with all properties |
print("Notebook complete. All features of SwapEquivariantFeatureMap demonstrated.")
Notebook complete. All features of SwapEquivariantFeatureMap demonstrated.