Source code for quscope.quantum_ctem.circuit_optimization

"""
Circuit optimization for quantum CTEM implementation.

This module provides tools to optimize quantum circuits for:
1. Reduced circuit depth (critical for NISQ devices)
2. Hardware-specific gate sets (IBM quantum devices)
3. Qubit connectivity constraints
4. Error mitigation strategies

Target Hardware: IBM Quantum devices (127+ qubits)
- ibm_kyoto: 127 qubits
- ibm_osaka: 127 qubits
- ibm_brisbane: 127 qubits

Author: QuScope Team
Date: October 2025
"""

from typing import Dict, List, Optional, Tuple, Union

import numpy as np
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import Initialize
from qiskit.quantum_info import Statevector
from qiskit.transpiler import CouplingMap, PassManager
from qiskit.transpiler.passes import (
    CommutativeCancellation,
    Depth,
    Optimize1qGates,
    RemoveBarriers,
)


[docs] class StatePreparationOptimizer: """ Optimize quantum state preparation circuits for hardware deployment. The state initialization circuit |0⟩^n → |ψ⟩ is often the deepest part of quantum algorithms. This class provides multiple strategies to reduce circuit depth while maintaining fidelity. Strategies: 1. Direct: Qiskit's built-in initialize() with optimization 2. Schmidt decomposition: Exploit entanglement structure 3. Variational: Parameterized circuits (requires training) 4. QGAN: Quantum GAN approach (requires training) For hardware deployment, we focus on Direct + transpilation. """ def __init__(self, method: str = "direct", optimization_level: int = 3): """ Initialize state preparation optimizer. Parameters ---------- method : str Preparation method: 'direct', 'schmidt', 'variational', 'qgan' optimization_level : int Qiskit transpilation optimization level (0-3) Level 3: Most aggressive optimization for hardware """ self.method = method self.optimization_level = optimization_level
[docs] def prepare_state( self, psi: np.ndarray, num_qubits: int, normalize: bool = True ) -> QuantumCircuit: """ Prepare quantum state |ψ⟩ from classical array. Parameters ---------- psi : np.ndarray Target state vector (flattened or 2D) num_qubits : int Number of qubits for the state normalize : bool Whether to normalize the state vector Returns ------- QuantumCircuit Optimized state preparation circuit Notes ----- For hardware execution, this circuit will be transpiled to the native gate set of the target device. IBM devices typically use {√X, X, RZ, CNOT} or {SX, RZ, ECR} basis. """ if self.method == "direct": return self._prepare_direct(psi, num_qubits, normalize) elif self.method == "schmidt": return self._prepare_schmidt(psi, num_qubits, normalize) elif self.method == "variational": raise NotImplementedError("Variational method requires training") elif self.method == "qgan": raise NotImplementedError("QGAN method requires training") else: raise ValueError(f"Unknown method: {self.method}")
def _prepare_direct( self, psi: np.ndarray, num_qubits: int, normalize: bool ) -> QuantumCircuit: """ Direct state preparation using Qiskit's initialize(). This uses the Shende-Bullock-Markov decomposition which gives O(2^n) gates but is exact. We then apply optimization passes. """ # Flatten if needed psi_flat = psi.flatten() # Validate size expected_size = 2**num_qubits if len(psi_flat) != expected_size: raise ValueError( f"State size {len(psi_flat)} doesn't match " f"2^{num_qubits} = {expected_size}" ) # Normalize if requested if normalize: norm = np.linalg.norm(psi_flat) if norm < 1e-10: # Zero state → uniform superposition qc = QuantumCircuit(num_qubits, name="uniform_prep") qc.h(range(num_qubits)) return qc psi_flat = psi_flat / norm # Create circuit with initialize instruction qc = QuantumCircuit(num_qubits, name="state_prep") qc.initialize(psi_flat, range(num_qubits)) # Decompose initialize into basic gates qc = qc.decompose() # Apply optimization passes qc_optimized = self._optimize_circuit(qc) return qc_optimized def _prepare_schmidt( self, psi: np.ndarray, num_qubits: int, normalize: bool ) -> QuantumCircuit: """ State preparation using Schmidt decomposition. For separable or low-entanglement states, this can significantly reduce circuit depth by exploiting tensor product structure. |ψ⟩ = Σᵢ λᵢ |φᵢ⟩_A ⊗ |χᵢ⟩_B If only a few λᵢ are significant, we can prepare with shallow circuits. """ # For 2D wave functions, split into x and y subsystems if psi.ndim == 2: # Check if state is approximately separable u, s, vh = np.linalg.svd(psi) # If state is highly separable (one dominant singular value) if s[0] / np.sum(s) > 0.99: # Prepare as product state return self._prepare_product_state(u[:, 0], vh[0, :], num_qubits) # Fall back to direct method return self._prepare_direct(psi, num_qubits, normalize) def _prepare_product_state( self, state_x: np.ndarray, state_y: np.ndarray, num_qubits: int ) -> QuantumCircuit: """ Prepare product state |φ⟩_x ⊗ |χ⟩_y efficiently. This reduces depth from O(2^n) to O(2^(n/2)) for separable states. """ n_x = num_qubits // 2 n_y = num_qubits - n_x qc = QuantumCircuit(num_qubits, name="product_prep") # Prepare x subsystem qc_x = QuantumCircuit(n_x) qc_x.initialize(state_x, range(n_x)) qc_x = qc_x.decompose() # Prepare y subsystem qc_y = QuantumCircuit(n_y) qc_y.initialize(state_y, range(n_y)) qc_y = qc_y.decompose() # Combine qc.compose(qc_x, range(n_x), inplace=True) qc.compose(qc_y, range(n_x, num_qubits), inplace=True) return self._optimize_circuit(qc) def _optimize_circuit(self, qc: QuantumCircuit) -> QuantumCircuit: """ Apply optimization passes to reduce circuit depth and gate count. These optimizations are hardware-agnostic and prepare the circuit for subsequent hardware-specific transpilation. """ # Create pass manager with optimization passes pm = PassManager( [ Optimize1qGates(), # Merge consecutive 1-qubit gates CommutativeCancellation(), # Cancel commuting gates RemoveBarriers(), # Remove unnecessary barriers ] ) # Run optimization qc_optimized = pm.run(qc) return qc_optimized
[docs] def get_circuit_metrics(self, qc: QuantumCircuit) -> Dict[str, int]: """ Get circuit complexity metrics. Returns ------- dict - 'depth': Circuit depth (critical for NISQ devices) - 'gates': Total gate count - '1q_gates': Single-qubit gate count - '2q_gates': Two-qubit gate count (most expensive) - 'qubits': Number of qubits used """ from qiskit.converters import circuit_to_dag dag = circuit_to_dag(qc) # Count gates by type gate_count = qc.count_ops() two_qubit_gates = [ "cx", "cz", "cy", "cp", "crx", "cry", "crz", "ecr", "swap", "iswap", "rxx", "ryy", "rzz", ] num_2q = sum(gate_count.get(g, 0) for g in two_qubit_gates) num_1q = sum(gate_count.values()) - num_2q return { "depth": qc.depth(), "gates": sum(gate_count.values()), "1q_gates": num_1q, "2q_gates": num_2q, "qubits": qc.num_qubits, }
[docs] class HardwareTranspiler: """ Transpile circuits for specific IBM quantum hardware. This class handles: 1. Gate set conversion to native gates 2. Qubit routing and SWAP insertion 3. Pulse-level optimization (optional) 4. Error mitigation preparation """ def __init__( self, backend_name: Optional[str] = None, optimization_level: int = 3, seed_transpiler: int = 42, ): """ Initialize hardware transpiler. Parameters ---------- backend_name : str, optional IBM backend name (e.g., 'ibm_kyoto', 'ibm_osaka') If None, uses a generic 127-qubit heavy-hex topology optimization_level : int Transpilation optimization (0-3), default 3 seed_transpiler : int Random seed for reproducible transpilation """ self.backend_name = backend_name self.optimization_level = optimization_level self.seed_transpiler = seed_transpiler # Define coupling map for target hardware # IBM's heavy-hex topology (simplified for now) self.coupling_map = self._get_coupling_map() def _get_coupling_map(self) -> Optional[CouplingMap]: """ Get qubit connectivity for target backend. For actual hardware execution, this would be loaded from the backend properties. For simulation, we use a generic all-to-all connectivity for small systems. """ if self.backend_name is None: # Generic all-to-all for testing (not realistic) return None # TODO: Load actual topology from IBM backend # For now, return None to allow all-to-all return None
[docs] def transpile_for_hardware( self, circuit: QuantumCircuit, initial_layout: Optional[List[int]] = None ) -> QuantumCircuit: """ Transpile circuit for target hardware. This converts the circuit to: 1. Native gate set (e.g., {SX, RZ, ECR} for IBM) 2. Hardware topology (insert SWAPs for non-adjacent qubits) 3. Optimized depth (minimize decoherence effects) Parameters ---------- circuit : QuantumCircuit High-level quantum circuit initial_layout : list, optional Initial qubit mapping to physical qubits Returns ------- QuantumCircuit Hardware-optimized transpiled circuit """ transpiled = transpile( circuit, coupling_map=self.coupling_map, optimization_level=self.optimization_level, seed_transpiler=self.seed_transpiler, initial_layout=initial_layout, ) return transpiled
[docs] def estimate_fidelity( self, circuit: QuantumCircuit, gate_error_1q: float = 1e-4, gate_error_2q: float = 1e-2, ) -> float: """ Estimate circuit fidelity on noisy hardware. Uses simple error model: F ≈ (1 - ε₁)^(n₁) × (1 - ε₂)^(n₂) where: - ε₁, ε₂: single/two-qubit gate errors - n₁, n₂: number of single/two-qubit gates Parameters ---------- circuit : QuantumCircuit Transpiled circuit gate_error_1q : float Single-qubit gate error rate (typical: 1e-4) gate_error_2q : float Two-qubit gate error rate (typical: 1e-2) Returns ------- float Estimated fidelity (0 to 1) """ metrics = StatePreparationOptimizer().get_circuit_metrics(circuit) n_1q = metrics["1q_gates"] n_2q = metrics["2q_gates"] # Simple error model fidelity = (1 - gate_error_1q) ** n_1q * (1 - gate_error_2q) ** n_2q return fidelity
[docs] def compare_strategies( self, circuits: Dict[str, QuantumCircuit] ) -> Dict[str, Dict]: """ Compare multiple implementation strategies. Parameters ---------- circuits : dict Dictionary of {strategy_name: circuit} Returns ------- dict Comparison metrics for each strategy """ results = {} for name, circuit in circuits.items(): # Transpile for hardware transpiled = self.transpile_for_hardware(circuit) # Get metrics metrics = StatePreparationOptimizer().get_circuit_metrics(transpiled) # Estimate fidelity fidelity = self.estimate_fidelity(transpiled) results[name] = { **metrics, "estimated_fidelity": fidelity, "circuit": transpiled, } return results
[docs] def benchmark_state_preparation( n_qubits_list: List[int], methods: List[str] = ["direct", "schmidt"] ) -> Dict: """ Benchmark state preparation methods across different system sizes. Parameters ---------- n_qubits_list : list List of qubit counts to test [2, 4, 6, 8, ...] methods : list Preparation methods to compare Returns ------- dict Benchmark results with circuit metrics """ results = {} for n_qubits in n_qubits_list: results[n_qubits] = {} # Create test state (Gaussian wave packet) size = 2**n_qubits x = np.linspace(-4, 4, size) psi_test = np.exp(-(x**2) / 2) psi_test /= np.linalg.norm(psi_test) for method in methods: try: optimizer = StatePreparationOptimizer(method=method) circuit = optimizer.prepare_state(psi_test, n_qubits) metrics = optimizer.get_circuit_metrics(circuit) results[n_qubits][method] = metrics except Exception as e: results[n_qubits][method] = {"error": str(e)} return results