Hamiltonian Encoding — Comprehensive Usage Guide¶
Library: encoding-atlas
Encoding: HamiltonianEncoding
Version: 0.2.0
What is Hamiltonian Encoding?¶
Hamiltonian encoding maps classical data into quantum states through time evolution under a data-dependent Hamiltonian operator:
$$|\psi(x)\rangle = e^{-iH(x)t} |+\rangle^{\otimes n}$$
where:
- $|+\rangle = H|0\rangle = \frac{1}{\sqrt{2}}(|0\rangle + |1\rangle)$ is the equal superposition state
- $H(x)$ is a data-dependent Hamiltonian
- $t$ is the evolution time
- $n$ is the number of qubits (one per feature)
The time evolution is approximated using Trotterization:
$$e^{-iH(x)t} \approx \left[\prod_j e^{-ic_j(x)\frac{t}{r}P_j}\right]^r$$
where $r$ is the number of Trotter steps (reps).
Why Hamiltonian Encoding?¶
- Physics-motivated design: Grounded in quantum dynamics, providing natural inductive bias for physics-related ML tasks
- Flexible Hamiltonian types: Choose from IQP, XY, Heisenberg, or Pauli-Z interaction structures
- Rich entanglement: Creates highly entangled quantum states via configurable two-qubit interactions
- Expressivity control: The
evolution_timeparameter provides explicit control over encoding strength - Not classically simulable: Potential for quantum advantage in kernel-based methods
Table of Contents¶
- Installation & Setup
- Basic Instantiation & Defaults
- Hamiltonian Types Deep Dive
- Entanglement Topologies
- Circuit Generation — Multi-Backend Support
- Batch Processing & Memory-Efficient Iteration
- Configuration Parameters In Depth
- Properties & Resource Analysis
- Gate Count Analysis (Upper-Bound vs Exact)
- Capability Protocols
- Registry System
- Serialization & Deep Copy
- Equality, Hashing & Collections
- Edge Cases & Robustness
- Input Validation & Error Handling
- Critical Point at π & Data Preprocessing
- Rotation Angle Mathematics
- Depth Computation & Parallelization
- Statevector Simulation & Verification
- Quantum Kernel Computation
- Comparing Hamiltonian Types Empirically
- Scaling & Performance Considerations
- Summary & Quick Reference
1. Installation & Setup¶
# Install the library (uncomment if not already installed)
# !pip install encoding-atlas
# Optional backends (Qiskit and Cirq are optional)
# !pip install encoding-atlas[all]
import numpy as np
import warnings
# Core imports
from encoding_atlas import HamiltonianEncoding, EncodingProperties
from encoding_atlas import list_encodings, get_encoding
from encoding_atlas.core.protocols import (
ResourceAnalyzable,
EntanglementQueryable,
DataDependentResourceAnalyzable,
DataTransformable,
)
# Check PennyLane availability (primary backend)
try:
import pennylane as qml
HAS_PENNYLANE = True
print(f"PennyLane version: {qml.__version__}")
except ImportError:
HAS_PENNYLANE = False
print("PennyLane not available")
# Check Qiskit availability
try:
import qiskit
from qiskit import QuantumCircuit
HAS_QISKIT = True
print(f"Qiskit version: {qiskit.__version__}")
except ImportError:
HAS_QISKIT = False
print("Qiskit not available (install with: pip install qiskit)")
# Check Cirq availability
try:
import cirq
HAS_CIRQ = True
print(f"Cirq version: {cirq.__version__}")
except ImportError:
HAS_CIRQ = False
print("Cirq not available (install with: pip install cirq-core)")
print(f"\nNumPy version: {np.__version__}")
from encoding_atlas import __version__
print(f"encoding-atlas version: {__version__}")
PennyLane version: 0.42.3 Qiskit version: 2.3.0 Cirq version: 1.5.0 NumPy version: 2.2.6 encoding-atlas version: 0.2.0
2. Basic Instantiation & Defaults¶
The simplest way to create a Hamiltonian encoding is to specify only n_features. All other parameters have sensible defaults.
# Minimal instantiation — uses all defaults
enc = HamiltonianEncoding(n_features=4)
print("=== Default Configuration ===")
print(f"repr: {enc!r}")
print(f"n_features: {enc.n_features}")
print(f"n_qubits: {enc.n_qubits}")
print(f"hamiltonian_type: {enc.hamiltonian_type}")
print(f"evolution_time: {enc.evolution_time}")
print(f"reps (Trotter steps): {enc.reps}")
print(f"entanglement: {enc.entanglement}")
print(f"insert_barriers: {enc.insert_barriers}")
print(f"max_pairs: {enc.max_pairs}")
print(f"include_single_qubit_terms:{enc.include_single_qubit_terms}")
print(f"depth: {enc.depth}")
=== Default Configuration === repr: HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', evolution_time=1.0, reps=2, entanglement='full', insert_barriers=True) n_features: 4 n_qubits: 4 hamiltonian_type: iqp evolution_time: 1.0 reps (Trotter steps): 2 entanglement: full insert_barriers: True max_pairs: None include_single_qubit_terms:True depth: 21
# Access the config dictionary (read-only copy)
config = enc.config
print("Configuration dict:")
for key, value in sorted(config.items()):
print(f" {key}: {value}")
Configuration dict: entanglement: full evolution_time: 1.0 hamiltonian_type: iqp include_single_qubit_terms: True insert_barriers: True max_pairs: None reps: 2
3. Hamiltonian Types Deep Dive¶
The library supports four Hamiltonian types, each defining a different interaction structure:
| Type | Hamiltonian | Interactions | Gate Overhead |
|---|---|---|---|
'iqp' |
$\sum_i x_i Z_i + \sum_{ij} (\pi - x_i)(\pi - x_j) Z_i Z_j$ | ZZ only | Lowest |
'pauli_z' |
Same as IQP | ZZ only | Lowest |
'xy' |
$\sum_{ij} (\pi - x_i)(\pi - x_j)(X_iX_j + Y_iY_j)$ | XX + YY | Medium |
'heisenberg' |
$\sum_{ij} (\pi - x_i)(\pi - x_j)(X_iX_j + Y_iY_j + Z_iZ_j)$ | XX + YY + ZZ | Highest |
# Create one encoding of each type
types = ['iqp', 'pauli_z', 'xy', 'heisenberg']
print(f"{'Type':<14} {'Depth':>6} {'Total Gates':>12} {'CNOTs':>6} {'Entangling':>11} {'Simulability':>15}")
print("-" * 70)
for htype in types:
enc_t = HamiltonianEncoding(n_features=4, hamiltonian_type=htype, reps=2)
props = enc_t.properties
breakdown = enc_t.gate_count_breakdown()
print(
f"{htype:<14} {props.depth:>6} {props.gate_count:>12} "
f"{breakdown['cnot']:>6} {str(props.is_entangling):>11} "
f"{props.simulability:>15}"
)
Type Depth Total Gates CNOTs Entangling Simulability ---------------------------------------------------------------------- iqp 21 48 24 True not_simulable pauli_z 21 48 24 True not_simulable xy 75 108 48 True not_simulable heisenberg 93 132 72 True not_simulable
# Case-insensitive type names
enc_upper = HamiltonianEncoding(n_features=3, hamiltonian_type='IQP')
enc_mixed = HamiltonianEncoding(n_features=3, hamiltonian_type='Heisenberg')
print(f"IQP (case-insensitive): {enc_upper.hamiltonian_type}")
print(f"Heisenberg (case-insensitive): {enc_mixed.hamiltonian_type}")
IQP (case-insensitive): iqp Heisenberg (case-insensitive): heisenberg
3.1 IQP / Pauli-Z Type¶
The IQP (Instantaneous Quantum Polynomial) type uses diagonal Hamiltonians with Z and ZZ terms. It is the default type.
Gate decomposition per ZZ interaction:
- CNOT → RZ → CNOT (depth 3, 2 CNOTs + 1 RZ)
pauli_z is functionally identical to iqp but provides a more explicit name.
enc_iqp = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', reps=1)
enc_pz = HamiltonianEncoding(n_features=4, hamiltonian_type='pauli_z', reps=1)
breakdown_iqp = enc_iqp.gate_count_breakdown()
breakdown_pz = enc_pz.gate_count_breakdown()
print("IQP gate breakdown:", dict(breakdown_iqp))
print("Pauli-Z gate breakdown:", dict(breakdown_pz))
print(f"\nIQP and Pauli-Z are identical: {breakdown_iqp == breakdown_pz}")
IQP gate breakdown: {'hadamard': 4, 'rz': 10, 's_gates': 0, 'cnot': 12, 'total_single_qubit': 14, 'total_two_qubit': 12, 'total': 26}
Pauli-Z gate breakdown: {'hadamard': 4, 'rz': 10, 's_gates': 0, 'cnot': 12, 'total_single_qubit': 14, 'total_two_qubit': 12, 'total': 26}
IQP and Pauli-Z are identical: True
3.2 XY Type¶
The XY model uses XX + YY interactions, creating coherent superpositions.
Gate decompositions:
- XX: H⊗H → CNOT → RZ → CNOT → H⊗H (depth 5)
- YY: Sdg⊗Sdg → H⊗H → CNOT → RZ → CNOT → H⊗H → S⊗S (depth 7)
The include_single_qubit_terms parameter controls whether $\sum_i x_i Z_i$ terms are added.
# XY with and without single-qubit terms
enc_xy_with = HamiltonianEncoding(n_features=4, hamiltonian_type='xy', reps=1, include_single_qubit_terms=True)
enc_xy_without = HamiltonianEncoding(n_features=4, hamiltonian_type='xy', reps=1, include_single_qubit_terms=False)
bd_with = enc_xy_with.gate_count_breakdown()
bd_without = enc_xy_without.gate_count_breakdown()
print("XY with single-qubit terms:")
print(f" Total gates: {bd_with['total']}, RZ: {bd_with['rz']}, S gates: {bd_with['s_gates']}, CNOT: {bd_with['cnot']}")
print(f" Depth: {enc_xy_with.depth}")
print("\nXY without single-qubit terms:")
print(f" Total gates: {bd_without['total']}, RZ: {bd_without['rz']}, S gates: {bd_without['s_gates']}, CNOT: {bd_without['cnot']}")
print(f" Depth: {enc_xy_without.depth}")
print(f"\nDifference in RZ gates: {bd_with['rz'] - bd_without['rz']} (= n_features = {enc_xy_with.n_features})")
XY with single-qubit terms: Total gates: 116, RZ: 16, S gates: 24, CNOT: 24 Depth: 38 XY without single-qubit terms: Total gates: 112, RZ: 12, S gates: 24, CNOT: 24 Depth: 37 Difference in RZ gates: 4 (= n_features = 4)
3.3 Heisenberg Type¶
The most expressive type: combines XX + YY + ZZ interactions. This corresponds to the full isotropic Heisenberg model.
enc_heisen = HamiltonianEncoding(n_features=4, hamiltonian_type='heisenberg', reps=1)
bd_heisen = enc_heisen.gate_count_breakdown()
print("Heisenberg gate breakdown:")
print(f" Hadamard: {bd_heisen['hadamard']}")
print(f" RZ: {bd_heisen['rz']}")
print(f" S gates: {bd_heisen['s_gates']}")
print(f" CNOT: {bd_heisen['cnot']}")
print(f" Total: {bd_heisen['total']}")
print(f" Depth: {enc_heisen.depth}")
n_pairs = len(enc_heisen.get_entanglement_pairs())
print(f"\nExpected CNOTs = 6 * n_pairs * reps = 6 * {n_pairs} * 1 = {6 * n_pairs}")
print(f"Actual CNOTs: {bd_heisen['cnot']}")
Heisenberg gate breakdown: Hadamard: 52 RZ: 22 S gates: 24 CNOT: 36 Total: 134 Depth: 47 Expected CNOTs = 6 * n_pairs * reps = 6 * 6 * 1 = 36 Actual CNOTs: 36
4. Entanglement Topologies¶
Three entanglement patterns are supported:
| Topology | Pairs | Scaling | Hardware | Best For |
|---|---|---|---|---|
'full' |
All (i,j) where i<j | O(n²) | All-to-all | Max expressivity |
'linear' |
(i, i+1) only | O(n) | Nearest-neighbor | NISQ devices |
'circular' |
Linear + (n-1, 0) | O(n) | Ring | Periodic problems |
n = 5
for topology in ['full', 'linear', 'circular']:
enc_t = HamiltonianEncoding(n_features=n, entanglement=topology, reps=1)
pairs = enc_t.get_entanglement_pairs()
print(f"\n{topology.upper()} entanglement (n={n}):")
print(f" Pairs ({len(pairs)}): {pairs}")
print(f" Depth: {enc_t.depth}")
print(f" CNOTs: {enc_t.gate_count_breakdown()['cnot']}")
FULL entanglement (n=5): Pairs (10): [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] Depth: 17 CNOTs: 20 LINEAR entanglement (n=5): Pairs (4): [(0, 1), (1, 2), (2, 3), (3, 4)] Depth: 8 CNOTs: 8 CIRCULAR entanglement (n=5): Pairs (5): [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] Depth: 11 CNOTs: 10
# Demonstrate pair count formulas
for n in [2, 3, 4, 5, 8, 10]:
enc_full = HamiltonianEncoding(n_features=n, entanglement='full', reps=1)
enc_lin = HamiltonianEncoding(n_features=n, entanglement='linear', reps=1)
enc_circ = HamiltonianEncoding(n_features=n, entanglement='circular', reps=1)
n_full = len(enc_full.get_entanglement_pairs())
n_lin = len(enc_lin.get_entanglement_pairs())
n_circ = len(enc_circ.get_entanglement_pairs())
expected_full = n * (n - 1) // 2
expected_lin = n - 1
expected_circ = n if n > 2 else n - 1
print(
f"n={n:>2}: full={n_full:>3} (exp={expected_full:>3}), "
f"linear={n_lin:>2} (exp={expected_lin:>2}), "
f"circular={n_circ:>2} (exp={expected_circ:>2})"
)
assert n_full == expected_full
assert n_lin == expected_lin
assert n_circ == expected_circ
print("\nAll pair count formulas verified!")
n= 2: full= 1 (exp= 1), linear= 1 (exp= 1), circular= 1 (exp= 1) n= 3: full= 3 (exp= 3), linear= 2 (exp= 2), circular= 3 (exp= 3) n= 4: full= 6 (exp= 6), linear= 3 (exp= 3), circular= 4 (exp= 4) n= 5: full= 10 (exp= 10), linear= 4 (exp= 4), circular= 5 (exp= 5) n= 8: full= 28 (exp= 28), linear= 7 (exp= 7), circular= 8 (exp= 8) n=10: full= 45 (exp= 45), linear= 9 (exp= 9), circular=10 (exp=10) All pair count formulas verified!
# Case-insensitive entanglement names
enc_ci = HamiltonianEncoding(n_features=3, entanglement='FULL')
print(f"Entanglement (from 'FULL'): {enc_ci.entanglement}")
enc_ci2 = HamiltonianEncoding(n_features=3, entanglement='Linear')
print(f"Entanglement (from 'Linear'): {enc_ci2.entanglement}")
Entanglement (from 'FULL'): full Entanglement (from 'Linear'): linear
5. Circuit Generation — Multi-Backend Support¶
Hamiltonian encoding supports three quantum computing backends:
- PennyLane (default): Returns a callable closure
- Qiskit: Returns a
QuantumCircuitobject - Cirq: Returns a
cirq.Circuitobject
enc = HamiltonianEncoding(n_features=3, hamiltonian_type='iqp', reps=1)
x = np.array([0.5, 0.3, 0.7])
5.1 PennyLane Backend¶
if HAS_PENNYLANE:
circuit_fn = enc.get_circuit(x, backend='pennylane')
print(f"Type: {type(circuit_fn)}")
print(f"Callable: {callable(circuit_fn)}")
# Use in a PennyLane QNode to get the statevector
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"\nStatevector (first 4 amplitudes): {state[:4]}")
print(f"State norm: {np.linalg.norm(state):.10f}")
print(f"Number of amplitudes: {len(state)} (= 2^{enc.n_qubits})")
else:
print("PennyLane not available, skipping.")
Type: <class 'function'> Callable: True Statevector (first 4 amplitudes): [-0.32524544+0.13861963j 0.30996488-0.17006403j 0.24339948+0.25643068j 0.11656124+0.33378657j] State norm: 1.0000000000 Number of amplitudes: 8 (= 2^3)
5.2 Qiskit Backend¶
if HAS_QISKIT:
qc = enc.get_circuit(x, backend='qiskit')
print(f"Type: {type(qc)}")
print(f"Qiskit circuit depth: {qc.depth()}")
print(f"Qiskit gate count: {qc.size()}")
print(f"\nCircuit diagram:")
print(qc.draw(output='text'))
else:
print("Qiskit not available, skipping.")
Type: <class 'qiskit.circuit.quantumcircuit.QuantumCircuit'>
Qiskit circuit depth: 11
Qiskit gate count: 15
Circuit diagram:
┌───┐ ┌───────┐ »
q_0: ┤ H ├─┤ Rz(1) ├───■──────────────────■────■──────────────────■───────»
├───┤┌┴───────┴┐┌─┴─┐┌────────────┐┌─┴─┐ │ │ »
q_1: ┤ H ├┤ Rz(0.6) ├┤ X ├┤ Rz(15.013) ├┤ X ├──┼──────────────────┼────■──»
├───┤├─────────┤└───┘└────────────┘└───┘┌─┴─┐┌────────────┐┌─┴─┐┌─┴─┐»
q_2: ┤ H ├┤ Rz(1.4) ├────────────────────────┤ X ├┤ Rz(12.899) ├┤ X ├┤ X ├»
└───┘└─────────┘ └───┘└────────────┘└───┘└───┘»
«
«q_0: ───────────────────
«
«q_1: ────────────────■──
« ┌────────────┐┌─┴─┐
«q_2: ┤ Rz(13.876) ├┤ X ├
« └────────────┘└───┘
# Qiskit with barriers (insert_barriers=True is the default)
if HAS_QISKIT:
enc_barrier = HamiltonianEncoding(n_features=3, reps=2, insert_barriers=True)
qc_barrier = enc_barrier.get_circuit(x, backend='qiskit')
print("Circuit with barriers between Trotter steps:")
print(qc_barrier.draw(output='text'))
enc_no_barrier = HamiltonianEncoding(n_features=3, reps=2, insert_barriers=False)
qc_no_barrier = enc_no_barrier.get_circuit(x, backend='qiskit')
print("\nCircuit without barriers:")
print(qc_no_barrier.draw(output='text'))
Circuit with barriers between Trotter steps:
┌───┐┌─────────┐ »
q_0: ┤ H ├┤ Rz(0.5) ├──■──────────────────■────■──────────────────■───────»
├───┤├─────────┤┌─┴─┐┌────────────┐┌─┴─┐ │ │ »
q_1: ┤ H ├┤ Rz(0.3) ├┤ X ├┤ Rz(7.5063) ├┤ X ├──┼──────────────────┼────■──»
├───┤├─────────┤└───┘└────────────┘└───┘┌─┴─┐┌────────────┐┌─┴─┐┌─┴─┐»
q_2: ┤ H ├┤ Rz(0.7) ├────────────────────────┤ X ├┤ Rz(6.4497) ├┤ X ├┤ X ├»
└───┘└─────────┘ └───┘└────────────┘└───┘└───┘»
« ░ ┌─────────┐ »
«q_0: ───────────────────░─┤ Rz(0.5) ├──■──────────────────■────■──»
« ░ ├─────────┤┌─┴─┐┌────────────┐┌─┴─┐ │ »
«q_1: ───────────────■───░─┤ Rz(0.3) ├┤ X ├┤ Rz(7.5063) ├┤ X ├──┼──»
« ┌───────────┐┌─┴─┐ ░ ├─────────┤└───┘└────────────┘└───┘┌─┴─┐»
«q_2: ┤ Rz(6.938) ├┤ X ├─░─┤ Rz(0.7) ├────────────────────────┤ X ├»
« └───────────┘└───┘ ░ └─────────┘ └───┘»
«
«q_0: ────────────────■─────────────────────────
« │
«q_1: ────────────────┼────■─────────────────■──
« ┌────────────┐┌─┴─┐┌─┴─┐┌───────────┐┌─┴─┐
«q_2: ┤ Rz(6.4497) ├┤ X ├┤ X ├┤ Rz(6.938) ├┤ X ├
« └────────────┘└───┘└───┘└───────────┘└───┘
Circuit without barriers:
┌───┐┌─────────┐ »
q_0: ┤ H ├┤ Rz(0.5) ├──■──────────────────■────■──────────────────■──»
├───┤├─────────┤┌─┴─┐┌────────────┐┌─┴─┐ │ │ »
q_1: ┤ H ├┤ Rz(0.3) ├┤ X ├┤ Rz(7.5063) ├┤ X ├──┼──────────────────┼──»
├───┤├─────────┤└───┘└────────────┘└───┘┌─┴─┐┌────────────┐┌─┴─┐»
q_2: ┤ H ├┤ Rz(0.7) ├────────────────────────┤ X ├┤ Rz(6.4497) ├┤ X ├»
└───┘└─────────┘ └───┘└────────────┘└───┘»
« ┌─────────┐ »
«q_0: ┤ Rz(0.5) ├───────────────────────────────■──────────────────■────■──»
« └─────────┘ ┌─────────┐┌─┴─┐┌────────────┐┌─┴─┐ │ »
«q_1: ─────■────────────────────■──┤ Rz(0.3) ├┤ X ├┤ Rz(7.5063) ├┤ X ├──┼──»
« ┌─┴─┐ ┌───────────┐┌─┴─┐├─────────┤└───┘└────────────┘└───┘┌─┴─┐»
«q_2: ───┤ X ├───┤ Rz(6.938) ├┤ X ├┤ Rz(0.7) ├────────────────────────┤ X ├»
« └───┘ └───────────┘└───┘└─────────┘ └───┘»
«
«q_0: ────────────────■─────────────────────────
« │
«q_1: ────────────────┼────■─────────────────■──
« ┌────────────┐┌─┴─┐┌─┴─┐┌───────────┐┌─┴─┐
«q_2: ┤ Rz(6.4497) ├┤ X ├┤ X ├┤ Rz(6.938) ├┤ X ├
« └────────────┘└───┘└───┘└───────────┘└───┘
5.3 Cirq Backend¶
if HAS_CIRQ:
cirq_circuit = enc.get_circuit(x, backend='cirq')
print(f"Type: {type(cirq_circuit)}")
print(f"\nCirq circuit:")
print(cirq_circuit)
else:
print("Cirq not available, skipping.")
Type: <class 'cirq.circuits.circuit.Circuit'>
Cirq circuit:
0: ───H───Rz(0.318π)───@────────────────@───@────────────────@────────────────────────
│ │ │ │
1: ───H───Rz(0.191π)───X───Rz(0.779π)───X───┼────────────────┼───@────────────────@───
│ │ │ │
2: ───H───Rz(0.446π)────────────────────────X───Rz(0.106π)───X───X───Rz(0.417π)───X───
5.4 Visualizing All Four Hamiltonian Types as Qiskit Circuits¶
if HAS_QISKIT:
x_demo = np.array([0.5, 0.3, 0.7])
for htype in ['iqp', 'xy', 'heisenberg', 'pauli_z']:
enc_demo = HamiltonianEncoding(
n_features=3, hamiltonian_type=htype, reps=1, insert_barriers=False
)
qc_demo = enc_demo.get_circuit(x_demo, backend='qiskit')
print(f"\n{'=' * 60}")
print(f"Hamiltonian Type: {htype.upper()}")
print(f"Depth: {qc_demo.depth()}, Gates: {qc_demo.size()}")
print(f"{'=' * 60}")
print(qc_demo.draw(output='text'))
============================================================
Hamiltonian Type: IQP
Depth: 11, Gates: 15
============================================================
┌───┐ ┌───────┐ »
q_0: ┤ H ├─┤ Rz(1) ├───■──────────────────■────■──────────────────■───────»
├───┤┌┴───────┴┐┌─┴─┐┌────────────┐┌─┴─┐ │ │ »
q_1: ┤ H ├┤ Rz(0.6) ├┤ X ├┤ Rz(15.013) ├┤ X ├──┼──────────────────┼────■──»
├───┤├─────────┤└───┘└────────────┘└───┘┌─┴─┐┌────────────┐┌─┴─┐┌─┴─┐»
q_2: ┤ H ├┤ Rz(1.4) ├────────────────────────┤ X ├┤ Rz(12.899) ├┤ X ├┤ X ├»
└───┘└─────────┘ └───┘└────────────┘└───┘└───┘»
«
«q_0: ───────────────────
«
«q_1: ────────────────■──
« ┌────────────┐┌─┴─┐
«q_2: ┤ Rz(13.876) ├┤ X ├
« └────────────┘└───┘
============================================================
Hamiltonian Type: XY
Depth: 38, Gates: 60
============================================================
┌───┐ ┌───────┐ ┌───┐ ┌───┐┌─────┐┌───┐ »
q_0: ┤ H ├─┤ Rz(1) ├─┤ H ├──■──────────────────■──┤ H ├┤ Sdg ├┤ H ├──■──»
├───┤┌┴───────┴┐├───┤┌─┴─┐┌────────────┐┌─┴─┐├───┤├─────┤├───┤┌─┴─┐»
q_1: ┤ H ├┤ Rz(0.6) ├┤ H ├┤ X ├┤ Rz(15.013) ├┤ X ├┤ H ├┤ Sdg ├┤ H ├┤ X ├»
├───┤├─────────┤├───┤└───┘└────────────┘└───┘└───┘└─────┘└───┘└───┘»
q_2: ┤ H ├┤ Rz(1.4) ├┤ H ├──────────────────────────────────────────────»
└───┘└─────────┘└───┘ »
« ┌───┐┌───┐┌───┐ ┌───┐┌─────┐»
«q_0: ────────────────■──┤ H ├┤ S ├┤ H ├──■──────────────────■──┤ H ├┤ Sdg ├»
« ┌────────────┐┌─┴─┐├───┤├───┤├───┤ │ │ └───┘└─────┘»
«q_1: ┤ Rz(15.013) ├┤ X ├┤ H ├┤ S ├┤ H ├──┼──────────────────┼──────────────»
« └────────────┘└───┘└───┘└───┘└───┘┌─┴─┐┌────────────┐┌─┴─┐┌───┐┌─────┐»
«q_2: ──────────────────────────────────┤ X ├┤ Rz(12.899) ├┤ X ├┤ H ├┤ Sdg ├»
« └───┘└────────────┘└───┘└───┘└─────┘»
« ┌───┐ ┌───┐┌───┐ »
«q_0: ┤ H ├──■──────────────────■──┤ H ├┤ S ├──────────────────────────────────»
« └───┘ │ │ └───┘└───┘ ┌───┐»
«q_1: ───────┼──────────────────┼───────────────────■──────────────────■──┤ H ├»
« ┌───┐┌─┴─┐┌────────────┐┌─┴─┐┌───┐┌───┐┌───┐┌─┴─┐┌────────────┐┌─┴─┐├───┤»
«q_2: ┤ H ├┤ X ├┤ Rz(12.899) ├┤ X ├┤ H ├┤ S ├┤ H ├┤ X ├┤ Rz(13.876) ├┤ X ├┤ H ├»
« └───┘└───┘└────────────┘└───┘└───┘└───┘└───┘└───┘└────────────┘└───┘└───┘»
«
«q_0: ──────────────────────────────────────────────
« ┌─────┐┌───┐ ┌───┐┌───┐
«q_1: ┤ Sdg ├┤ H ├──■──────────────────■──┤ H ├┤ S ├
« ├─────┤├───┤┌─┴─┐┌────────────┐┌─┴─┐├───┤├───┤
«q_2: ┤ Sdg ├┤ H ├┤ X ├┤ Rz(13.876) ├┤ X ├┤ H ├┤ S ├
« └─────┘└───┘└───┘└────────────┘└───┘└───┘└───┘
============================================================
Hamiltonian Type: HEISENBERG
Depth: 47, Gates: 69
============================================================
┌───┐ ┌───────┐ ┌───┐ ┌───┐┌─────┐┌───┐ »
q_0: ┤ H ├─┤ Rz(1) ├─┤ H ├──■──────────────────■──┤ H ├┤ Sdg ├┤ H ├──■──»
├───┤┌┴───────┴┐├───┤┌─┴─┐┌────────────┐┌─┴─┐├───┤├─────┤├───┤┌─┴─┐»
q_1: ┤ H ├┤ Rz(0.6) ├┤ H ├┤ X ├┤ Rz(15.013) ├┤ X ├┤ H ├┤ Sdg ├┤ H ├┤ X ├»
├───┤├─────────┤├───┤└───┘└────────────┘└───┘└───┘└─────┘└───┘└───┘»
q_2: ┤ H ├┤ Rz(1.4) ├┤ H ├──────────────────────────────────────────────»
└───┘└─────────┘└───┘ »
« ┌───┐┌───┐ ┌───┐ »
«q_0: ────────────────■──┤ H ├┤ S ├──■──────────────────■──┤ H ├──■──»
« ┌────────────┐┌─┴─┐├───┤├───┤┌─┴─┐┌────────────┐┌─┴─┐├───┤ │ »
«q_1: ┤ Rz(15.013) ├┤ X ├┤ H ├┤ S ├┤ X ├┤ Rz(15.013) ├┤ X ├┤ H ├──┼──»
« └────────────┘└───┘└───┘└───┘└───┘└────────────┘└───┘└───┘┌─┴─┐»
«q_2: ──────────────────────────────────────────────────────────┤ X ├»
« └───┘»
« ┌───┐┌─────┐┌───┐ ┌───┐┌───┐»
«q_0: ────────────────■──┤ H ├┤ Sdg ├┤ H ├──■──────────────────■──┤ H ├┤ S ├»
« │ └───┘└─────┘└───┘ │ │ └───┘└───┘»
«q_1: ────────────────┼─────────────────────┼──────────────────┼────────────»
« ┌────────────┐┌─┴─┐┌───┐┌─────┐┌───┐┌─┴─┐┌────────────┐┌─┴─┐┌───┐┌───┐»
«q_2: ┤ Rz(12.899) ├┤ X ├┤ H ├┤ Sdg ├┤ H ├┤ X ├┤ Rz(12.899) ├┤ X ├┤ H ├┤ S ├»
« └────────────┘└───┘└───┘└─────┘└───┘└───┘└────────────┘└───┘└───┘└───┘»
« »
«q_0: ──■──────────────────■────────────────────────────────────────────────»
« │ │ ┌───┐┌─────┐┌───┐»
«q_1: ──┼──────────────────┼─────────■──────────────────■──┤ H ├┤ Sdg ├┤ H ├»
« ┌─┴─┐┌────────────┐┌─┴─┐┌───┐┌─┴─┐┌────────────┐┌─┴─┐├───┤├─────┤├───┤»
«q_2: ┤ X ├┤ Rz(12.899) ├┤ X ├┤ H ├┤ X ├┤ Rz(13.876) ├┤ X ├┤ H ├┤ Sdg ├┤ H ├»
« └───┘└────────────┘└───┘└───┘└───┘└────────────┘└───┘└───┘└─────┘└───┘»
«
«q_0: ──────────────────────────────────────────────────────────
« ┌───┐┌───┐
«q_1: ──■──────────────────■──┤ H ├┤ S ├──■──────────────────■──
« ┌─┴─┐┌────────────┐┌─┴─┐├───┤├───┤┌─┴─┐┌────────────┐┌─┴─┐
«q_2: ┤ X ├┤ Rz(13.876) ├┤ X ├┤ H ├┤ S ├┤ X ├┤ Rz(13.876) ├┤ X ├
« └───┘└────────────┘└───┘└───┘└───┘└───┘└────────────┘└───┘
============================================================
Hamiltonian Type: PAULI_Z
Depth: 11, Gates: 15
============================================================
┌───┐ ┌───────┐ »
q_0: ┤ H ├─┤ Rz(1) ├───■──────────────────■────■──────────────────■───────»
├───┤┌┴───────┴┐┌─┴─┐┌────────────┐┌─┴─┐ │ │ »
q_1: ┤ H ├┤ Rz(0.6) ├┤ X ├┤ Rz(15.013) ├┤ X ├──┼──────────────────┼────■──»
├───┤├─────────┤└───┘└────────────┘└───┘┌─┴─┐┌────────────┐┌─┴─┐┌─┴─┐»
q_2: ┤ H ├┤ Rz(1.4) ├────────────────────────┤ X ├┤ Rz(12.899) ├┤ X ├┤ X ├»
└───┘└─────────┘ └───┘└────────────┘└───┘└───┘»
«
«q_0: ───────────────────
«
«q_1: ────────────────■──
« ┌────────────┐┌─┴─┐
«q_2: ┤ Rz(13.876) ├┤ X ├
« └────────────┘└───┘
6. Batch Processing & Memory-Efficient Iteration¶
The library offers three ways to process multiple samples:
get_circuits()— Returns a list of all circuitsget_circuits(..., parallel=True)— Parallel batch processingiter_circuits()— Memory-efficient generator
enc = HamiltonianEncoding(n_features=4, reps=2)
X_batch = np.random.default_rng(42).uniform(0, 1, size=(20, 4))
# Method 1: Sequential batch (default)
circuits_seq = enc.get_circuits(X_batch, backend='pennylane')
print(f"Sequential batch: {len(circuits_seq)} circuits")
# Method 2: Parallel batch
circuits_par = enc.get_circuits(X_batch, backend='pennylane', parallel=True)
print(f"Parallel batch: {len(circuits_par)} circuits")
# Method 3: Parallel batch with custom max_workers
circuits_par2 = enc.get_circuits(X_batch, backend='pennylane', parallel=True, max_workers=2)
print(f"Parallel (2 workers): {len(circuits_par2)} circuits")
Sequential batch: 20 circuits Parallel batch: 20 circuits Parallel (2 workers): 20 circuits
# Method 3: Memory-efficient generator with iter_circuits()
enc = HamiltonianEncoding(n_features=4, reps=1)
X_large = np.random.default_rng(42).uniform(0, 1, size=(100, 4))
# iter_circuits returns a generator — circuits are yielded one at a time
gen = enc.iter_circuits(X_large, backend='pennylane')
print(f"Generator type: {type(gen)}")
# Process circuits one at a time (constant memory)
count = 0
for circuit in enc.iter_circuits(X_large, backend='pennylane'):
count += 1
print(f"Processed {count} circuits via generator")
# Can also use islice for partial iteration
from itertools import islice
first_5 = list(islice(enc.iter_circuits(X_large, backend='pennylane'), 5))
print(f"First 5 circuits: {len(first_5)} circuits")
Generator type: <class 'generator'> Processed 100 circuits via generator First 5 circuits: 5 circuits
# get_circuits also accepts a single 1D sample (auto-reshaped)
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 via get_circuits: {len(circuits_single)} circuit(s)")
Single sample via get_circuits: 1 circuit(s)
7. Configuration Parameters In Depth¶
7.1 evolution_time — Encoding Strength¶
The evolution time $t$ scales all rotation angles uniformly. Larger values create more complex states but may lead to harder optimization landscapes.
x = np.array([0.5, 0.3, 0.7, 0.1])
for t in [0.0, 0.1, 0.5, 1.0, 2.0, 5.0]:
enc_t = HamiltonianEncoding(n_features=4, evolution_time=t, reps=1)
# The time_step is evolution_time / reps
print(f"evolution_time={t:<4} time_step={enc_t._time_step:<6.3f}")
evolution_time=0.0 time_step=0.000 evolution_time=0.1 time_step=0.100 evolution_time=0.5 time_step=0.500 evolution_time=1.0 time_step=1.000 evolution_time=2.0 time_step=2.000 evolution_time=5.0 time_step=5.000
# Negative evolution time is allowed (time-reversal)
enc_neg = HamiltonianEncoding(n_features=3, evolution_time=-1.0)
print(f"Negative evolution_time: {enc_neg.evolution_time}")
print(f"Time step: {enc_neg._time_step}")
Negative evolution_time: -1.0 Time step: -0.5
# Zero evolution time produces a uniform superposition |+>^n
if HAS_PENNYLANE:
enc_zero = HamiltonianEncoding(n_features=3, evolution_time=0.0, reps=1)
circuit_fn = enc_zero.get_circuit(np.array([0.5, 0.3, 0.7]), backend='pennylane')
dev = qml.device('default.qubit', wires=3)
@qml.qnode(dev)
def get_state():
circuit_fn()
return qml.state()
state = get_state()
# With t=0, all amplitudes should be equal: 1/sqrt(2^n)
expected_amp = 1.0 / np.sqrt(2**3)
print(f"Expected uniform amplitude: {expected_amp:.6f}")
print(f"Actual amplitudes: {np.abs(state)}")
print(f"All equal: {np.allclose(np.abs(state), expected_amp)}")
Expected uniform amplitude: 0.353553 Actual amplitudes: [0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339 0.35355339] All equal: True
7.2 reps — Trotter Steps¶
Higher reps improves the Trotterization accuracy but increases circuit depth linearly.
print(f"{'reps':>4} {'Depth':>6} {'Total Gates':>12} {'CNOTs':>6} {'Time Step':>10}")
print("-" * 50)
for r in [1, 2, 3, 4, 5, 10]:
enc_r = HamiltonianEncoding(n_features=4, reps=r, evolution_time=1.0)
bd = enc_r.gate_count_breakdown()
print(f"{r:>4} {enc_r.depth:>6} {bd['total']:>12} {bd['cnot']:>6} {enc_r._time_step:>10.4f}")
reps Depth Total Gates CNOTs Time Step -------------------------------------------------- 1 11 26 12 1.0000 2 21 48 24 0.5000 3 31 70 36 0.3333 4 41 92 48 0.2500 5 51 114 60 0.2000 10 101 224 120 0.1000
7.3 max_pairs — Limiting Two-Qubit Interactions¶
For large systems with full entanglement, max_pairs limits the number of interaction pairs to control circuit depth.
# Full entanglement with 6 qubits: 15 pairs
enc_all = HamiltonianEncoding(n_features=6, entanglement='full', reps=1)
print(f"All pairs ({len(enc_all.get_entanglement_pairs())}): {enc_all.get_entanglement_pairs()}")
# Limit to 5 pairs (warning issued)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_limited = HamiltonianEncoding(n_features=6, entanglement='full', max_pairs=5, reps=1)
if w:
print(f"\nWarning: {w[0].message}")
print(f"\nLimited pairs ({len(enc_limited.get_entanglement_pairs())}): {enc_limited.get_entanglement_pairs()}")
# Depth comparison
print(f"\nDepth with all pairs: {enc_all.depth}")
print(f"Depth with 5 pairs: {enc_limited.depth}")
All pairs (15): [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)] Warning: Limiting entanglement to 5 pairs (full pattern has 15 pairs). This reduces circuit depth but may affect encoding expressiveness. Pairs are selected in order, prioritizing lower-indexed qubits. Limited pairs (5): [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)] Depth with all pairs: 17 Depth with 5 pairs: 17
# max_pairs=0 means no two-qubit interactions at all
enc_zero_pairs = HamiltonianEncoding(n_features=4, entanglement='full', max_pairs=0, reps=1)
print(f"Pairs with max_pairs=0: {enc_zero_pairs.get_entanglement_pairs()}")
print(f"Entangling: {enc_zero_pairs.properties.is_entangling}")
print(f"Depth: {enc_zero_pairs.depth}")
Pairs with max_pairs=0: [] Entangling: False Depth: 2
C:\Users\ashut\AppData\Local\Temp\ipykernel_10552\3695028303.py:2: UserWarning: Limiting entanglement to 0 pairs (full pattern has 6 pairs). This reduces circuit depth but may affect encoding expressiveness. Pairs are selected in order, prioritizing lower-indexed qubits. enc_zero_pairs = HamiltonianEncoding(n_features=4, entanglement='full', max_pairs=0, reps=1)
7.4 include_single_qubit_terms¶
For IQP and Pauli-Z, single-qubit Z rotations are always included (this parameter is ignored). For XY and Heisenberg, this controls whether $\sum_i x_i Z_i$ terms are added.
# For IQP: single-qubit terms always present
enc_iqp_no = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', include_single_qubit_terms=False, reps=1)
enc_iqp_yes = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', include_single_qubit_terms=True, reps=1)
# IQP always includes single-qubit RZ gates regardless of this flag
bd1 = enc_iqp_no.gate_count_breakdown()
bd2 = enc_iqp_yes.gate_count_breakdown()
print(f"IQP (include=False): RZ={bd1['rz']}, Total={bd1['total']}")
print(f"IQP (include=True): RZ={bd2['rz']}, Total={bd2['total']}")
print(f"Note: IQP always includes single-qubit terms")
# For XY: the flag has real effect
print()
enc_xy_no = HamiltonianEncoding(n_features=4, hamiltonian_type='xy', include_single_qubit_terms=False, reps=1)
enc_xy_yes = HamiltonianEncoding(n_features=4, hamiltonian_type='xy', include_single_qubit_terms=True, reps=1)
bd3 = enc_xy_no.gate_count_breakdown()
bd4 = enc_xy_yes.gate_count_breakdown()
print(f"XY (include=False): RZ={bd3['rz']}, Depth={enc_xy_no.depth}")
print(f"XY (include=True): RZ={bd4['rz']}, Depth={enc_xy_yes.depth}")
print(f"Extra RZ gates from single-qubit terms: {bd4['rz'] - bd3['rz']}")
IQP (include=False): RZ=10, Total=26 IQP (include=True): RZ=10, Total=26 Note: IQP always includes single-qubit terms XY (include=False): RZ=12, Depth=37 XY (include=True): RZ=16, Depth=38 Extra RZ gates from single-qubit terms: 4
8. Properties & Resource Analysis¶
Each encoding computes and caches an EncodingProperties dataclass with theoretical circuit properties.
enc = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', reps=2, entanglement='full')
props = enc.properties
print("=== EncodingProperties ===")
print(f"Type: {type(props).__name__} (frozen dataclass)")
print(f"")
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} (all data-dependent, no trainable params)")
print(f"is_entangling: {props.is_entangling}")
print(f"simulability: {props.simulability}")
print(f"trainability_estimate: {props.trainability_estimate}")
print(f"notes: {props.notes[:80]}...")
# Properties are frozen (immutable)
try:
props.n_qubits = 10
except Exception as e:
print(f"\nCannot modify properties: {type(e).__name__}: {e}")
=== EncodingProperties === Type: EncodingProperties (frozen dataclass) n_qubits: 4 depth: 21 gate_count: 48 single_qubit_gates: 24 two_qubit_gates: 24 parameter_count: 0 (all data-dependent, no trainable params) is_entangling: True simulability: not_simulable trainability_estimate: None notes: Hamiltonian type: iqp, Evolution time: 1.0, Trotter steps: 2, Entanglement: full... Cannot modify properties: FrozenInstanceError: cannot assign to field 'n_qubits'
# Properties can be converted to a dictionary
props_dict = props.to_dict()
print("Properties as dict:")
for key, value in props_dict.items():
if isinstance(value, str) and len(value) > 60:
value = value[:60] + "..."
print(f" {key}: {value}")
Properties as dict: n_qubits: 4 depth: 21 gate_count: 48 single_qubit_gates: 24 two_qubit_gates: 24 parameter_count: 0 is_entangling: True simulability: not_simulable expressibility: None entanglement_capability: None trainability_estimate: None noise_resilience_estimate: None notes: Hamiltonian type: iqp, Evolution time: 1.0, Trotter steps: 2...
# Thread-safe lazy initialization: properties are computed once and cached
enc_lazy = HamiltonianEncoding(n_features=4, reps=3)
# First access triggers computation
p1 = enc_lazy.properties
# Second access returns cached value (same object)
p2 = enc_lazy.properties
print(f"Same object: {p1 is p2}")
Same object: True
8.1 resource_summary() — Comprehensive Resource Analysis¶
enc = HamiltonianEncoding(n_features=6, hamiltonian_type='heisenberg', reps=2, entanglement='linear')
summary = enc.resource_summary()
print("=== Resource Summary ===")
for key, value in summary.items():
if key == 'gate_counts':
print(f" {key}:")
for gk, gv in value.items():
print(f" {gk}: {gv}")
elif key == 'hardware_requirements':
print(f" {key}:")
for hk, hv in value.items():
print(f" {hk}: {hv}")
elif key == 'entanglement_pairs':
print(f" {key}: {value}")
else:
print(f" {key}: {value}")
=== Resource Summary ===
n_qubits: 6
n_features: 6
depth: 63
reps: 2
hamiltonian_type: heisenberg
evolution_time: 1.0
include_single_qubit_terms: True
entanglement: linear
entanglement_pairs: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
n_entanglement_pairs: 5
gate_counts:
hadamard: 86
rz: 42
s_gates: 40
cnot: 60
total_single_qubit: 168
total_two_qubit: 60
total: 228
is_entangling: True
simulability: not_simulable
trainability_estimate: None
hardware_requirements:
connectivity: linear
native_gates: ['CNOT', 'H', 'RZ', 'S', 'Sdg']
min_two_qubit_gate_fidelity: 0.99
9. Gate Count Analysis (Upper-Bound vs Exact)¶
The library distinguishes between:
gate_count_breakdown(): Upper-bound estimates (no input data needed)count_gates(x): Exact counts for specific input (skips zero-angle gates)
enc = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', reps=2, entanglement='full')
# Upper-bound (theoretical max)
upper = enc.gate_count_breakdown()
print("Gate count breakdown (UPPER BOUND):")
for k, v in upper.items():
print(f" {k}: {v}")
# Exact count with specific input
x = np.array([0.5, 0.3, 0.7, 0.1])
exact = enc.count_gates(x)
print(f"\nExact gates for x={x}:")
for k, v in exact.items():
print(f" {k}: {v}")
print(f"\nUpper bound total: {upper['total']}, Exact total: {exact['total']}")
Gate count breakdown (UPPER BOUND): hadamard: 4 rz: 20 s_gates: 0 cnot: 24 total_single_qubit: 24 total_two_qubit: 24 total: 48 Exact gates for x=[0.5 0.3 0.7 0.1]: hadamard: 4 rz: 20 s_gates: 0 cnot: 24 single_qubit: 24 two_qubit: 24 total: 48 Upper bound total: 48, Exact total: 48
# Demonstrate that zero inputs can reduce gate count
# Features equal to zero produce zero single-qubit angles
x_zeros = np.array([0.0, 0.0, 0.0, 0.0])
exact_zeros = enc.count_gates(x_zeros)
x_nonzero = np.array([1.0, 1.0, 1.0, 1.0])
exact_nonzero = enc.count_gates(x_nonzero)
print(f"All zeros -> total gates: {exact_zeros['total']}, CNOTs: {exact_zeros['cnot']}")
print(f"All ones -> total gates: {exact_nonzero['total']}, CNOTs: {exact_nonzero['cnot']}")
print(f"Upper bound -> total gates: {upper['total']}, CNOTs: {upper['cnot']}")
All zeros -> total gates: 40, CNOTs: 24 All ones -> total gates: 48, CNOTs: 24 Upper bound -> total gates: 48, CNOTs: 24
# Compare gate counts across all four Hamiltonian types
x_test = np.array([0.5, 0.3, 0.7, 0.1])
print(f"{'Type':<14} {'Upper H':>8} {'Upper RZ':>9} {'Upper S':>8} {'Upper CNOT':>11} {'Upper Total':>12} {'Exact Total':>12}")
print("-" * 80)
for htype in ['iqp', 'pauli_z', 'xy', 'heisenberg']:
enc_t = HamiltonianEncoding(n_features=4, hamiltonian_type=htype, reps=2, entanglement='full')
bd = enc_t.gate_count_breakdown()
exact = enc_t.count_gates(x_test)
print(
f"{htype:<14} {bd['hadamard']:>8} {bd['rz']:>9} {bd['s_gates']:>8} "
f"{bd['cnot']:>11} {bd['total']:>12} {exact['total']:>12}"
)
Type Upper H Upper RZ Upper S Upper CNOT Upper Total Exact Total -------------------------------------------------------------------------------- iqp 4 20 0 24 48 48 pauli_z 4 20 0 24 48 48 xy 100 32 48 48 228 228 heisenberg 100 44 48 72 264 264
10. Capability Protocols¶
The library uses Python's structural subtyping (protocols) to check encoding capabilities at runtime.
enc = HamiltonianEncoding(n_features=4)
# HamiltonianEncoding implements ResourceAnalyzable
print(f"ResourceAnalyzable: {isinstance(enc, ResourceAnalyzable)}")
print(f"EntanglementQueryable: {isinstance(enc, EntanglementQueryable)}")
print(f"DataDependentResourceAnalyzable: {isinstance(enc, DataDependentResourceAnalyzable)}")
print(f"DataTransformable: {isinstance(enc, DataTransformable)}")
ResourceAnalyzable: True EntanglementQueryable: True DataDependentResourceAnalyzable: False DataTransformable: False
# Type-guard functions
from encoding_atlas.core.protocols import (
is_resource_analyzable,
is_entanglement_queryable,
is_data_dependent_resource_analyzable,
is_data_transformable,
)
print(f"is_resource_analyzable: {is_resource_analyzable(enc)}")
print(f"is_entanglement_queryable: {is_entanglement_queryable(enc)}")
is_resource_analyzable: True is_entanglement_queryable: True
# Protocol-based generic function
def analyze_encoding(encoding):
"""Analyze any encoding using its capabilities."""
info = {"class": type(encoding).__name__}
if isinstance(encoding, ResourceAnalyzable):
summary = encoding.resource_summary()
info["n_qubits"] = summary["n_qubits"]
info["depth"] = summary["depth"]
info["total_gates"] = summary["gate_counts"]["total"]
if isinstance(encoding, EntanglementQueryable):
pairs = encoding.get_entanglement_pairs()
info["n_pairs"] = len(pairs)
return info
result = analyze_encoding(enc)
print("Protocol-based analysis:")
for k, v in result.items():
print(f" {k}: {v}")
Protocol-based analysis: class: HamiltonianEncoding n_qubits: 4 depth: 21 total_gates: 48 n_pairs: 6
11. Registry System¶
All encodings are registered in a global registry and can be accessed by name.
# List all registered encodings
all_encodings = list_encodings()
print(f"Registered encodings ({len(all_encodings)}):")
for name in all_encodings:
print(f" - {name}")
Registered encodings (26): - amplitude - angle - angle_ry - basis - covariant - covariant_feature_map - cyclic_equivariant - cyclic_equivariant_feature_map - data_reuploading - hamiltonian - hamiltonian_encoding - hardware_efficient - higher_order_angle - iqp - pauli_feature_map - qaoa - qaoa_encoding - so2_equivariant - so2_equivariant_feature_map - swap_equivariant - swap_equivariant_feature_map - symmetry_inspired - symmetry_inspired_feature_map - trainable - trainable_encoding - zz_feature_map
# Get an encoding by name using the registry
enc_from_registry = get_encoding('hamiltonian', n_features=4, hamiltonian_type='xy', reps=3)
print(f"From registry: {enc_from_registry!r}")
print(f"Type: {type(enc_from_registry).__name__}")
From registry: HamiltonianEncoding(n_features=4, hamiltonian_type='xy', evolution_time=1.0, reps=3, entanglement='full', insert_barriers=True) Type: HamiltonianEncoding
12. Serialization & Deep Copy¶
HamiltonianEncoding supports pickling (for distributed computing) and deep copying.
import pickle
import copy
enc = HamiltonianEncoding(
n_features=4,
hamiltonian_type='heisenberg',
evolution_time=2.5,
reps=3,
entanglement='linear',
insert_barriers=False,
include_single_qubit_terms=False,
)
# Pickle serialization round-trip
serialized = pickle.dumps(enc)
enc_restored = pickle.loads(serialized)
print("=== Pickle Round-Trip ===")
print(f"Original: {enc!r}")
print(f"Restored: {enc_restored!r}")
print(f"Equal: {enc == enc_restored}")
print(f"Same obj: {enc is enc_restored}")
# Verify circuit generation works after unpickling
x = np.array([0.5, 0.3, 0.7, 0.1])
c1 = enc.get_circuit(x, backend='pennylane')
c2 = enc_restored.get_circuit(x, backend='pennylane')
print(f"Both generate circuits: {callable(c1) and callable(c2)}")
=== Pickle Round-Trip === Original: HamiltonianEncoding(n_features=4, hamiltonian_type='heisenberg', evolution_time=2.5, reps=3, entanglement='linear', insert_barriers=False, include_single_qubit_terms=False) Restored: HamiltonianEncoding(n_features=4, hamiltonian_type='heisenberg', evolution_time=2.5, reps=3, entanglement='linear', insert_barriers=False, include_single_qubit_terms=False) Equal: True Same obj: False Both generate circuits: True
# Deep copy
enc_copy = copy.deepcopy(enc)
print(f"Deep copy: {enc_copy!r}")
print(f"Equal to original: {enc_copy == enc}")
print(f"Same object: {enc_copy is enc}")
# Deep copy creates a fully independent instance
# Useful for parallel processing where each thread gets its own encoding
print(f"Independent pair lists: {enc_copy.get_entanglement_pairs() is not enc.get_entanglement_pairs()}")
Deep copy: HamiltonianEncoding(n_features=4, hamiltonian_type='heisenberg', evolution_time=2.5, reps=3, entanglement='linear', insert_barriers=False, include_single_qubit_terms=False) Equal to original: True Same object: False Independent pair lists: True
13. Equality, Hashing & Collections¶
HamiltonianEncoding instances are hashable and support equality comparison based on all configuration parameters. This allows using them as dictionary keys and in sets.
enc1 = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', reps=2)
enc2 = HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', reps=2)
enc3 = HamiltonianEncoding(n_features=4, hamiltonian_type='xy', reps=2)
# Equality
print(f"enc1 == enc2 (same config): {enc1 == enc2}")
print(f"enc1 == enc3 (diff type): {enc1 == enc3}")
print(f"enc1 == 'not an encoding': {enc1 == 'not an encoding'}")
# Hashing
print(f"\nhash(enc1) == hash(enc2): {hash(enc1) == hash(enc2)}")
print(f"hash(enc1) == hash(enc3): {hash(enc1) == hash(enc3)}")
# In sets (deduplication)
encoding_set = {enc1, enc2, enc3}
print(f"\nSet size (enc1, enc2, enc3): {len(encoding_set)} (enc1 and enc2 deduplicated)")
# As dictionary keys
encoding_dict = {enc1: "IQP encoding"}
print(f"Dict lookup with enc2: {encoding_dict[enc2]}")
enc1 == enc2 (same config): True enc1 == enc3 (diff type): False enc1 == 'not an encoding': False hash(enc1) == hash(enc2): True hash(enc1) == hash(enc3): False Set size (enc1, enc2, enc3): 2 (enc1 and enc2 deduplicated) Dict lookup with enc2: IQP encoding
14. Edge Cases & Robustness¶
This section demonstrates that the library handles various edge cases gracefully.
14.1 Single Qubit (n_features=1)¶
# With only 1 qubit, there are no entanglement pairs
enc_1q = HamiltonianEncoding(n_features=1, hamiltonian_type='iqp', reps=2)
print(f"n_qubits: {enc_1q.n_qubits}")
print(f"Entanglement pairs: {enc_1q.get_entanglement_pairs()}")
print(f"Is entangling: {enc_1q.properties.is_entangling}")
print(f"Depth: {enc_1q.depth}")
# Still generates valid circuits
x_1d = np.array([0.5])
circuit = enc_1q.get_circuit(x_1d, backend='pennylane')
print(f"Circuit generated: {callable(circuit)}")
# Works with all Hamiltonian types
for htype in ['iqp', 'xy', 'heisenberg', 'pauli_z']:
enc_1 = HamiltonianEncoding(n_features=1, hamiltonian_type=htype)
print(f" {htype}: pairs={len(enc_1.get_entanglement_pairs())}, depth={enc_1.depth}")
n_qubits: 1 Entanglement pairs: [] Is entangling: False Depth: 3 Circuit generated: True iqp: pairs=0, depth=3 xy: pairs=0, depth=3 heisenberg: pairs=0, depth=3 pauli_z: pairs=0, depth=3
14.2 Two Qubits (n_features=2)¶
enc_2q = HamiltonianEncoding(n_features=2, hamiltonian_type='iqp', reps=1)
print(f"n_qubits: {enc_2q.n_qubits}")
print(f"Entanglement pairs: {enc_2q.get_entanglement_pairs()}")
# All topologies give the same single pair for n=2
for topology in ['full', 'linear', 'circular']:
enc_2 = HamiltonianEncoding(n_features=2, entanglement=topology)
print(f" {topology}: {enc_2.get_entanglement_pairs()}")
n_qubits: 2 Entanglement pairs: [(0, 1)] full: [(0, 1)] linear: [(0, 1)] circular: [(0, 1)]
14.3 Very Small and Very Large Inputs¶
enc = HamiltonianEncoding(n_features=3, reps=1)
# Very small inputs (near zero)
x_tiny = np.array([1e-15, 1e-15, 1e-15])
exact_tiny = enc.count_gates(x_tiny)
print(f"Tiny inputs: total={exact_tiny['total']}, cnot={exact_tiny['cnot']}")
# Zero angles are below tolerance, so some gates may be skipped
# Very large inputs
x_large = np.array([100.0, 200.0, 300.0])
exact_large = enc.count_gates(x_large)
print(f"Large inputs: total={exact_large['total']}, cnot={exact_large['cnot']}")
# Negative inputs
x_neg = np.array([-0.5, -1.0, -0.3])
exact_neg = enc.count_gates(x_neg)
print(f"Negative inputs: total={exact_neg['total']}, cnot={exact_neg['cnot']}")
Tiny inputs: total=12, cnot=6 Large inputs: total=15, cnot=6 Negative inputs: total=15, cnot=6
14.4 2D Input (Single Sample as Row)¶
enc = HamiltonianEncoding(n_features=3)
# 1D input
x_1d = np.array([0.1, 0.2, 0.3])
c1 = enc.get_circuit(x_1d, backend='pennylane')
print(f"1D input accepted: {callable(c1)}")
# 2D input with shape (1, n_features)
x_2d = np.array([[0.1, 0.2, 0.3]])
c2 = enc.get_circuit(x_2d, backend='pennylane')
print(f"2D input (1, n) accepted: {callable(c2)}")
1D input accepted: True 2D input (1, n) accepted: True
14.5 Numpy Types for Parameters¶
Most parameters (reps, max_pairs, evolution_time) accept numpy numeric types natively. However, n_features goes through BaseEncoding.__init__ which requires a Python int. Wrap it with int() if coming from numpy.
# numpy integer/float types for parameters
#
# Note: n_features passes through BaseEncoding.__init__ which only accepts
# Python int (not np.integer). HamiltonianEncoding's own parameters (reps,
# max_pairs, evolution_time) DO accept numpy types and cast internally.
# So: cast n_features to int() yourself, but reps/evolution_time/max_pairs
# handle numpy types natively.
enc_np = HamiltonianEncoding(
n_features=int(np.int32(4)), # must cast to Python int
reps=np.int64(3), # numpy int accepted
evolution_time=np.float64(1.5), # numpy float accepted
max_pairs=np.int32(4), # numpy int accepted
)
print(f"Created with numpy types: {enc_np!r}")
print(f"evolution_time type: {type(enc_np.evolution_time).__name__}")
print(f"reps type: {type(enc_np.reps).__name__}")
Created with numpy types: HamiltonianEncoding(n_features=4, hamiltonian_type='iqp', evolution_time=1.5, reps=3, entanglement='full', insert_barriers=True, max_pairs=4) evolution_time type: float reps type: int
C:\Users\ashut\AppData\Local\Temp\ipykernel_10552\652875891.py:9: UserWarning: Limiting entanglement to 4 pairs (full pattern has 6 pairs). This reduces circuit depth but may affect encoding expressiveness. Pairs are selected in order, prioritizing lower-indexed qubits. enc_np = HamiltonianEncoding(
15. Input Validation & Error Handling¶
The library validates all inputs and provides clear error messages.
# === Constructor Validation ===
# Invalid n_features
for bad_n in [0, -1, 1.5, 'four']:
try:
HamiltonianEncoding(n_features=bad_n)
except (ValueError, TypeError) as e:
print(f"n_features={bad_n!r}: {type(e).__name__}: {e}")
print()
n_features=0: ValueError: n_features must be a positive integer, got 0 n_features=-1: ValueError: n_features must be a positive integer, got -1 n_features=1.5: ValueError: n_features must be a positive integer, got 1.5 n_features='four': ValueError: n_features must be a positive integer, got four
# Invalid hamiltonian_type
for bad_type in ['invalid', 123, None]:
try:
HamiltonianEncoding(n_features=4, hamiltonian_type=bad_type)
except (ValueError, TypeError) as e:
print(f"hamiltonian_type={bad_type!r}: {type(e).__name__}: {e}")
hamiltonian_type='invalid': ValueError: hamiltonian_type must be one of ['heisenberg', 'iqp', 'pauli_z', 'xy'], got 'invalid' hamiltonian_type=123: TypeError: hamiltonian_type must be a string, got int hamiltonian_type=None: TypeError: hamiltonian_type must be a string, got NoneType
# Invalid evolution_time
for bad_t in [float('inf'), float('nan'), 'one']:
try:
HamiltonianEncoding(n_features=4, evolution_time=bad_t)
except (ValueError, TypeError) as e:
print(f"evolution_time={bad_t!r}: {type(e).__name__}: {e}")
evolution_time=inf: ValueError: evolution_time must be finite, got inf evolution_time=nan: ValueError: evolution_time must be finite, got nan evolution_time='one': TypeError: evolution_time must be a number, got str
# Invalid reps
for bad_r in [0, -1, 1.5, 'two']:
try:
HamiltonianEncoding(n_features=4, reps=bad_r)
except (ValueError, TypeError) as e:
print(f"reps={bad_r!r}: {type(e).__name__}: {e}")
reps=0: ValueError: reps must be at least 1, got 0 reps=-1: ValueError: reps must be at least 1, got -1 reps=1.5: TypeError: reps must be an integer, got float reps='two': TypeError: reps must be an integer, got str
# Invalid entanglement
try:
HamiltonianEncoding(n_features=4, entanglement='star')
except ValueError as e:
print(f"entanglement='star': {e}")
entanglement='star': entanglement must be one of ['circular', 'full', 'linear'], got 'star'
# Invalid insert_barriers type
try:
HamiltonianEncoding(n_features=4, insert_barriers='yes')
except TypeError as e:
print(f"insert_barriers='yes': {e}")
insert_barriers='yes': insert_barriers must be a bool, got str
# Invalid max_pairs
for bad_mp in [-1, 1.5, 'ten']:
try:
HamiltonianEncoding(n_features=4, max_pairs=bad_mp)
except (ValueError, TypeError) as e:
print(f"max_pairs={bad_mp!r}: {type(e).__name__}: {e}")
max_pairs=-1: ValueError: max_pairs must be non-negative, got -1 max_pairs=1.5: TypeError: max_pairs must be an integer or None, got float max_pairs='ten': TypeError: max_pairs must be an integer or None, got str
# Invalid include_single_qubit_terms type
try:
HamiltonianEncoding(n_features=4, include_single_qubit_terms=1)
except TypeError as e:
print(f"include_single_qubit_terms=1: {e}")
include_single_qubit_terms=1: include_single_qubit_terms must be a bool, got int
# === Input Data Validation ===
enc = HamiltonianEncoding(n_features=3)
# Wrong number of features
try:
enc.get_circuit(np.array([0.1, 0.2]), backend='pennylane') # 2 instead of 3
except ValueError as e:
print(f"Wrong shape: {e}")
# NaN in input
try:
enc.get_circuit(np.array([0.1, float('nan'), 0.3]), backend='pennylane')
except ValueError as e:
print(f"NaN input: {e}")
# Inf in input
try:
enc.get_circuit(np.array([0.1, float('inf'), 0.3]), backend='pennylane')
except ValueError as e:
print(f"Inf input: {e}")
Wrong shape: Expected 3 features, got 2 NaN input: Input contains NaN or infinite values Inf input: Input contains NaN or infinite values
# Invalid backend
try:
enc.get_circuit(np.array([0.1, 0.2, 0.3]), backend='tensorflow')
except ValueError as e:
print(f"Invalid backend: {e}")
Invalid backend: Unknown backend 'tensorflow'. Supported backends: 'pennylane', 'qiskit', 'cirq'
# get_circuit rejects multi-sample 2D arrays
try:
enc.get_circuit(np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]), backend='pennylane')
except ValueError as e:
print(f"Multi-sample for get_circuit: {e}")
# count_gates rejects multi-sample 2D arrays
try:
enc.count_gates(np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]))
except ValueError as e:
print(f"Multi-sample for count_gates: {e}")
Multi-sample for get_circuit: get_circuit expects a single sample, got shape (2, 3). Use get_circuits for batches. Multi-sample for count_gates: count_gates expects a single sample, got shape (2, 3)
16. Critical Point at π & Data Preprocessing¶
The two-qubit rotation angle uses the formula:
$$\text{angle} = \text{time\_step} \times (\pi - x_i)(\pi - x_j)$$
This has a critical point at $x = \pi$ where interactions vanish. The library warns you about this.
# Demonstrate the critical point warning
enc_crit = HamiltonianEncoding(n_features=3, reps=1)
# Features near pi will trigger a warning
x_near_pi = np.array([3.14, 0.5, 0.3]) # First feature is close to pi
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
circuit = enc_crit.get_circuit(x_near_pi, backend='pennylane')
if w:
print(f"Warning triggered: {w[0].message}")
else:
print("No warning (feature not close enough to pi)")
Warning triggered: Feature value(s) detected near π (critical point). Values: x[0]=3.1400, x[1]=0.5000. Two-qubit interactions will be minimal for features near π (~3.14159). Consider scaling features to [0, 1] or [-1, 1] for stronger two-qubit correlations. See class docstring 'Input Scaling' section for details.
# The warning is only issued once per instance
enc_once = HamiltonianEncoding(n_features=3, reps=1)
x_pi = np.array([np.pi, 0.5, 0.3])
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_once.get_circuit(x_pi, backend='pennylane') # Triggers warning
enc_once.get_circuit(x_pi, backend='pennylane') # No duplicate warning
n_warnings = len([x for x in w if issubclass(x.category, UserWarning)])
print(f"Number of critical-point warnings from 2 calls: {n_warnings} (only 1 per instance)")
Number of critical-point warnings from 2 calls: 1 (only 1 per instance)
# Demonstrate recommended input ranges
enc = HamiltonianEncoding(n_features=3, reps=1)
# Range [0, 1] - SAFE
x_01 = np.array([0.5, 0.3, 0.7])
# The two-qubit angle for features in [0,1]: (pi-0.5)(pi-0.3) ~ (2.64)(2.84) ~ 7.5
# Range [-1, 1] - SAFE
x_neg1_1 = np.array([-0.5, 0.0, 0.8])
# (pi-(-0.5))(pi-0) ~ (3.64)(3.14) ~ 11.4
# Range [0, 2*pi] - CAUTION (includes critical point)
x_2pi = np.array([3.14, 1.0, 5.0])
# (pi-3.14)(pi-1.0) ~ (0.0016)(2.14) ~ 0.003 (nearly zero!)
print("Recommended data ranges and their effect on two-qubit angles:")
for name, x in [("[0, 1]", x_01), ("[-1, 1]", x_neg1_1), ("[0, 2pi]", x_2pi)]:
# Compute a sample two-qubit angle
angle = (np.pi - x[0]) * (np.pi - x[1]) * enc._time_step
print(f" Range {name:>8}: x={x}, angle(0,1)={angle:.6f}")
Recommended data ranges and their effect on two-qubit angles: Range [0, 1]: x=[0.5 0.3 0.7], angle(0,1)=7.506330 Range [-1, 1]: x=[-0.5 0. 0.8], angle(0,1)=11.440401 Range [0, 2pi]: x=[3.14 1. 5. ], angle(0,1)=0.003411
17. Rotation Angle Mathematics¶
Understanding the rotation angle formulas is key to using Hamiltonian encoding effectively.
enc = HamiltonianEncoding(n_features=4, evolution_time=1.0, reps=2)
x = np.array([0.5, 0.3, 0.7, 0.1])
# time_step = evolution_time / reps
time_step = enc._time_step
print(f"time_step = evolution_time / reps = {enc.evolution_time} / {enc.reps} = {time_step}")
# Single-qubit angle: time_step * x_i
print("\nSingle-qubit rotation angles:")
for i in range(4):
angle_single = enc._compute_rotation_angle(x, i)
expected = time_step * x[i]
print(f" qubit {i}: angle = {time_step} * {x[i]} = {angle_single:.6f} (expected {expected:.6f})")
assert np.isclose(angle_single, expected)
# Two-qubit angle: time_step * (pi - x_i)(pi - x_j)
print("\nTwo-qubit rotation angles (first 3 pairs):")
pairs = enc.get_entanglement_pairs()
for i, j in pairs[:3]:
angle_two = enc._compute_rotation_angle(x, i, j)
expected = time_step * (np.pi - x[i]) * (np.pi - x[j])
print(f" pair ({i},{j}): angle = {time_step} * (pi-{x[i]}) * (pi-{x[j]}) = {angle_two:.6f}")
assert np.isclose(angle_two, expected)
print("\nAll angle formulas verified!")
time_step = evolution_time / reps = 1.0 / 2 = 0.5 Single-qubit rotation angles: qubit 0: angle = 0.5 * 0.5 = 0.250000 (expected 0.250000) qubit 1: angle = 0.5 * 0.3 = 0.150000 (expected 0.150000) qubit 2: angle = 0.5 * 0.7 = 0.350000 (expected 0.350000) qubit 3: angle = 0.5 * 0.1 = 0.050000 (expected 0.050000) Two-qubit rotation angles (first 3 pairs): pair (0,1): angle = 0.5 * (pi-0.5) * (pi-0.3) = 3.753165 pair (0,2): angle = 0.5 * (pi-0.5) * (pi-0.7) = 3.224847 pair (0,3): angle = 0.5 * (pi-0.5) * (pi-0.1) = 4.017324 All angle formulas verified!
18. Depth Computation & Parallelization¶
Circuit depth is computed exactly using graph coloring for parallelization of two-qubit gates.
Two-qubit gates on disjoint qubit pairs can execute in parallel. The number of parallel groups (colors) depends on the entanglement topology:
| Topology | Parallel Groups | Formula |
|---|---|---|
| Linear | 2 | Always 2 (odd/even pairs) |
| Circular | 2 or 3 | 2 if n even, 3 if n odd |
| Full | n-1 or n | n-1 if n even, n if n odd |
# Verify depth computation for different topologies
for n in [3, 4, 5, 6]:
print(f"\nn = {n} qubits:")
for topology in ['linear', 'circular', 'full']:
for htype in ['iqp', 'xy', 'heisenberg']:
enc_d = HamiltonianEncoding(
n_features=n, hamiltonian_type=htype,
entanglement=topology, reps=1
)
print(
f" {topology:>8} / {htype:<12}: "
f"depth={enc_d.depth:>3}, "
f"parallel_groups={enc_d._compute_parallel_layers()}"
)
n = 3 qubits:
linear / iqp : depth= 8, parallel_groups=2
linear / xy : depth= 26, parallel_groups=2
linear / heisenberg : depth= 32, parallel_groups=2
circular / iqp : depth= 11, parallel_groups=3
circular / xy : depth= 38, parallel_groups=3
circular / heisenberg : depth= 47, parallel_groups=3
full / iqp : depth= 11, parallel_groups=3
full / xy : depth= 38, parallel_groups=3
full / heisenberg : depth= 47, parallel_groups=3
n = 4 qubits:
linear / iqp : depth= 8, parallel_groups=2
linear / xy : depth= 26, parallel_groups=2
linear / heisenberg : depth= 32, parallel_groups=2
circular / iqp : depth= 8, parallel_groups=2
circular / xy : depth= 26, parallel_groups=2
circular / heisenberg : depth= 32, parallel_groups=2
full / iqp : depth= 11, parallel_groups=3
full / xy : depth= 38, parallel_groups=3
full / heisenberg : depth= 47, parallel_groups=3
n = 5 qubits:
linear / iqp : depth= 8, parallel_groups=2
linear / xy : depth= 26, parallel_groups=2
linear / heisenberg : depth= 32, parallel_groups=2
circular / iqp : depth= 11, parallel_groups=3
circular / xy : depth= 38, parallel_groups=3
circular / heisenberg : depth= 47, parallel_groups=3
full / iqp : depth= 17, parallel_groups=5
full / xy : depth= 62, parallel_groups=5
full / heisenberg : depth= 77, parallel_groups=5
n = 6 qubits:
linear / iqp : depth= 8, parallel_groups=2
linear / xy : depth= 26, parallel_groups=2
linear / heisenberg : depth= 32, parallel_groups=2
circular / iqp : depth= 8, parallel_groups=2
circular / xy : depth= 26, parallel_groups=2
circular / heisenberg : depth= 32, parallel_groups=2
full / iqp : depth= 17, parallel_groups=5
full / xy : depth= 62, parallel_groups=5
full / heisenberg : depth= 77, parallel_groups=5
# Depth formula breakdown:
# depth = 1 (Hadamard) + reps * (single_qubit_depth + two_qubit_depth * parallel_groups)
# Gate depth constants
ZZ_DEPTH = 3 # CNOT, RZ, CNOT
XX_DEPTH = 5 # H, H, CNOT, RZ, CNOT, H, H
YY_DEPTH = 7 # Sdg, Sdg, H, H, CNOT, RZ, CNOT, H, H, S, S
enc_example = HamiltonianEncoding(n_features=4, hamiltonian_type='heisenberg', entanglement='full', reps=2)
pg = enc_example._compute_parallel_layers() # n-1 = 3 for n=4 even
expected_depth = 1 + 2 * (1 + (XX_DEPTH + YY_DEPTH + ZZ_DEPTH) * pg)
actual_depth = enc_example.depth
print(f"Heisenberg, n=4, full, reps=2:")
print(f" Parallel groups: {pg}")
print(f" Expected depth: 1 + 2*(1 + (5+7+3)*{pg}) = {expected_depth}")
print(f" Actual depth: {actual_depth}")
assert expected_depth == actual_depth, f"Mismatch: {expected_depth} != {actual_depth}"
Heisenberg, n=4, full, reps=2: Parallel groups: 3 Expected depth: 1 + 2*(1 + (5+7+3)*3) = 93 Actual depth: 93
# Depth is cached after first computation
enc_cached = HamiltonianEncoding(n_features=4, reps=3)
d1 = enc_cached.depth
d2 = enc_cached.depth
print(f"First access: {d1}")
print(f"Second access: {d2}")
print(f"Same value: {d1 == d2}")
First access: 31 Second access: 31 Same value: True
19. Statevector Simulation & Verification¶
Verify that the Hamiltonian encoding produces valid quantum states and that different configurations produce distinct states.
if HAS_PENNYLANE:
def get_statevector(encoding, x):
"""Get the statevector produced by an encoding."""
dev = qml.device('default.qubit', wires=encoding.n_qubits)
circuit_fn = encoding.get_circuit(x, backend='pennylane')
@qml.qnode(dev)
def circuit():
circuit_fn()
return qml.state()
return circuit()
enc = HamiltonianEncoding(n_features=3, hamiltonian_type='iqp', reps=2)
x = np.array([0.5, 0.3, 0.7])
state = get_statevector(enc, x)
print("=== Statevector Properties ===")
print(f"Dimension: {len(state)} (2^{enc.n_qubits} = {2**enc.n_qubits})")
print(f"Norm: {np.linalg.norm(state):.10f} (should be 1.0)")
print(f"Sum of probabilities: {np.sum(np.abs(state)**2):.10f}")
assert np.isclose(np.linalg.norm(state), 1.0), "State is not normalized!"
print("State is properly normalized.")
else:
print("PennyLane not available, skipping.")
=== Statevector Properties === Dimension: 8 (2^3 = 8) Norm: 1.0000000000 (should be 1.0) Sum of probabilities: 1.0000000000 State is properly normalized.
if HAS_PENNYLANE:
# Different inputs produce different states
enc = HamiltonianEncoding(n_features=3, hamiltonian_type='iqp', reps=2)
x1 = np.array([0.5, 0.3, 0.7])
x2 = np.array([0.1, 0.8, 0.4])
s1 = get_statevector(enc, x1)
s2 = get_statevector(enc, x2)
fidelity = np.abs(np.vdot(s1, s2))**2
print(f"State fidelity between x1 and x2: {fidelity:.6f}")
print(f"States are distinct: {not np.allclose(s1, s2)}")
State fidelity between x1 and x2: 0.037930 States are distinct: True
if HAS_PENNYLANE:
# Different Hamiltonian types produce different states for the same input
x = np.array([0.5, 0.3, 0.7])
states = {}
for htype in ['iqp', 'xy', 'heisenberg', 'pauli_z']:
enc_t = HamiltonianEncoding(n_features=3, hamiltonian_type=htype, reps=2)
states[htype] = get_statevector(enc_t, x)
print("Pairwise fidelities between Hamiltonian types:")
type_list = list(states.keys())
for i in range(len(type_list)):
for j in range(i + 1, len(type_list)):
t1, t2 = type_list[i], type_list[j]
fid = np.abs(np.vdot(states[t1], states[t2]))**2
print(f" {t1:>12} vs {t2:<12}: {fid:.6f}")
# IQP and Pauli-Z should produce the same state
print(f"\nIQP == Pauli-Z: {np.allclose(states['iqp'], states['pauli_z'])}")
Pairwise fidelities between Hamiltonian types:
iqp vs xy : 0.715412
iqp vs heisenberg : 0.064909
iqp vs pauli_z : 1.000000
xy vs heisenberg : 0.142644
xy vs pauli_z : 0.715412
heisenberg vs pauli_z : 0.064909
IQP == Pauli-Z: True
20. Quantum Kernel Computation¶
A key use case for Hamiltonian encoding is computing quantum kernels:
$$k(x, x') = |\langle \psi(x) | \psi(x') \rangle|^2$$
if HAS_PENNYLANE:
def compute_kernel_matrix(encoding, X):
"""Compute the quantum kernel matrix for a dataset."""
n_samples = X.shape[0]
# Get all statevectors
statevectors = []
for i in range(n_samples):
sv = get_statevector(encoding, X[i])
statevectors.append(sv)
# Compute kernel matrix
K = np.zeros((n_samples, n_samples))
for i in range(n_samples):
for j in range(n_samples):
K[i, j] = np.abs(np.vdot(statevectors[i], statevectors[j]))**2
return K
# Create a small dataset
rng = np.random.default_rng(42)
X_kernel = rng.uniform(0, 1, size=(6, 3))
enc = HamiltonianEncoding(n_features=3, hamiltonian_type='iqp', reps=2)
K = compute_kernel_matrix(enc, X_kernel)
print("Quantum Kernel Matrix (IQP, 6 samples):")
print(np.array2string(K, precision=4, suppress_small=True))
# Verify kernel properties
print(f"\nDiagonal elements (should be 1.0): {np.diag(K)}")
print(f"Symmetric: {np.allclose(K, K.T)}")
print(f"All eigenvalues >= 0 (PSD): {np.all(np.linalg.eigvalsh(K) >= -1e-10)}")
else:
print("PennyLane not available, skipping kernel computation.")
Quantum Kernel Matrix (IQP, 6 samples): [[1. 0.1762 0.1332 0.1535 0.0579 0.0949] [0.1762 1. 0.2217 0.4534 0.1406 0.0458] [0.1332 0.2217 1. 0.1926 0.3204 0.311 ] [0.1535 0.4534 0.1926 1. 0.0208 0.0362] [0.0579 0.1406 0.3204 0.0208 1. 0.4394] [0.0949 0.0458 0.311 0.0362 0.4394 1. ]] Diagonal elements (should be 1.0): [1. 1. 1. 1. 1. 1.] Symmetric: True All eigenvalues >= 0 (PSD): True
if HAS_PENNYLANE:
# Compare kernels from different Hamiltonian types
rng = np.random.default_rng(42)
X_small = rng.uniform(0, 1, size=(5, 3))
print("Kernel matrix Frobenius norms (measure of kernel spread):")
for htype in ['iqp', 'xy', 'heisenberg']:
enc_k = HamiltonianEncoding(n_features=3, hamiltonian_type=htype, reps=2)
K_t = compute_kernel_matrix(enc_k, X_small)
# Off-diagonal elements indicate how distinguishable samples are
off_diag = K_t[np.triu_indices_from(K_t, k=1)]
print(f" {htype:<12}: mean off-diag={np.mean(off_diag):.4f}, std={np.std(off_diag):.4f}")
Kernel matrix Frobenius norms (measure of kernel spread): iqp : mean off-diag=0.1870, std=0.1186 xy : mean off-diag=0.2117, std=0.1206 heisenberg : mean off-diag=0.8047, std=0.0656
21. Comparing Hamiltonian Types Empirically¶
Let's compare all four Hamiltonian types across multiple dimensions.
n_features = 4
reps = 2
print(f"Comprehensive comparison (n_features={n_features}, reps={reps}, entanglement='full')")
print("=" * 90)
header = (
f"{'Type':<14} {'Qubits':>6} {'Depth':>6} {'Total':>6} {'1Q':>6} "
f"{'2Q':>6} {'CNOT':>6} {'H':>6} {'RZ':>6} {'S':>6} {'Pairs':>6}"
)
print(header)
print("-" * 90)
for htype in ['iqp', 'pauli_z', 'xy', 'heisenberg']:
enc = HamiltonianEncoding(n_features=n_features, hamiltonian_type=htype, reps=reps)
bd = enc.gate_count_breakdown()
props = enc.properties
n_pairs = len(enc.get_entanglement_pairs())
print(
f"{htype:<14} {props.n_qubits:>6} {props.depth:>6} {bd['total']:>6} "
f"{bd['total_single_qubit']:>6} {bd['total_two_qubit']:>6} {bd['cnot']:>6} "
f"{bd['hadamard']:>6} {bd['rz']:>6} {bd['s_gates']:>6} {n_pairs:>6}"
)
Comprehensive comparison (n_features=4, reps=2, entanglement='full') ========================================================================================== Type Qubits Depth Total 1Q 2Q CNOT H RZ S Pairs ------------------------------------------------------------------------------------------ iqp 4 21 48 24 24 24 4 20 0 6 pauli_z 4 21 48 24 24 24 4 20 0 6 xy 4 75 228 180 48 48 100 32 48 6 heisenberg 4 93 264 192 72 72 100 44 48 6
# Compare entanglement topologies for IQP
print(f"\nTopology comparison (IQP, n_features=6, reps=2)")
print("=" * 70)
print(f"{'Topology':<12} {'Pairs':>6} {'Depth':>6} {'CNOTs':>6} {'Total':>6} {'Connectivity':>15}")
print("-" * 70)
for topology in ['linear', 'circular', 'full']:
enc = HamiltonianEncoding(n_features=6, hamiltonian_type='iqp', entanglement=topology, reps=2)
bd = enc.gate_count_breakdown()
summary = enc.resource_summary()
conn = summary['hardware_requirements']['connectivity']
print(
f"{topology:<12} {len(enc.get_entanglement_pairs()):>6} "
f"{enc.depth:>6} {bd['cnot']:>6} {bd['total']:>6} {conn:>15}"
)
Topology comparison (IQP, n_features=6, reps=2) ====================================================================== Topology Pairs Depth CNOTs Total Connectivity ---------------------------------------------------------------------- linear 5 15 20 48 linear circular 6 15 24 54 ring full 15 33 60 108 all-to-all
22. Scaling & Performance Considerations¶
# Scaling: gate count and depth vs number of features
print("Scaling of IQP encoding with full entanglement:")
print(f"{'n':>4} {'Pairs':>6} {'CNOTs':>8} {'Total Gates':>12} {'Depth':>6}")
print("-" * 42)
for n in [2, 4, 6, 8, 10, 12, 16, 20]:
enc = HamiltonianEncoding(n_features=n, hamiltonian_type='iqp', entanglement='full', reps=2)
bd = enc.gate_count_breakdown()
n_pairs = len(enc.get_entanglement_pairs())
print(f"{n:>4} {n_pairs:>6} {bd['cnot']:>8} {bd['total']:>12} {enc.depth:>6}")
Scaling of IQP encoding with full entanglement: n Pairs CNOTs Total Gates Depth ------------------------------------------ 2 1 4 12 9 4 6 24 48 21 6 15 60 108 33 8 28 112 192 45 10 45 180 300 57 12 66 264 432 69 16 120 480 768 93 20 190 760 1200 117
# Scaling: linear entanglement keeps gates O(n)
print("Scaling of IQP with LINEAR entanglement:")
print(f"{'n':>4} {'Pairs':>6} {'CNOTs':>8} {'Total Gates':>12} {'Depth':>6}")
print("-" * 42)
for n in [2, 4, 6, 8, 10, 12, 16, 20]:
enc = HamiltonianEncoding(n_features=n, hamiltonian_type='iqp', entanglement='linear', reps=2)
bd = enc.gate_count_breakdown()
n_pairs = len(enc.get_entanglement_pairs())
print(f"{n:>4} {n_pairs:>6} {bd['cnot']:>8} {bd['total']:>12} {enc.depth:>6}")
Scaling of IQP with LINEAR entanglement: n Pairs CNOTs Total Gates Depth ------------------------------------------ 2 1 4 12 9 4 3 12 30 15 6 5 20 48 15 8 7 28 66 15 10 9 36 84 15 12 11 44 102 15 16 15 60 138 15 20 19 76 174 15
# Large system warning (>20 qubits with full entanglement)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_large = HamiltonianEncoding(n_features=25, entanglement='full', reps=1)
for warning in w:
if 'SCALABILITY' in str(warning.message):
msg = str(warning.message)
print(f"Scalability warning for n=25, full:\n{msg[:200]}...")
break
Scalability warning for n=25, full: SCALABILITY WARNING: Full entanglement with 25 qubits creates 300 qubit pairs (O(n²) scaling). Current configuration: 300 pairs × 1 reps = 300 two-qubit interaction layers. RECOMMENDED MITIGATION...
# Large evolution_time with few reps triggers accuracy warning
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
enc_bad_trotter = HamiltonianEncoding(n_features=4, evolution_time=200.0, reps=3)
for warning in w:
if 'evolution_time' in str(warning.message).lower() or 'trotter' in str(warning.message).lower():
print(f"Trotter accuracy warning: {warning.message}")
break
Trotter accuracy warning: Large evolution_time (200.0) with few Trotter steps (3) may result in poor Trotterization accuracy. Consider increasing reps.
# Performance: timing circuit generation
import time
enc = HamiltonianEncoding(n_features=6, hamiltonian_type='iqp', reps=2)
X_perf = np.random.default_rng(42).uniform(0, 1, size=(100, 6))
# Sequential
t0 = time.perf_counter()
circuits_seq = enc.get_circuits(X_perf, backend='pennylane')
t_seq = time.perf_counter() - t0
# Parallel
t0 = time.perf_counter()
circuits_par = enc.get_circuits(X_perf, backend='pennylane', parallel=True)
t_par = time.perf_counter() - t0
print(f"100 circuits (n=6, reps=2):")
print(f" Sequential: {t_seq:.4f}s")
print(f" Parallel: {t_par:.4f}s")
print(f" Note: For lightweight PennyLane closures, parallel overhead may exceed benefit.")
print(f" Parallel is more beneficial for Qiskit/Cirq backends with larger circuits.")
100 circuits (n=6, reps=2): Sequential: 0.0079s Parallel: 0.0779s Note: For lightweight PennyLane closures, parallel overhead may exceed benefit. Parallel is more beneficial for Qiskit/Cirq backends with larger circuits.
23. Summary & Quick Reference¶
Constructor Parameters¶
HamiltonianEncoding(
n_features: int, # Required: 1+
hamiltonian_type: str = 'iqp', # 'iqp', 'pauli_z', 'xy', 'heisenberg'
evolution_time: float = 1.0, # Any finite float (incl. negative)
reps: int = 2, # >= 1 (Trotter steps)
entanglement: str = 'full', # 'full', 'linear', 'circular'
insert_barriers: bool = True, # Qiskit-only barriers
max_pairs: int | None = None, # Limit entanglement pairs
include_single_qubit_terms: bool = True, # Z_i terms (XY/Heisenberg only)
)
Key Methods¶
| Method | Returns | Description |
|---|---|---|
get_circuit(x, backend) |
Circuit | Single sample circuit |
get_circuits(X, backend, parallel, max_workers) |
List[Circuit] | Batch circuits |
iter_circuits(X, backend) |
Generator | Memory-efficient batch |
get_entanglement_pairs() |
List[Tuple] | Qubit interaction pairs |
gate_count_breakdown() |
GateCountBreakdown | Upper-bound gate counts |
count_gates(x) |
Dict | Exact gate counts for input |
resource_summary() |
Dict | Comprehensive resource analysis |
Key Properties¶
| Property | Type | Description |
|---|---|---|
n_features |
int | Number of input features |
n_qubits |
int | = n_features |
depth |
int | Exact circuit depth (cached) |
properties |
EncodingProperties | Full property dataclass |
config |
dict | Configuration (read-only copy) |
Rotation Angle Formulas¶
- Single-qubit: $\text{angle} = \frac{t}{r} \cdot x_i$
- Two-qubit: $\text{angle} = \frac{t}{r} \cdot (\pi - x_i)(\pi - x_j)$
Data Preprocessing¶
- Recommended: Scale features to $[0, 1]$ or $[-1, 1]$
- Avoid: Ranges including $\pi \approx 3.14159$ in the interior
- Zero features: Produce zero single-qubit rotation (gates skipped)
- Features = $\pi$: Produce zero two-qubit interaction (critical point)
# Final validation: create every valid configuration combination
count = 0
for htype in ['iqp', 'xy', 'heisenberg', 'pauli_z']:
for topology in ['full', 'linear', 'circular']:
for reps in [1, 2, 3]:
for single_q in [True, False]:
enc = HamiltonianEncoding(
n_features=4,
hamiltonian_type=htype,
entanglement=topology,
reps=reps,
include_single_qubit_terms=single_q,
)
# Verify properties compute without error
_ = enc.properties
_ = enc.gate_count_breakdown()
_ = enc.resource_summary()
_ = enc.get_entanglement_pairs()
count += 1
print(f"Successfully validated {count} configuration combinations!")
print(f"(4 types x 3 topologies x 3 reps x 2 single_q = {4*3*3*2})")
Successfully validated 72 configuration combinations! (4 types x 3 topologies x 3 reps x 2 single_q = 72)
# Final validation: generate circuits for all configs with all backends
x_final = np.array([0.5, 0.3, 0.7, 0.1])
backends_available = []
if HAS_PENNYLANE:
backends_available.append('pennylane')
if HAS_QISKIT:
backends_available.append('qiskit')
if HAS_CIRQ:
backends_available.append('cirq')
circuit_count = 0
for htype in ['iqp', 'xy', 'heisenberg', 'pauli_z']:
for topology in ['full', 'linear', 'circular']:
enc = HamiltonianEncoding(n_features=4, hamiltonian_type=htype, entanglement=topology, reps=1)
for backend in backends_available:
circuit = enc.get_circuit(x_final, backend=backend)
circuit_count += 1
print(f"Generated {circuit_count} circuits across all type/topology/backend combinations!")
print(f"Backends tested: {backends_available}")
print("\nAll tests passed. The Hamiltonian encoding is working correctly.")
Generated 36 circuits across all type/topology/backend combinations! Backends tested: ['pennylane', 'qiskit', 'cirq'] All tests passed. The Hamiltonian encoding is working correctly.