SO2EquivariantFeatureMap: Complete Feature Demonstration¶
This notebook provides a comprehensive walkthrough of the SO2EquivariantFeatureMap encoding from the Quantum Encoding Atlas library. This encoding is equivariant to 2D rotations, satisfying the mathematical property:
$$U(\phi)|\psi(r, \theta)\rangle = |\psi(r, \theta + \phi)\rangle$$
where $(r, \theta)$ are polar coordinates of a 2D point and $\phi$ is a rotation angle.
The encoding maps 2D Cartesian data into quantum states using angular momentum eigenstates:
$$|\psi(r, \theta)\rangle = \sum_{m} c_m(r) \cdot e^{im\theta} |m\rangle$$
where:
- $m \in \{-\text{max\_m}, \ldots, +\text{max\_m}\}$ are angular momentum quantum numbers
- $c_m(r)$ are radial amplitude functions (rotation-invariant)
- $e^{im\theta}$ encodes the angle as phases
- $|m\rangle$ are angular momentum basis states
Table of Contents
- Setup & Installation
- Instantiation & Constructor Validation
- Core Properties & Configuration
- Angular Momentum Basis & State Encoding
- Circuit Generation (Multi-Backend)
- Batch Circuit Generation
- SO(2) Group Actions & Unitary Representation
- Equivariance Verification
- Statistical Equivariance Verification
- Resource Analysis & Gate Counts
- Capability Protocols
- Radial Functions: Gaussian vs Uniform
- Varying Angular Momentum
- Edge Cases & Numerical Stability
- Equality, Hashing & Serialization
- Thread Safety & Concurrent Access
- Comparison with Other Encodings
- Practical Application: Rotation-Invariant Classification
- Summary
1. Setup & Installation¶
# Install the library (uncomment if not already installed)
# !pip install encoding-atlas
# For full multi-backend support, also install optional dependencies:
# !pip install encoding-atlas[qiskit] # Qiskit backend
# !pip install encoding-atlas[cirq] # Cirq backend
import numpy as np
import warnings
# Check library version
import encoding_atlas
print(f"encoding-atlas version: {encoding_atlas.__version__}")
encoding-atlas version: 0.2.0
# 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}: {status}")
Backend availability: pennylane: v0.42.3 qiskit: v2.3.0 cirq: v1.5.0
2. Instantiation & Constructor Validation¶
The SO2EquivariantFeatureMap enforces strict mathematical constraints. SO(2) rotation equivariance requires exactly 2D input (Cartesian coordinates [x, y]).
from encoding_atlas import SO2EquivariantFeatureMap
2.1 Default Instantiation¶
# Default: max_angular_momentum=1, gaussian radial, sigma=1.0
enc = SO2EquivariantFeatureMap()
print(f"repr: {enc!r}")
print(f"n_features: {enc.n_features}")
print(f"n_qubits: {enc.n_qubits}")
print(f"depth: {enc.depth}")
print(f"max_angular_momentum: {enc.max_angular_momentum}")
print(f"radial_function: {enc.radial_function}")
print(f"radial_sigma: {enc.radial_sigma}")
print(f"angular_momenta: {enc.angular_momenta}")
repr: SO2EquivariantFeatureMap(max_angular_momentum=1, radial_function='gaussian', radial_sigma=1.0) n_features: 2 n_qubits: 2 depth: 6 max_angular_momentum: 1 radial_function: gaussian radial_sigma: 1.0 angular_momenta: [-1, 0, 1]
2.2 Custom Parameters¶
# Higher angular momentum with uniform radial function
enc_custom = SO2EquivariantFeatureMap(
max_angular_momentum=2,
radial_function="uniform",
radial_sigma=2.0, # radial_sigma is stored but not used when radial_function="uniform"
)
print(f"repr: {enc_custom!r}")
print(f"n_qubits: {enc_custom.n_qubits}")
print(f"angular_momenta: {enc_custom.angular_momenta}")
print(f"Number of states: {len(enc_custom.angular_momenta)}")
repr: SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function='uniform', radial_sigma=2.0) n_qubits: 3 angular_momenta: [-2, -1, 0, 1, 2] Number of states: 5
2.3 Validation: n_features Must Be Exactly 2¶
This is a fundamental mathematical constraint: SO(2) rotation equivariance is defined for 2D data.
# n_features != 2 raises ValueError
for n in [1, 3, 4, 10]:
try:
SO2EquivariantFeatureMap(n_features=n)
print(f" n_features={n}: unexpectedly succeeded")
except ValueError as e:
print(f" n_features={n}: ValueError - {str(e)[:80]}...")
# n_features=2 is valid (explicit or default)
enc_explicit = SO2EquivariantFeatureMap(n_features=2)
print(f"\n n_features=2: OK -> {enc_explicit!r}")
n_features=1: ValueError - SO2EquivariantFeatureMap requires n_features=2 (2D Cartesian coordinates [x, y])... n_features=3: ValueError - SO2EquivariantFeatureMap requires n_features=2 (2D Cartesian coordinates [x, y])... n_features=4: ValueError - SO2EquivariantFeatureMap requires n_features=2 (2D Cartesian coordinates [x, y])... n_features=10: ValueError - SO2EquivariantFeatureMap requires n_features=2 (2D Cartesian coordinates [x, y])... n_features=2: OK -> SO2EquivariantFeatureMap(max_angular_momentum=1, radial_function='gaussian', radial_sigma=1.0)
2.4 Validation: Type Checking¶
# --- n_features type errors ---
for bad_val in [2.0, "2", True, [2]]:
try:
SO2EquivariantFeatureMap(n_features=bad_val)
print(f" n_features={bad_val!r}: unexpectedly succeeded")
except TypeError as e:
print(f" n_features={bad_val!r} ({type(bad_val).__name__}): TypeError - {e}")
n_features=2.0 (float): TypeError - n_features must be an integer, got float n_features='2' (str): TypeError - n_features must be an integer, got str n_features=True (bool): TypeError - n_features must be an integer, got bool n_features=[2] (list): TypeError - n_features must be an integer, got list
# --- max_angular_momentum validation ---
# Must be a non-negative integer
print("max_angular_momentum validation:")
# Negative values
try:
SO2EquivariantFeatureMap(max_angular_momentum=-1)
except ValueError as e:
print(f" -1: ValueError - {e}")
# Non-integer types
for bad_val in [1.5, True, "1"]:
try:
SO2EquivariantFeatureMap(max_angular_momentum=bad_val)
except TypeError as e:
print(f" {bad_val!r} ({type(bad_val).__name__}): TypeError - {e}")
# Zero is valid (single state m=0)
enc_zero = SO2EquivariantFeatureMap(max_angular_momentum=0)
print(f" 0: OK -> qubits={enc_zero.n_qubits}, momenta={enc_zero.angular_momenta}")
max_angular_momentum validation: -1: ValueError - max_angular_momentum must be a non-negative integer, got -1 1.5 (float): TypeError - max_angular_momentum must be an integer, got float True (bool): TypeError - max_angular_momentum must be an integer, got bool '1' (str): TypeError - max_angular_momentum must be an integer, got str 0: OK -> qubits=1, momenta=[0]
# --- radial_function validation ---
print("radial_function validation:")
try:
SO2EquivariantFeatureMap(radial_function="invalid")
except ValueError as e:
print(f" 'invalid': ValueError - {e}")
# Valid options
for fn in ["gaussian", "uniform"]:
enc_fn = SO2EquivariantFeatureMap(radial_function=fn)
print(f" '{fn}': OK")
radial_function validation: 'invalid': ValueError - radial_function must be one of ['gaussian', 'uniform'], got 'invalid' 'gaussian': OK 'uniform': OK
# --- radial_sigma validation ---
print("radial_sigma validation:")
# Must be positive and finite
for bad_val, expected_err in [
(0, ValueError),
(-1.0, ValueError),
(float('inf'), ValueError),
(float('nan'), ValueError),
(True, TypeError),
("1.0", TypeError),
]:
try:
SO2EquivariantFeatureMap(radial_sigma=bad_val)
print(f" sigma={bad_val!r}: unexpectedly succeeded")
except (TypeError, ValueError) as e:
print(f" sigma={bad_val!r}: {type(e).__name__} - {str(e)[:70]}...")
# Integer sigma is accepted (converted internally)
enc_int_sigma = SO2EquivariantFeatureMap(radial_sigma=2)
print(f"\n sigma=2 (int): OK -> sigma={enc_int_sigma.radial_sigma}")
radial_sigma validation: sigma=0: ValueError - radial_sigma must be positive, got 0... sigma=-1.0: ValueError - radial_sigma must be positive, got -1.0... sigma=inf: ValueError - radial_sigma must be finite, got inf... sigma=nan: ValueError - radial_sigma must be finite, got nan... sigma=True: TypeError - radial_sigma must be a positive number, got bool... sigma='1.0': TypeError - radial_sigma must be a positive number, got str... sigma=2 (int): OK -> sigma=2
2.5 Large Angular Momentum Warning¶
# max_angular_momentum > 3 emits a UserWarning about scalability
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_large = SO2EquivariantFeatureMap(max_angular_momentum=4)
if w:
print(f"Warning raised: {w[0].category.__name__}")
print(f"Message: {w[0].message}")
print(f"\nEncoding created: qubits={enc_large.n_qubits}, "
f"states={len(enc_large.angular_momenta)}, "
f"momenta={enc_large.angular_momenta}")
Large max_angular_momentum (4) with 4 qubits may have scalability issues due to O(2^n) state preparation
Warning raised: UserWarning Message: Large max_angular_momentum (4) requires 4 qubits and O(2^4) gates for state preparation. This may not be practical for hardware deployment. Consider using a smaller max_angular_momentum for NISQ devices. Encoding created: qubits=4, states=9, momenta=[-4, -3, -2, -1, 0, 1, 2, 3, 4]
3. Core Properties & Configuration¶
Every encoding exposes its configuration and computed properties.
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# The config property returns a copy of the encoding-specific parameters
config = enc.config
print("Configuration:")
for key, value in config.items():
print(f" {key}: {value}")
# Config is a copy - modifying it doesn't affect the encoding
config['max_angular_momentum'] = 999
print(f"\nOriginal max_angular_momentum: {enc.max_angular_momentum} (unchanged)")
Configuration: max_angular_momentum: 1 radial_function: gaussian radial_sigma: 1.0 Original max_angular_momentum: 1 (unchanged)
# The properties object is a frozen dataclass with computed circuit metrics
props = enc.properties
print(f"Type: {type(props).__name__}")
print(f"\nEncoding Properties:")
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_estimate: {props.trainability_estimate}")
Type: EncodingProperties Encoding Properties: n_qubits: 2 depth: 6 gate_count: 5 single_qubit_gates: 5 two_qubit_gates: 0 parameter_count: 0 is_entangling: True simulability: not_simulable trainability_estimate: None
# Properties are frozen (immutable)
try:
props.n_qubits = 100
except AttributeError as e:
print(f"Cannot modify frozen properties: {e}")
Cannot modify frozen properties: cannot assign to field 'n_qubits'
# Properties are lazily computed and cached (thread-safe)
props1 = enc.properties
props2 = enc.properties
print(f"Same object returned: {props1 is props2}")
# Can convert to dictionary
props_dict = props.to_dict()
print(f"\nAs dict: {props_dict}")
Same object returned: True
As dict: {'n_qubits': 2, 'depth': 6, 'gate_count': 5, 'single_qubit_gates': 5, 'two_qubit_gates': 0, 'parameter_count': 0, 'is_entangling': True, 'simulability': 'not_simulable', 'expressibility': None, 'entanglement_capability': None, 'trainability_estimate': None, 'noise_resilience_estimate': None, 'notes': ''}
4. Angular Momentum Basis & State Encoding¶
The SO(2) encoding uses angular momentum eigenstates as the computational basis. Understanding this mapping is key to understanding how equivariance works.
4.1 Spin-1 Triplet Basis (max_angular_momentum=1)¶
For max_angular_momentum=1 with 2 qubits, the encoding uses the spin-1 triplet states:
| Angular Momentum $m$ | Basis State $|m\rangle$ | |---|---| | $m = +1$ | $|00\rangle$ | | $m = 0$ | $(|01\rangle + |10\rangle)/\sqrt{2}$ | | $m = -1$ | $|11\rangle$ |
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Access the internal basis mapping
print("Spin-1 triplet basis states (max_m=1, 2 qubits):")
print(f" |m=+1> = {enc._basis_map[+1]} (= |00>)")
print(f" |m= 0> = {enc._basis_map[0]} (= (|01> + |10>)/sqrt(2))")
print(f" |m=-1> = {enc._basis_map[-1]} (= |11>)")
# Verify orthonormality
print("\nOrthonormality check:")
for m1 in enc.angular_momenta:
for m2 in enc.angular_momenta:
overlap = np.abs(np.vdot(enc._basis_map[m1], enc._basis_map[m2]))
expected = 1.0 if m1 == m2 else 0.0
print(f" <m={m1:+d}|m={m2:+d}> = {overlap:.6f} (expected {expected})")
Spin-1 triplet basis states (max_m=1, 2 qubits): |m=+1> = [1.+0.j 0.+0.j 0.+0.j 0.+0.j] (= |00>) |m= 0> = [0. +0.j 0.70710678+0.j 0.70710678+0.j 0. +0.j] (= (|01> + |10>)/sqrt(2)) |m=-1> = [0.+0.j 0.+0.j 0.+0.j 1.+0.j] (= |11>) Orthonormality check: <m=-1|m=-1> = 1.000000 (expected 1.0) <m=-1|m=+0> = 0.000000 (expected 0.0) <m=-1|m=+1> = 0.000000 (expected 0.0) <m=+0|m=-1> = 0.000000 (expected 0.0) <m=+0|m=+0> = 1.000000 (expected 1.0) <m=+0|m=+1> = 0.000000 (expected 0.0) <m=+1|m=-1> = 0.000000 (expected 0.0) <m=+1|m=+0> = 0.000000 (expected 0.0) <m=+1|m=+1> = 1.000000 (expected 1.0)
4.2 General Basis (max_angular_momentum > 1)¶
# For max_angular_momentum=2, we get 5 states and 3 qubits
enc2 = SO2EquivariantFeatureMap(max_angular_momentum=2)
print(f"Angular momenta: {enc2.angular_momenta}")
print(f"Number of states: {len(enc2.angular_momenta)}")
print(f"Qubits needed: ceil(log2({len(enc2.angular_momenta)})) = {enc2.n_qubits}")
print("\nBasis states (standard computational basis ordering):")
for m in enc2.angular_momenta:
nonzero_idx = np.nonzero(enc2._basis_map[m])[0]
print(f" |m={m:+d}> -> computational basis |{nonzero_idx[0]:0{enc2.n_qubits}b}>")
Angular momenta: [-2, -1, 0, 1, 2] Number of states: 5 Qubits needed: ceil(log2(5)) = 3 Basis states (standard computational basis ordering): |m=-2> -> computational basis |000> |m=-1> -> computational basis |001> |m=+0> -> computational basis |010> |m=+1> -> computational basis |011> |m=+2> -> computational basis |100>
4.3 State Encoding: Polar Decomposition¶
The encoding converts Cartesian $(x, y)$ to polar $(r, \theta)$, then builds:
$$|\psi(r, \theta)\rangle = \sum_m c_m(r) \cdot e^{im\theta} |m\rangle$$
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Encode a 2D point
x = np.array([0.5, 0.3])
r = np.sqrt(x[0]**2 + x[1]**2)
theta = np.arctan2(x[1], x[0])
print(f"Input: x = {x}")
print(f"Polar: r = {r:.6f}, theta = {theta:.6f} rad ({np.degrees(theta):.2f} deg)")
# Get the encoded quantum state
state = enc._encode_state(x)
print(f"\nQuantum state |psi(x)>:")
for i, amp in enumerate(state):
if abs(amp) > 1e-10:
print(f" |{i:0{enc.n_qubits}b}> : {amp:.6f} (|amp|^2 = {abs(amp)**2:.6f})")
# Verify normalization
norm = np.linalg.norm(state)
print(f"\nState norm: {norm:.10f} (should be 1.0)")
Input: x = [0.5 0.3] Polar: r = 0.583095, theta = 0.540420 rad (30.96 deg) Quantum state |psi(x)>: |00> : 0.508213+0.304928j (|amp|^2 = 0.351261) |01> : 0.385667+0.000000j (|amp|^2 = 0.148739) |10> : 0.385667+0.000000j (|amp|^2 = 0.148739) |11> : 0.508213-0.304928j (|amp|^2 = 0.351261) State norm: 1.0000000000 (should be 1.0)
# Radial amplitudes control how the radius is distributed across angular momenta
print("Radial amplitudes c_m(r):")
c = enc._radial_amplitudes(r)
for m, amp in sorted(c.items()):
print(f" c_{m:+d}(r={r:.4f}) = {amp:.6f}")
# Verify normalization of radial amplitudes
norm_sq = sum(amp**2 for amp in c.values())
print(f"\nSum |c_m|^2 = {norm_sq:.10f} (should be 1.0)")
Radial amplitudes c_m(r): c_-1(r=0.5831) = 0.592673 c_+0(r=0.5831) = 0.545415 c_+1(r=0.5831) = 0.592673 Sum |c_m|^2 = 1.0000000000 (should be 1.0)
5. Circuit Generation (Multi-Backend)¶
The encoding supports three quantum computing backends: PennyLane, Qiskit, and Cirq.
5.1 PennyLane Backend¶
import pennylane as qml
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
# get_circuit returns a callable for PennyLane
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():
circuit_fn()
return qml.state()
state = get_state()
print("Statevector from PennyLane:")
for i, amp in enumerate(state):
if abs(amp) > 1e-10:
print(f" |{i:0{enc.n_qubits}b}> : {amp:.6f}")
Statevector from PennyLane: |00> : 0.508213+0.304928j |01> : 0.385667+0.000000j |10> : 0.385667+0.000000j |11> : 0.508213-0.304928j
# Visualize the circuit using PennyLane's drawer
@qml.qnode(dev)
def draw_circuit():
circuit_fn()
return qml.state()
print(qml.draw(draw_circuit)())
0: ─╭|Ψ⟩─┤ State 1: ─╰|Ψ⟩─┤ State
5.2 Qiskit Backend¶
if backends_available['qiskit']:
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
# get_circuit returns a QuantumCircuit for Qiskit
qc = enc.get_circuit(x, backend="qiskit")
print(f"Type: {type(qc).__name__}")
print(f"Qubits: {qc.num_qubits}")
print(f"\nCircuit:")
print(qc)
else:
print("Qiskit not installed - skipping")
Type: QuantumCircuit
Qubits: 2
Circuit:
┌────────────────────────────────────────────────────────────────┐
q_0: ┤0 ├
│ Initialize(0.50821+0.30493j,0.38567,0.38567,0.50821-0.30493j) │
q_1: ┤1 ├
└────────────────────────────────────────────────────────────────┘
5.3 Cirq Backend¶
if backends_available['cirq']:
import cirq
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
# get_circuit returns a cirq.Circuit
cirq_circuit = enc.get_circuit(x, backend="cirq")
print(f"Type: {type(cirq_circuit).__name__}")
print(f"\nCircuit:")
print(cirq_circuit)
# Simulate and verify state
sim = cirq.Simulator()
result = sim.simulate(cirq_circuit)
print(f"\nFinal state (Cirq):")
for i, amp in enumerate(result.final_state_vector):
if abs(amp) > 1e-10:
print(f" |{i:0{enc.n_qubits}b}> : {amp:.6f}")
else:
print("Cirq not installed - skipping")
Type: Circuit
Circuit:
┌ ┐
│ 0.508+0.305j 0.212+0.127j 0.253+0.152j 0.333+0.624j│
0: ───│ 0.386+0.j -0.923+0.j 0. +0.j 0. -0.j │───
│ 0.386+0.j 0.161+0.j -0.908-0.j 0. -0.j │
│ 0.508-0.305j 0.212-0.127j 0.253-0.152j -0.707-0.j │
└ ┘
│
1: ───#2──────────────────────────────────────────────────────────
Final state (Cirq):
|00> : 0.508213+0.304928j
|01> : 0.385667+0.000000j
|10> : 0.385667+0.000000j
|11> : 0.508213-0.304928j
5.4 Backend Validation¶
# Invalid backend raises ValueError
enc = SO2EquivariantFeatureMap()
x = np.array([0.5, 0.3])
try:
enc.get_circuit(x, backend="invalid_backend")
except ValueError as e:
print(f"Invalid backend: {e}")
Invalid backend: Unknown backend: invalid_backend
5.5 Input Validation for Circuit Generation¶
enc = SO2EquivariantFeatureMap()
# --- Wrong number of features ---
try:
enc.get_circuit(np.array([1.0, 2.0, 3.0]), backend="pennylane")
except ValueError as e:
print(f"Wrong features: {e}")
# --- NaN values ---
try:
enc.get_circuit(np.array([1.0, float('nan')]), backend="pennylane")
except ValueError as e:
print(f"NaN input: {e}")
# --- Inf values ---
try:
enc.get_circuit(np.array([float('inf'), 1.0]), backend="pennylane")
except ValueError as e:
print(f"Inf input: {e}")
# --- Complex values ---
try:
enc.get_circuit(np.array([1.0 + 2j, 0.5]), backend="pennylane")
except TypeError as e:
print(f"Complex input: {e}")
# --- String values ---
try:
enc.get_circuit(["0.5", "0.3"], backend="pennylane")
except TypeError as e:
print(f"String input: {e}")
Wrong features: Expected 2 features, got 3 NaN input: Input contains NaN or infinite values Inf input: Input contains NaN or infinite values Complex input: Input contains complex values (dtype: complex128). Complex numbers are not supported. Use real-valued data only. String input: Input contains string values. Expected numeric data, got str. Convert strings to floats before encoding.
6. Batch Circuit Generation¶
The get_circuits method generates circuits for multiple data samples at once, with optional parallel processing.
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Batch of 2D points
X = np.array([
[0.5, 0.3],
[1.0, 0.0],
[-0.2, 0.8],
[0.0, 1.5],
[-1.0, -1.0],
])
# Sequential processing (default)
circuits = enc.get_circuits(X, backend="pennylane")
print(f"Generated {len(circuits)} circuits")
print(f"Each circuit is callable: {all(callable(c) for c in circuits)}")
Generated 5 circuits Each circuit is callable: True
# Parallel processing for large batches
X_large = np.random.randn(50, 2)
import time
# Sequential
t0 = time.perf_counter()
circuits_seq = enc.get_circuits(X_large, backend="pennylane")
t_seq = time.perf_counter() - t0
# Parallel
t0 = time.perf_counter()
circuits_par = enc.get_circuits(X_large, backend="pennylane", parallel=True, max_workers=4)
t_par = time.perf_counter() - t0
print(f"Sequential: {t_seq:.4f}s ({len(circuits_seq)} circuits)")
print(f"Parallel: {t_par:.4f}s ({len(circuits_par)} circuits)")
Sequential: 0.0072s (50 circuits) Parallel: 0.0102s (50 circuits)
# Single sample can also be passed to get_circuits (auto-reshaped)
single = np.array([0.5, 0.3])
circuits_single = enc.get_circuits(single, backend="pennylane")
print(f"Single sample -> {len(circuits_single)} circuit(s)")
Single sample -> 1 circuit(s)
7. SO(2) Group Actions & Unitary Representation¶
The SO(2) group consists of 2D rotations. The encoding provides methods to apply group actions to both classical data and quantum states.
7.1 Group Action on Classical Data¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Rotate a 2D point
x = np.array([1.0, 0.0]) # Point on the x-axis
rotations = {
"0 (identity)": 0,
"pi/6 (30 deg)": np.pi / 6,
"pi/4 (45 deg)": np.pi / 4,
"pi/2 (90 deg)": np.pi / 2,
"pi (180 deg)": np.pi,
"3pi/2 (270 deg)": 3 * np.pi / 2,
"2pi (360 deg)": 2 * np.pi,
}
print(f"Original point: {x}")
print(f"{'Rotation':<20} {'Rotated point':<30} {'Radius preserved?'}")
print("-" * 70)
for label, phi in rotations.items():
rotated = enc.group_action(phi, x)
r_original = np.linalg.norm(x)
r_rotated = np.linalg.norm(rotated)
print(f"{label:<20} [{rotated[0]:+.6f}, {rotated[1]:+.6f}] "
f"r={r_rotated:.6f} {'YES' if np.isclose(r_original, r_rotated) else 'NO'}")
Original point: [1. 0.] Rotation Rotated point Radius preserved? ---------------------------------------------------------------------- 0 (identity) [+1.000000, +0.000000] r=1.000000 YES pi/6 (30 deg) [+0.866025, +0.500000] r=1.000000 YES pi/4 (45 deg) [+0.707107, +0.707107] r=1.000000 YES pi/2 (90 deg) [+0.000000, +1.000000] r=1.000000 YES pi (180 deg) [-1.000000, +0.000000] r=1.000000 YES 3pi/2 (270 deg) [-0.000000, -1.000000] r=1.000000 YES 2pi (360 deg) [+1.000000, -0.000000] r=1.000000 YES
7.2 Unitary Representation U(phi)¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
phi = np.pi / 4 # 45-degree rotation
U = enc.unitary_representation(phi)
print(f"U(pi/4) shape: {U.shape}")
print(f"\nU(pi/4) matrix:")
print(np.array2string(U, precision=4, suppress_small=True))
# Verify unitarity: U U^dag = I
identity_check = U @ U.conj().T
print(f"\nU @ U^dag = I? {np.allclose(identity_check, np.eye(U.shape[0]))}")
U(pi/4) shape: (4, 4) U(pi/4) matrix: [[0.7071+0.7071j 0. +0.j 0. +0.j 0. +0.j ] [0. +0.j 1. +0.j 0. +0.j 0. +0.j ] [0. +0.j 0. +0.j 1. +0.j 0. +0.j ] [0. +0.j 0. +0.j 0. +0.j 0.7071-0.7071j]] U @ U^dag = I? True
# Verify group homomorphism: U(phi1) @ U(phi2) = U(phi1 + phi2)
print("Group homomorphism property: U(phi1) @ U(phi2) = U(phi1 + phi2)")
print("=" * 60)
test_pairs = [
(np.pi / 6, np.pi / 3), # pi/6 + pi/3 = pi/2
(np.pi / 4, np.pi / 4), # pi/4 + pi/4 = pi/2
(np.pi, np.pi), # pi + pi = 2pi
(np.pi / 2, 3 * np.pi / 2), # pi/2 + 3pi/2 = 2pi
]
for phi1, phi2 in test_pairs:
U1 = enc.unitary_representation(phi1)
U2 = enc.unitary_representation(phi2)
U_product = U1 @ U2
U_sum = enc.unitary_representation(phi1 + phi2)
match = np.allclose(U_product, U_sum, atol=1e-10)
print(f" U({phi1:.4f}) @ U({phi2:.4f}) = U({phi1+phi2:.4f}): {match}")
Group homomorphism property: U(phi1) @ U(phi2) = U(phi1 + phi2) ============================================================ U(0.5236) @ U(1.0472) = U(1.5708): True U(0.7854) @ U(0.7854) = U(1.5708): True U(3.1416) @ U(3.1416) = U(6.2832): True U(1.5708) @ U(4.7124) = U(6.2832): True
# Identity: U(0) = I
U_identity = enc.unitary_representation(0.0)
print(f"U(0) = I? {np.allclose(U_identity, np.eye(U_identity.shape[0]))}")
# Inverse: U(phi) @ U(-phi) = I
phi = np.pi / 3
U_phi = enc.unitary_representation(phi)
U_neg_phi = enc.unitary_representation(-phi)
print(f"U(pi/3) @ U(-pi/3) = I? {np.allclose(U_phi @ U_neg_phi, np.eye(U_phi.shape[0]))}")
# Periodicity: U(2pi) = I
U_2pi = enc.unitary_representation(2 * np.pi)
print(f"U(2*pi) = I? {np.allclose(U_2pi, np.eye(U_2pi.shape[0]), atol=1e-10)}")
U(0) = I? True U(pi/3) @ U(-pi/3) = I? True U(2*pi) = I? True
7.3 Group Generators¶
# SO(2) is continuous, so generators are representative test angles
generators = enc.group_generators()
print(f"Group generators (representative angles): {generators}")
print(f"In degrees: {[np.degrees(g) for g in generators]}")
Group generators (representative angles): [0.7853981633974483, 1.5707963267948966, 3.141592653589793, 4.71238898038469] In degrees: [np.float64(45.0), np.float64(90.0), np.float64(180.0), np.float64(270.0)]
8. Equivariance Verification¶
The core mathematical property: rotating the input is equivalent to applying the rotation operator on the quantum state.
$$U(\phi)|\psi(x)\rangle = |\psi(\text{rotate}(x, \phi))\rangle$$
8.1 Basic Equivariance Check¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
# Verify at a specific angle
phi = np.pi / 4
result = enc.verify_equivariance(x, phi)
print(f"Equivariant at phi=pi/4? {result}")
Equivariant at phi=pi/4? True
# Verify across many angles
angles = np.linspace(0, 2 * np.pi, 12, endpoint=False)
print(f"{'Angle (rad)':<15} {'Angle (deg)':<15} {'Equivariant?'}")
print("-" * 45)
for phi in angles:
is_eq = enc.verify_equivariance(x, phi, atol=1e-6)
print(f"{phi:<15.4f} {np.degrees(phi):<15.1f} {is_eq}")
# All angles should be equivariant
all_equivariant = all(enc.verify_equivariance(x, phi, atol=1e-6) for phi in angles)
print(f"\nAll angles equivariant: {all_equivariant}")
Angle (rad) Angle (deg) Equivariant? --------------------------------------------- 0.0000 0.0 True 0.5236 30.0 True 1.0472 60.0 True 1.5708 90.0 True 2.0944 120.0 True 2.6180 150.0 True 3.1416 180.0 True 3.6652 210.0 True 4.1888 240.0 True 4.7124 270.0 True 5.2360 300.0 True 5.7596 330.0 True All angles equivariant: True
8.2 Detailed Equivariance Verification¶
# verify_equivariance_detailed returns overlap and diagnostic info
result = enc.verify_equivariance_detailed(x, np.pi / 4)
print("Detailed verification result:")
for key, value in result.items():
print(f" {key}: {value}")
Detailed verification result: equivariant: True overlap: 1.0 tolerance: 1e-10 group_element: 0.7853981633974483
# Verify overlap values across multiple angles
print(f"{'Angle':<12} {'Overlap':<20} {'Equivariant?'}")
print("-" * 50)
for phi in [0, np.pi/6, np.pi/4, np.pi/3, np.pi/2, np.pi, 3*np.pi/2]:
res = enc.verify_equivariance_detailed(x, phi)
print(f"{phi:<12.4f} {res['overlap']:<20.15f} {res['equivariant']}")
Angle Overlap Equivariant? -------------------------------------------------- 0.0000 1.000000000000000 True 0.5236 1.000000000000000 True 0.7854 1.000000000000000 True 1.0472 1.000000000000000 True 1.5708 1.000000000000000 True 3.1416 1.000000000000000 True 4.7124 1.000000000000000 True
8.3 Verification on Group Generators¶
# Verifying on generators is sufficient for the entire group
all_gens_ok = enc.verify_equivariance_on_generators(x, atol=1e-6)
print(f"Equivariant on all generators: {all_gens_ok}")
print(f"Generators tested: {enc.group_generators()}")
Equivariant on all generators: True Generators tested: [0.7853981633974483, 1.5707963267948966, 3.141592653589793, 4.71238898038469]
8.4 Equivariance with Different Data Points¶
# Test equivariance across various 2D points
test_points = [
np.array([1.0, 0.0]), # On x-axis
np.array([0.0, 1.0]), # On y-axis
np.array([1.0, 1.0]), # 45 degrees
np.array([-0.5, 0.5]), # Second quadrant
np.array([-1.0, -1.0]), # Third quadrant
np.array([3.0, 4.0]), # Larger radius
np.array([0.01, 0.01]), # Near origin
]
phi = np.pi / 3 # 60-degree rotation
print(f"Testing equivariance at phi = pi/3 ({np.degrees(phi):.0f} deg):")
print(f"{'Point':<25} {'Equivariant?'}")
print("-" * 40)
for pt in test_points:
is_eq = enc.verify_equivariance(pt, phi, atol=1e-6)
print(f"[{pt[0]:+.2f}, {pt[1]:+.2f}]{'':<15} {is_eq}")
Testing equivariance at phi = pi/3 (60 deg): Point Equivariant? ---------------------------------------- [+1.00, +0.00] True [+0.00, +1.00] True [+1.00, +1.00] True [-0.50, +0.50] True [-1.00, -1.00] True [+3.00, +4.00] True [+0.01, +0.01] True
8.5 Manual Equivariance Verification (Step by Step)¶
Let's manually verify the equivariance condition: $U(\phi)|\psi(x)\rangle = |\psi(\text{rotate}(x, \phi))\rangle$
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
phi = np.pi / 4
# Step 1: Encode x -> |psi(x)>
state_x = enc._encode_state(x)
print("Step 1 - |psi(x)>:")
for i, amp in enumerate(state_x):
if abs(amp) > 1e-10:
print(f" |{i:02b}> : {amp:.6f}")
# Step 2: Apply U(phi) to |psi(x)>
U_phi = enc.unitary_representation(phi)
left_side = U_phi @ state_x
print(f"\nStep 2 - U(phi)|psi(x)>:")
for i, amp in enumerate(left_side):
if abs(amp) > 1e-10:
print(f" |{i:02b}> : {amp:.6f}")
# Step 3: Rotate x, then encode -> |psi(rotate(x, phi))>
x_rotated = enc.group_action(phi, x)
print(f"\nStep 3a - Rotated point: [{x_rotated[0]:.6f}, {x_rotated[1]:.6f}]")
right_side = enc._encode_state(x_rotated)
print(f"Step 3b - |psi(rotate(x, phi))>:")
for i, amp in enumerate(right_side):
if abs(amp) > 1e-10:
print(f" |{i:02b}> : {amp:.6f}")
# Step 4: Compare (up to global phase)
overlap = np.abs(np.vdot(left_side, right_side))
print(f"\nStep 4 - Overlap |<left|right>|: {overlap:.15f}")
print(f"States match (up to global phase): {np.isclose(overlap, 1.0, atol=1e-10)}")
Step 1 - |psi(x)>: |00> : 0.508213+0.304928j |01> : 0.385667+0.000000j |10> : 0.385667+0.000000j |11> : 0.508213-0.304928j Step 2 - U(phi)|psi(x)>: |00> : 0.143744+0.574977j |01> : 0.385667+0.000000j |10> : 0.385667+0.000000j |11> : 0.143744-0.574977j Step 3a - Rotated point: [0.141421, 0.565685] Step 3b - |psi(rotate(x, phi))>: |00> : 0.143744+0.574977j |01> : 0.385667+0.000000j |10> : 0.385667+0.000000j |11> : 0.143744-0.574977j Step 4 - Overlap |<left|right>|: 1.000000000000000 States match (up to global phase): True
9. Statistical Equivariance Verification¶
For larger systems where exact statevector comparison is infeasible, the encoding provides a measurement-based statistical test using chi-squared analysis.
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
phi = np.pi / 4
# Statistical verification with default parameters
stat_result = enc.verify_equivariance_statistical(x, phi)
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.354905 test_statistic: 3.248287 significance: 0.010000 n_shots: 10000 group_element: 0.785398 method: chi_squared confidence_level: 0.990000
# Vary confidence levels
print(f"{'Shots':<10} {'Significance':<15} {'Equivariant?':<15} {'P-value':<15} {'Confidence'}")
print("-" * 70)
for n_shots, sig in [(1000, 0.05), (5000, 0.01), (10000, 0.01), (10000, 0.001)]:
res = enc.verify_equivariance_statistical(x, phi, n_shots=n_shots, significance=sig)
print(f"{n_shots:<10} {sig:<15} {str(res['equivariant']):<15} "
f"{res['p_value']:<15.6f} {res['confidence_level']:.3f}")
Shots Significance Equivariant? P-value Confidence ---------------------------------------------------------------------- 1000 0.05 True 0.587252 0.950 5000 0.01 True 0.725662 0.990 10000 0.01 True 0.555010 0.990 10000 0.001 True 0.906695 0.999
# Statistical verification parameter validation
print("Parameter validation:")
# n_shots < 100 is rejected
try:
enc.verify_equivariance_statistical(x, phi, n_shots=50)
except ValueError as e:
print(f" n_shots=50: ValueError - {e}")
# significance must be in (0, 1)
for sig in [0, 1, -0.1, 1.5]:
try:
enc.verify_equivariance_statistical(x, phi, significance=sig)
except ValueError as e:
print(f" significance={sig}: ValueError - {str(e)[:60]}")
Parameter validation: 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 significance=1: ValueError - significance must be in (0, 1), got 1 significance=-0.1: ValueError - significance must be in (0, 1), got -0.1 significance=1.5: ValueError - significance must be in (0, 1), got 1.5
9.1 Automatic Method Selection¶
# verify_equivariance_auto chooses between exact and statistical based on n_qubits
# n_qubits <= 12: exact, n_qubits > 12: statistical
enc = SO2EquivariantFeatureMap(max_angular_momentum=1) # 2 qubits
x = np.array([0.5, 0.3])
result = enc.verify_equivariance_auto(x, np.pi / 4)
print(f"n_qubits={enc.n_qubits} -> auto selects exact verification")
print(f"Result: {result}")
n_qubits=2 -> auto selects exact verification Result: True
10. Resource Analysis & Gate Counts¶
The encoding provides comprehensive resource analysis for hardware planning.
10.1 Gate Count Breakdown¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
breakdown = enc.gate_count_breakdown()
print("Gate count breakdown (max_m=1):")
for key, value in breakdown.items():
print(f" {key}: {value}")
Gate count breakdown (max_m=1): state_preparation: 3 phase_gates: 2 total_single_qubit: 5 total_two_qubit: 0 total: 5
# Compare gate counts across different angular momenta
print(f"{'max_m':<8} {'qubits':<8} {'state_prep':<12} {'phase':<8} {'total_1q':<10} {'total_2q':<10} {'total'}")
print("-" * 70)
for max_m in [0, 1, 2, 3]:
enc_m = SO2EquivariantFeatureMap(max_angular_momentum=max_m)
bd = enc_m.gate_count_breakdown()
print(f"{max_m:<8} {enc_m.n_qubits:<8} {bd['state_preparation']:<12} "
f"{bd['phase_gates']:<8} {bd['total_single_qubit']:<10} "
f"{bd['total_two_qubit']:<10} {bd['total']}")
max_m qubits state_prep phase total_1q total_2q total ---------------------------------------------------------------------- 0 1 1 1 2 0 2 1 2 3 2 5 0 5 2 3 7 3 10 0 10 3 3 7 3 10 0 10
10.2 Comprehensive Resource Summary¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=2)
summary = enc.resource_summary()
print("=" * 60)
print("SO(2) Equivariant Feature Map - Resource Summary")
print("=" * 60)
print("\n--- Circuit Structure ---")
print(f" Qubits: {summary['n_qubits']}")
print(f" Features: {summary['n_features']}")
print(f" Depth: {summary['depth']}")
print(f" Max angular momentum: {summary['max_angular_momentum']}")
print(f" Radial function: {summary['radial_function']}")
print(f" Radial sigma: {summary['radial_sigma']}")
print("\n--- Symmetry Information ---")
print(f" Symmetry group: {summary['symmetry_group']}")
print(f" Angular momenta: {summary['angular_momenta']}")
print(f" Number of states: {summary['n_angular_states']}")
print("\n--- Gate Counts ---")
for key, value in summary['gate_counts'].items():
print(f" {key}: {value}")
print("\n--- Encoding Characteristics ---")
print(f" Is entangling: {summary['is_entangling']}")
print(f" Simulability: {summary['simulability']}")
print("\n--- Hardware Requirements ---")
hw = summary['hardware_requirements']
print(f" Connectivity: {hw['connectivity']}")
print(f" Native gates: {hw['native_gates']}")
print(f" State preparation: {hw['state_preparation']}")
print("\n--- Verification Cost ---")
vc = summary['verification_cost']
print(f" Exact - memory: {vc['exact']['memory']}")
print(f" Exact - max qubits: {vc['exact']['recommended_max_qubits']}")
print(f" Statistical - memory: {vc['statistical']['memory']}")
print(f" Statistical - shots: {vc['statistical']['default_shots']}")
print("\n--- Verification Methods ---")
for method in summary['verification_methods']:
print(f" - {method}")
============================================================ SO(2) Equivariant Feature Map - Resource Summary ============================================================ --- Circuit Structure --- Qubits: 3 Features: 2 Depth: 10 Max angular momentum: 2 Radial function: gaussian Radial sigma: 1.0 --- Symmetry Information --- Symmetry group: SO(2) Angular momenta: [-2, -1, 0, 1, 2] Number of states: 5 --- Gate Counts --- state_preparation: 7 phase_gates: 3 total_single_qubit: 10 total_two_qubit: 0 total: 10 --- Encoding Characteristics --- Is entangling: True Simulability: not_simulable --- Hardware Requirements --- Connectivity: all-to-all Native gates: ['StatePrep'] State preparation: Requires arbitrary state preparation with 5 angular momentum states --- Verification Cost --- Exact - memory: O(2^3) Exact - max qubits: 12 Statistical - memory: O(n_shots) Statistical - shots: 10000 --- Verification Methods --- - verify_equivariance (exact) - verify_equivariance_statistical (scalable) - verify_equivariance_auto (automatic selection)
10.3 Entanglement Pairs¶
# SO(2) encoding uses state preparation which implicitly creates entanglement
# The explicit entanglement pairs list is empty since it's abstracted by StatePrep
pairs = enc.get_entanglement_pairs()
print(f"Explicit entanglement pairs: {pairs}")
print(f"Is entangling: {enc.properties.is_entangling}")
print("\nNote: State preparation (StatePrep) internally creates entanglement,")
print("but the specific gates are backend-dependent.")
Explicit entanglement pairs: [] Is entangling: True Note: State preparation (StatePrep) internally creates entanglement, but the specific gates are backend-dependent.
11. Capability Protocols¶
The Encoding Atlas uses a layered contract architecture with runtime-checkable protocols.
from encoding_atlas.core.base import BaseEncoding
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
DataDependentResourceAnalyzable,
EntanglementQueryable,
DataTransformable,
)
from encoding_atlas.encodings.equivariant_feature_map import EquivariantFeatureMap
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
print("Protocol support:")
print(f" isinstance(enc, BaseEncoding): {isinstance(enc, BaseEncoding)}")
print(f" isinstance(enc, EquivariantFeatureMap): {isinstance(enc, EquivariantFeatureMap)}")
print(f" isinstance(enc, ResourceAnalyzable): {isinstance(enc, ResourceAnalyzable)}")
print(f" isinstance(enc, EntanglementQueryable): {isinstance(enc, EntanglementQueryable)}")
print(f" isinstance(enc, DataDependentResourceAnalyzable): {isinstance(enc, DataDependentResourceAnalyzable)}")
print(f" isinstance(enc, DataTransformable): {isinstance(enc, DataTransformable)}")
Protocol support: isinstance(enc, BaseEncoding): True isinstance(enc, EquivariantFeatureMap): True isinstance(enc, ResourceAnalyzable): True isinstance(enc, EntanglementQueryable): True isinstance(enc, DataDependentResourceAnalyzable): False isinstance(enc, DataTransformable): False
# Write generic analysis code that checks capabilities
def analyze_encoding(enc):
"""Demonstrate protocol-based capability checking."""
print(f"Encoding: {enc!r}")
print(f" n_features: {enc.n_features}")
print(f" n_qubits: {enc.n_qubits}")
print(f" depth: {enc.depth}")
if isinstance(enc, ResourceAnalyzable):
summary = enc.resource_summary()
print(f" ResourceAnalyzable: YES")
print(f" Total gates: {summary['gate_counts']['total']}")
else:
print(f" ResourceAnalyzable: NO")
if isinstance(enc, EntanglementQueryable):
pairs = enc.get_entanglement_pairs()
print(f" EntanglementQueryable: YES")
print(f" Entanglement pairs: {pairs}")
else:
print(f" EntanglementQueryable: NO")
analyze_encoding(enc)
Encoding: SO2EquivariantFeatureMap(max_angular_momentum=1, radial_function='gaussian', radial_sigma=1.0)
n_features: 2
n_qubits: 2
depth: 6
ResourceAnalyzable: YES
Total gates: 5
EntanglementQueryable: YES
Entanglement pairs: []
12. Radial Functions: Gaussian vs Uniform¶
The radial function $c_m(r)$ controls how the radius is encoded in state amplitudes.
# Compare Gaussian and Uniform radial functions
enc_gauss = SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function="gaussian", radial_sigma=1.0)
enc_uniform = SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function="uniform")
# Test with different radii
radii = [0.0, 0.5, 1.0, 2.0, 5.0]
print("Gaussian radial amplitudes c_m(r):")
print(f"{'r':<8}", end="")
for m in enc_gauss.angular_momenta:
print(f"{'m='+str(m):<10}", end="")
print()
print("-" * 60)
for r in radii:
c = enc_gauss._radial_amplitudes(r)
print(f"{r:<8.1f}", end="")
for m in enc_gauss.angular_momenta:
print(f"{c[m]:<10.4f}", end="")
print()
print("\nUniform radial amplitudes c_m(r):")
print(f"{'r':<8}", end="")
for m in enc_uniform.angular_momenta:
print(f"{'m='+str(m):<10}", end="")
print()
print("-" * 60)
for r in radii:
c = enc_uniform._radial_amplitudes(r)
print(f"{r:<8.1f}", end="")
for m in enc_uniform.angular_momenta:
print(f"{c[m]:<10.4f}", end="")
print()
Gaussian radial amplitudes c_m(r): r m=-2 m=-1 m=0 m=1 m=2 ------------------------------------------------------------ 0.0 0.1017 0.4556 0.7511 0.4556 0.1017 0.5 0.2034 0.5529 0.5529 0.5529 0.2034 1.0 0.3443 0.5676 0.3443 0.5676 0.3443 2.0 0.6026 0.3655 0.0815 0.3655 0.6026 5.0 0.7068 0.0213 0.0002 0.0213 0.7068 Uniform radial amplitudes c_m(r): r m=-2 m=-1 m=0 m=1 m=2 ------------------------------------------------------------ 0.0 0.4472 0.4472 0.4472 0.4472 0.4472 0.5 0.4472 0.4472 0.4472 0.4472 0.4472 1.0 0.4472 0.4472 0.4472 0.4472 0.4472 2.0 0.4472 0.4472 0.4472 0.4472 0.4472 5.0 0.4472 0.4472 0.4472 0.4472 0.4472
# Key insight: Gaussian function peaks at r=|m|, so different radii
# activate different angular momentum modes
# At r=0: mostly m=0 mode
c_r0 = enc_gauss._radial_amplitudes(0.0)
print("Gaussian at r=0 (near origin - activates m=0):")
for m, amp in sorted(c_r0.items()):
bar = '#' * int(abs(amp) * 40)
print(f" m={m:+d}: {amp:.4f} {bar}")
# At r=2: mostly m=+/-2 modes
c_r2 = enc_gauss._radial_amplitudes(2.0)
print("\nGaussian at r=2 (activates m=+/-2):")
for m, amp in sorted(c_r2.items()):
bar = '#' * int(abs(amp) * 40)
print(f" m={m:+d}: {amp:.4f} {bar}")
Gaussian at r=0 (near origin - activates m=0): m=-2: 0.1017 #### m=-1: 0.4556 ################## m=+0: 0.7511 ############################## m=+1: 0.4556 ################## m=+2: 0.1017 #### Gaussian at r=2 (activates m=+/-2): m=-2: 0.6026 ######################## m=-1: 0.3655 ############## m=+0: 0.0815 ### m=+1: 0.3655 ############## m=+2: 0.6026 ########################
# Both radial functions preserve equivariance
x = np.array([0.5, 0.3])
phi = np.pi / 3
eq_gauss = enc_gauss.verify_equivariance(x, phi, atol=1e-6)
eq_uniform = enc_uniform.verify_equivariance(x, phi, atol=1e-6)
print(f"Gaussian equivariant: {eq_gauss}")
print(f"Uniform equivariant: {eq_uniform}")
Gaussian equivariant: True Uniform equivariant: True
# Effect of radial_sigma on the Gaussian radial function
print("Effect of sigma on c_m(r) at r=1.0:")
print(f"{'sigma':<10}", end="")
for m in [-2, -1, 0, 1, 2]:
print(f"{'m='+str(m):<10}", end="")
print()
print("-" * 60)
for sigma in [0.1, 0.5, 1.0, 2.0, 5.0]:
enc_s = SO2EquivariantFeatureMap(max_angular_momentum=2, radial_sigma=sigma)
c = enc_s._radial_amplitudes(1.0)
print(f"{sigma:<10.1f}", end="")
for m in enc_s.angular_momenta:
print(f"{c[m]:<10.4f}", end="")
print()
print("\nSmall sigma -> sharply peaked at r=|m|")
print("Large sigma -> approaches uniform distribution")
Effect of sigma on c_m(r) at r=1.0: sigma m=-2 m=-1 m=0 m=1 m=2 ------------------------------------------------------------ 0.1 0.0000 0.7071 0.0000 0.7071 0.0000 0.5 0.0944 0.6976 0.0944 0.6976 0.0944 1.0 0.3443 0.5676 0.3443 0.5676 0.3443 2.0 0.4238 0.4802 0.4238 0.4802 0.4238 5.0 0.4436 0.4526 0.4436 0.4526 0.4436 Small sigma -> sharply peaked at r=|m| Large sigma -> approaches uniform distribution
13. Varying Angular Momentum¶
The max_angular_momentum parameter controls the encoding's expressiveness and qubit requirements.
# How angular momentum affects the encoding
print(f"{'max_m':<8} {'States':<10} {'Qubits':<10} {'Depth':<10} {'Momenta'}")
print("=" * 70)
for max_m in range(5):
with warnings.catch_warnings():
warnings.simplefilter("ignore") # Suppress large angular momentum warning
e = SO2EquivariantFeatureMap(max_angular_momentum=max_m)
n_states = 2 * max_m + 1
print(f"{max_m:<8} {n_states:<10} {e.n_qubits:<10} {e.depth:<10} {e.angular_momenta}")
Large max_angular_momentum (4) with 4 qubits may have scalability issues due to O(2^n) state preparation
max_m States Qubits Depth Momenta ====================================================================== 0 1 1 2 [0] 1 3 2 6 [-1, 0, 1] 2 5 3 10 [-2, -1, 0, 1, 2] 3 7 3 14 [-3, -2, -1, 0, 1, 2, 3] 4 9 4 18 [-4, -3, -2, -1, 0, 1, 2, 3, 4]
# max_angular_momentum=0: single state (m=0), 1 qubit
# This is a degenerate case: pure radial encoding, no angular information
enc_0 = SO2EquivariantFeatureMap(max_angular_momentum=0)
print(f"max_m=0: {enc_0!r}")
print(f" Qubits: {enc_0.n_qubits}")
print(f" Angular momenta: {enc_0.angular_momenta}")
# With only m=0, the phase factor e^{i*0*theta} = 1 for all theta
# So the state is the same regardless of angle
x1 = np.array([1.0, 0.0]) # theta = 0
x2 = np.array([0.0, 1.0]) # theta = pi/2
state1 = enc_0._encode_state(x1)
state2 = enc_0._encode_state(x2)
# Same radius, different angle -> same state (angle information is lost)
overlap = np.abs(np.vdot(state1, state2))
print(f"\n |<psi(1,0)|psi(0,1)>| = {overlap:.6f}")
print(f" Both points have r=1.0, but different theta")
print(f" With max_m=0, angle information is lost (overlap ≈ 1.0)")
max_m=0: SO2EquivariantFeatureMap(max_angular_momentum=0, radial_function='gaussian', radial_sigma=1.0) Qubits: 1 Angular momenta: [0] |<psi(1,0)|psi(0,1)>| = 1.000000 Both points have r=1.0, but different theta With max_m=0, angle information is lost (overlap ≈ 1.0)
# Higher angular momentum enables finer angular resolution
x1 = np.array([1.0, 0.0]) # theta = 0
x2 = np.array([0.0, 1.0]) # theta = pi/2
x3 = np.array([np.cos(0.1), np.sin(0.1)]) # theta = 0.1 (close to x1)
print("State distinguishability vs angular momentum:")
print(f"{'max_m':<8} {'|<psi(0)|psi(pi/2)>|':<25} {'|<psi(0)|psi(0.1)>|':<25}")
print("-" * 60)
for max_m in range(5):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
e = SO2EquivariantFeatureMap(max_angular_momentum=max_m)
s1 = e._encode_state(x1)
s2 = e._encode_state(x2)
s3 = e._encode_state(x3)
ov_12 = np.abs(np.vdot(s1, s2))
ov_13 = np.abs(np.vdot(s1, s3))
print(f"{max_m:<8} {ov_12:<25.6f} {ov_13:<25.6f}")
print("\nHigher max_m -> better angular discrimination")
State distinguishability vs angular momentum: max_m |<psi(0)|psi(pi/2)>| |<psi(0)|psi(0.1)>| ------------------------------------------------------------ 0 1.000000 1.000000 1 0.155362 0.995780 2 0.118532 0.992055 3 0.117149 0.991627
Large max_angular_momentum (4) with 4 qubits may have scalability issues due to O(2^n) state preparation
4 0.117061 0.991621 Higher max_m -> better angular discrimination
14.1 Origin Point (r=0)¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# At the origin, angle is undefined but the encoding still works
x_origin = np.array([0.0, 0.0])
state = enc._encode_state(x_origin)
norm = np.linalg.norm(state)
print(f"State at origin: {state}")
print(f"Norm: {norm:.10f} (should be 1.0)")
print(f"Valid quantum state: {np.isclose(norm, 1.0)}")
# Circuit generation works
circuit = enc.get_circuit(x_origin, backend="pennylane")
print(f"Circuit generated: {callable(circuit)}")
# Equivariance still holds at origin
is_eq = enc.verify_equivariance(x_origin, np.pi / 4, atol=1e-6)
print(f"Equivariant at origin: {is_eq}")
State at origin: [0.46037111+0.j 0.53671076+0.j 0.53671076+0.j 0.46037111+0.j] Norm: 1.0000000000 (should be 1.0) Valid quantum state: True Circuit generated: True Equivariant at origin: False
14.2 Very Large and Very Small Values¶
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Very small values (near-zero radius)
x_tiny = np.array([1e-10, 1e-10])
state_tiny = enc._encode_state(x_tiny)
print(f"Tiny input {x_tiny}: norm = {np.linalg.norm(state_tiny):.10f}")
# Very large values (large radius)
x_large = np.array([1e6, 1e6])
state_large = enc._encode_state(x_large)
print(f"Large input {x_large}: norm = {np.linalg.norm(state_large):.10f}")
# Extremely large (may trigger radial amplitude fallback)
x_huge = np.array([1e10, 1e15])
state_huge = enc._encode_state(x_huge)
print(f"Huge input {x_huge}: norm = {np.linalg.norm(state_huge):.10f}")
# All produce valid quantum states
for name, s in [("tiny", state_tiny), ("large", state_large), ("huge", state_huge)]:
print(f" {name}: valid state = {np.isclose(np.linalg.norm(s), 1.0)}")
Tiny input [1.e-10 1.e-10]: norm = 1.0000000000 Large input [1000000. 1000000.]: norm = 1.0000000000 Huge input [1.e+10 1.e+15]: norm = 1.0000000000 tiny: valid state = True large: valid state = True huge: valid state = True
# Circuit generation succeeds for extreme values
for x_test in [x_tiny, x_large, x_huge]:
circuit = enc.get_circuit(x_test, backend="pennylane")
print(f"Input {str(x_test):<25} -> circuit generated: {callable(circuit)}")
Input [1.e-10 1.e-10] -> circuit generated: True Input [1000000. 1000000.] -> circuit generated: True Input [1.e+10 1.e+15] -> circuit generated: True
14.3 Negative Coordinates¶
# All quadrants work correctly
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
quadrant_points = [
("Q1 (+,+)", np.array([1.0, 1.0])),
("Q2 (-,+)", np.array([-1.0, 1.0])),
("Q3 (-,-)", np.array([-1.0, -1.0])),
("Q4 (+,-)", np.array([1.0, -1.0])),
]
phi = np.pi / 4
print(f"{'Quadrant':<15} {'Point':<20} {'Norm':<10} {'Equivariant?'}")
print("-" * 60)
for label, pt in quadrant_points:
state = enc._encode_state(pt)
norm = np.linalg.norm(state)
eq = enc.verify_equivariance(pt, phi, atol=1e-6)
print(f"{label:<15} [{pt[0]:+.1f}, {pt[1]:+.1f}]{'':<10} {norm:.6f} {eq}")
Quadrant Point Norm Equivariant? ------------------------------------------------------------ Q1 (+,+) [+1.0, +1.0] 1.000000 True Q2 (-,+) [-1.0, +1.0] 1.000000 True Q3 (-,-) [-1.0, -1.0] 1.000000 True Q4 (+,-) [+1.0, -1.0] 1.000000 True
14.4 Full 2pi Periodicity¶
# Rotation by 2*pi should return to the same state
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
x = np.array([0.5, 0.3])
state_original = enc._encode_state(x)
# Rotate by 2pi
x_2pi = enc.group_action(2 * np.pi, x)
state_2pi = enc._encode_state(x_2pi)
overlap = np.abs(np.vdot(state_original, state_2pi))
print(f"Original point: {x}")
print(f"After 2pi rotation: {x_2pi}")
print(f"State overlap: {overlap:.15f}")
print(f"States match: {np.isclose(overlap, 1.0, atol=1e-10)}")
Original point: [0.5 0.3] After 2pi rotation: [0.5 0.3] State overlap: 1.000000000000000 States match: True
14.5 Batch Processing Edge Cases¶
enc = SO2EquivariantFeatureMap()
# Single sample as 2D array (1, 2) shape
x_2d = np.array([[0.5, 0.3]])
circuit = enc.get_circuit(x_2d, backend="pennylane")
print(f"2D array shape {x_2d.shape}: circuit generated = {callable(circuit)}")
# Batch with single sample via get_circuits
circuits = enc.get_circuits(x_2d, backend="pennylane")
print(f"Batch with 1 sample: {len(circuits)} circuit(s)")
# Cannot pass multi-sample batch to get_circuit
try:
enc.get_circuit(np.array([[0.5, 0.3], [1.0, 0.0]]), backend="pennylane")
except ValueError as e:
print(f"Multi-sample to get_circuit: ValueError - {e}")
2D array shape (1, 2): circuit generated = True Batch with 1 sample: 1 circuit(s) Multi-sample to get_circuit: ValueError - get_circuit requires a single sample, use get_circuits for batches
15. Equality, Hashing & Serialization¶
15.1 Equality and Hashing¶
# Same parameters -> equal
enc1 = SO2EquivariantFeatureMap(max_angular_momentum=1, radial_function="gaussian", radial_sigma=1.0)
enc2 = SO2EquivariantFeatureMap(max_angular_momentum=1, radial_function="gaussian", radial_sigma=1.0)
print(f"Same params: enc1 == enc2 -> {enc1 == enc2}")
print(f"Same hash: hash(enc1) == hash(enc2) -> {hash(enc1) == hash(enc2)}")
# Different parameters -> not equal
enc3 = SO2EquivariantFeatureMap(max_angular_momentum=2)
print(f"Different max_m: enc1 == enc3 -> {enc1 == enc3}")
enc4 = SO2EquivariantFeatureMap(max_angular_momentum=1, radial_function="uniform")
print(f"Different radial_fn: enc1 == enc4 -> {enc1 == enc4}")
Same params: enc1 == enc2 -> True Same hash: hash(enc1) == hash(enc2) -> True Different max_m: enc1 == enc3 -> False Different radial_fn: enc1 == enc4 -> False
# Can be used as dictionary keys and in sets
encoding_set = {enc1, enc2, enc3, enc4}
print(f"Set of 4 encodings (2 equal): {len(encoding_set)} unique encodings")
encoding_dict = {enc1: "config_1", enc3: "config_2"}
print(f"Dict lookup enc2 (== enc1): {encoding_dict.get(enc2)}")
Set of 4 encodings (2 equal): 3 unique encodings Dict lookup enc2 (== enc1): config_1
15.2 Serialization (Pickle)¶
import pickle
enc = SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function="gaussian", radial_sigma=1.5)
# Serialize
data = pickle.dumps(enc)
print(f"Serialized size: {len(data)} bytes")
# Deserialize
enc_restored = pickle.loads(data)
print(f"Restored: {enc_restored!r}")
print(f"Equal to original: {enc == enc_restored}")
# Restored encoding is fully functional
x = np.array([0.5, 0.3])
circuit = enc_restored.get_circuit(x, backend="pennylane")
print(f"Circuit generation after restore: {callable(circuit)}")
# Equivariance still works
eq = enc_restored.verify_equivariance(x, np.pi / 4, atol=1e-6)
print(f"Equivariance after restore: {eq}")
Serialized size: 1258 bytes Restored: SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function='gaussian', radial_sigma=1.5) Equal to original: True Circuit generation after restore: True Equivariance after restore: True
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Concurrent circuit generation from multiple threads
results = {}
errors = []
def generate_circuit(idx):
"""Generate circuit in a thread."""
x = np.random.randn(2)
circuit = enc.get_circuit(x, backend="pennylane")
return idx, callable(circuit)
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {executor.submit(generate_circuit, i): i for i in range(20)}
for future in as_completed(futures):
try:
idx, success = future.result()
results[idx] = success
except Exception as e:
errors.append(str(e))
print(f"Concurrent circuit generation: {len(results)} successful, {len(errors)} errors")
print(f"All circuits valid: {all(results.values())}")
Concurrent circuit generation: 20 successful, 0 errors All circuits valid: True
# Thread-safe properties access (lazy initialization with double-checked locking)
enc_new = SO2EquivariantFeatureMap(max_angular_momentum=1)
props_results = []
def access_properties(idx):
"""Access properties from a thread."""
p = enc_new.properties
return p.n_qubits
with ThreadPoolExecutor(max_workers=8) as executor:
futures = [executor.submit(access_properties, i) for i in range(20)]
for f in as_completed(futures):
props_results.append(f.result())
print(f"Concurrent properties access: all returned n_qubits={props_results[0]}")
print(f"All consistent: {len(set(props_results)) == 1}")
Concurrent properties access: all returned n_qubits=2 All consistent: True
17. Comparison with Other Encodings¶
Let's compare the SO(2) equivariant encoding with other encodings in the library.
from encoding_atlas import AngleEncoding, AmplitudeEncoding, IQPEncoding
# All configured for 2 features for fair comparison
encodings = {
"SO2 Equivariant (m=1)": SO2EquivariantFeatureMap(max_angular_momentum=1),
"SO2 Equivariant (m=2)": SO2EquivariantFeatureMap(max_angular_momentum=2),
"Angle (RY)": AngleEncoding(n_features=2, rotation="Y"),
"Amplitude": AmplitudeEncoding(n_features=2),
"IQP": IQPEncoding(n_features=2, reps=1),
}
print(f"{'Encoding':<25} {'Qubits':<8} {'Depth':<8} {'Gates':<8} {'Entangling?':<12} {'Simulability'}")
print("=" * 80)
for name, enc in encodings.items():
props = enc.properties
print(f"{name:<25} {props.n_qubits:<8} {props.depth:<8} {props.gate_count:<8} "
f"{str(props.is_entangling):<12} {props.simulability}")
Encoding Qubits Depth Gates Entangling? Simulability ================================================================================ SO2 Equivariant (m=1) 2 6 5 True not_simulable SO2 Equivariant (m=2) 3 10 10 True not_simulable Angle (RY) 2 1 2 False simulable Amplitude 1 2 2 True not_simulable IQP 2 3 7 True not_simulable
# What makes SO(2) encoding special: it preserves rotational symmetry
enc_so2 = SO2EquivariantFeatureMap(max_angular_momentum=1)
enc_angle = AngleEncoding(n_features=2, rotation="Y")
x = np.array([1.0, 0.0])
phi = np.pi / 4 # 45-degree rotation
# SO2 encoding is rotation-equivariant by construction
print(f"SO2 equivariant at phi=pi/4: {enc_so2.verify_equivariance(x, phi, atol=1e-6)}")
# AngleEncoding does NOT have rotation equivariance
# (it encodes features independently as RY rotations)
print(f"\nAngleEncoding does not have a group action or unitary representation")
print(f"because it is not designed for rotational symmetry.")
SO2 equivariant at phi=pi/4: True AngleEncoding does not have a group action or unitary representation because it is not designed for rotational symmetry.
18. Practical Application: Rotation-Invariant Classification¶
A practical demonstration showing how SO(2) equivariance is useful for rotation-invariant tasks.
# Scenario: Classify points as "near" or "far" from the origin
# This classification should be rotation-invariant!
enc = SO2EquivariantFeatureMap(max_angular_momentum=1)
# Generate some 2D data
np.random.seed(42)
n_samples = 10
# "Near" points (r < 1) and "far" points (r > 2)
near_points = 0.5 * np.random.randn(n_samples, 2)
far_points = 3.0 + 0.5 * np.random.randn(n_samples, 2)
# Encode both sets
print("Encoding demonstration:")
print(f"Near points (r ≈ 0.5): radius range [{min(np.linalg.norm(near_points, axis=1)):.2f}, "
f"{max(np.linalg.norm(near_points, axis=1)):.2f}]")
print(f"Far points (r ≈ 3.0): radius range [{min(np.linalg.norm(far_points, axis=1)):.2f}, "
f"{max(np.linalg.norm(far_points, axis=1)):.2f}]")
Encoding demonstration: Near points (r ≈ 0.5): radius range [0.17, 0.96] Far points (r ≈ 3.0): radius range [3.70, 4.76]
# Key insight: rotating a "near" point keeps it "near"
# The equivariant encoding reflects this in the quantum state
from encoding_atlas.analysis import compute_fidelity
x_near = np.array([0.3, 0.4]) # r = 0.5
x_far = np.array([3.0, 4.0]) # r = 5.0
phi = np.pi / 3 # arbitrary rotation
# Encode original and rotated versions
state_near = enc._encode_state(x_near)
state_near_rot = enc._encode_state(enc.group_action(phi, x_near))
state_far = enc._encode_state(x_far)
state_far_rot = enc._encode_state(enc.group_action(phi, x_far))
# Fidelity between same class (rotated) should be high
fid_near = compute_fidelity(state_near, state_near_rot)
fid_far = compute_fidelity(state_far, state_far_rot)
# Fidelity between different classes should be lower
fid_cross = compute_fidelity(state_near, state_far)
print(f"Fidelity (near original vs near rotated): {fid_near:.6f}")
print(f"Fidelity (far original vs far rotated): {fid_far:.6f}")
print(f"Fidelity (near vs far): {fid_cross:.6f}")
print(f"\nWith uniform radial function, near/far fidelity would be higher")
print(f"because the Gaussian radial function encodes radius information.")
Fidelity (near original vs near rotated): 0.444444 Fidelity (far original vs far rotated): 0.250031 Fidelity (near vs far): 0.674052 With uniform radial function, near/far fidelity would be higher because the Gaussian radial function encodes radius information.
# Data preprocessing recommendation for SO(2) encoding
print("Data Preprocessing for SO2EquivariantFeatureMap:")
print("="*50)
print()
print("1. Input must be 2D Cartesian coordinates [x, y]")
print("2. No normalization required (encoding handles arbitrary magnitudes)")
print("3. Centering recommended: translate so center of rotation is at origin")
print("4. For purely angular data: use radial_function='uniform'")
print("5. For data where distance matters: use radial_function='gaussian' (default)")
print()
# Example preprocessing
raw_data = np.array([
[5.0, 10.0],
[6.0, 11.0],
[4.5, 9.5],
])
# Center around mean
centered = raw_data - raw_data.mean(axis=0)
print(f"Raw data mean: {raw_data.mean(axis=0)}")
print(f"Centered data mean: {centered.mean(axis=0)}")
# Encode centered data
circuits = enc.get_circuits(centered, backend="pennylane")
print(f"\nGenerated {len(circuits)} circuits from centered data")
Data Preprocessing for SO2EquivariantFeatureMap: ================================================== 1. Input must be 2D Cartesian coordinates [x, y] 2. No normalization required (encoding handles arbitrary magnitudes) 3. Centering recommended: translate so center of rotation is at origin 4. For purely angular data: use radial_function='uniform' 5. For data where distance matters: use radial_function='gaussian' (default) Raw data mean: [ 5.16666667 10.16666667] Centered data mean: [-2.96059473e-16 5.92118946e-16] Generated 3 circuits from centered data
19. Summary¶
Key Takeaways¶
| Feature | Details |
|---|---|
| Symmetry Group | SO(2) - 2D rotations |
| Input | 2D Cartesian coordinates [x, y] |
| Equivariance | $U(\phi)|\psi(x)\rangle = |\psi(\text{rotate}(x, \phi))\rangle$ |
| Parameters | max_angular_momentum, radial_function, radial_sigma |
| Qubits | $\lceil\log_2(2 \cdot \text{max\_m} + 1)\rceil$ |
| Backends | PennyLane, Qiskit, Cirq |
| Verification | Exact (small systems), Statistical (scalable), Auto |
| Thread Safety | Yes (double-checked locking for properties) |
| Serialization | Pickle-compatible |
When to Use SO2EquivariantFeatureMap¶
- Your data has 2D rotational symmetry (point clouds, images, molecular orientations)
- You want provable symmetry preservation in the quantum encoding
- You need rotation-equivariant feature extraction for quantum ML
- You want reduced hypothesis space for better sample efficiency
When NOT to Use¶
- Data has more than 2 features (consider projecting to 2D first)
- No rotational symmetry in the problem
- Need for approximate symmetry handling (this encoding is exact)
# Final comprehensive test: all features working together
print("Final Verification: All Features")
print("=" * 40)
enc = SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function="gaussian", radial_sigma=1.5)
# 1. Basic info
print(f"1. Encoding: {enc!r}")
print(f" Qubits: {enc.n_qubits}, Depth: {enc.depth}")
# 2. Properties
p = enc.properties
print(f"2. Properties: gates={p.gate_count}, entangling={p.is_entangling}")
# 3. Circuit generation
x = np.array([0.5, 0.3])
circ = enc.get_circuit(x, backend="pennylane")
print(f"3. Circuit: {callable(circ)}")
# 4. Equivariance
eq = enc.verify_equivariance(x, np.pi/4, atol=1e-6)
print(f"4. Equivariance at pi/4: {eq}")
# 5. All generators
eq_gens = enc.verify_equivariance_on_generators(x, atol=1e-6)
print(f"5. Equivariant on all generators: {eq_gens}")
# 6. Resource summary
summary = enc.resource_summary()
print(f"6. Symmetry: {summary['symmetry_group']}, States: {summary['n_angular_states']}")
# 7. Serialization
restored = pickle.loads(pickle.dumps(enc))
print(f"7. Pickle roundtrip: {enc == restored}")
print("\nAll features verified successfully!")
Final Verification: All Features ======================================== 1. Encoding: SO2EquivariantFeatureMap(max_angular_momentum=2, radial_function='gaussian', radial_sigma=1.5) Qubits: 3, Depth: 10 2. Properties: gates=10, entangling=True 3. Circuit: True 4. Equivariance at pi/4: True 5. Equivariant on all generators: True 6. Symmetry: SO(2), States: 5 7. Pickle roundtrip: True All features verified successfully!