Source code for quscope.simulations.quantum_utils

"""
Quantum Transform Utilities for TEM Simulations

This module contains all quantum circuit operations for implementing
Quantum Fourier Transforms (QFT) and Inverse Quantum Fourier Transforms (iQFT).
"""

import numpy as np

# Optional Qiskit imports with graceful fallback
QISKIT_AVAILABLE = True
try:
    from qiskit import QuantumCircuit, QuantumRegister  # type: ignore
    from qiskit.circuit.library import QFT  # type: ignore

    try:
        from qiskit_aer import AerSimulator  # type: ignore
    except Exception:  # Aer may be a separate optional package
        AerSimulator = None  # type: ignore
    # Support both modern and legacy import paths for Statevector
    try:
        from qiskit.quantum_info import Statevector  # type: ignore
    except Exception:
        try:
            from qiskit.quantum.info import Statevector  # type: ignore
        except Exception:
            Statevector = None  # type: ignore
except Exception:
    QISKIT_AVAILABLE = False
    QuantumCircuit = QuantumRegister = QFT = AerSimulator = Statevector = None  # type: ignore


[docs] class TEMQFT: """ Class containing quantum transform operations for CTEM simulations. """ def __init__(self, n_qubits=8): """ Initialize quantum transforms. Parameters: ----------- n_qubits : int Number of qubits used for the circuit. """ if not QISKIT_AVAILABLE or AerSimulator is None or Statevector is None: raise ImportError( "Qiskit and qiskit-aer are required for TEMQFT. Install with:" " pip install qiskit qiskit-aer" ) self.n_qubits = n_qubits self.backend = AerSimulator()
[docs] def encode_to_quantum_state(self, data_1d): """ Encode classical 1D array into quantum state amplitudes. Parameters: ----------- data_1d : np.ndarray 1D numpy array of length 2^n_qubits to encode. Returns: -------- circuit : QuantumCircuit Quantum circuit with encoded data. norm : float Normalization factor. """ n = len(data_1d) if n != 2**self.n_qubits: raise ValueError( f"Data length {n} must equal 2^{self.n_qubits} = {2**self.n_qubits}" ) # Calculate norm for later restoration norm = np.linalg.norm(data_1d) # Handle zero or near-zero data if norm < 1e-10: # Create uniform superposition for zero data normalized_data = np.ones(n, dtype=complex) / np.sqrt(n) norm = 0.0 # Mark as zero for restoration else: normalized_data = data_1d / norm # Create quantum circuit and initialize qreg = QuantumRegister(self.n_qubits, "q") circuit = QuantumCircuit(qreg) circuit.initialize(normalized_data, qreg) return circuit, norm
[docs] def apply_qft(self, circuit, qubits): """ Apply Quantum Fourier Transform to specified qubits. Parameters: ----------- circuit : QuantumCircuit Quantum circuit. qubits : list List of qubit indices. Returns: -------- circuit : QuantumCircuit Circuit with QFT applied. """ qft = QFT(num_qubits=len(qubits), do_swaps=True) circuit.compose(qft, qubits, inplace=True) return circuit
[docs] def apply_iqft(self, circuit, qubits): """ Apply Inverse Quantum Fourier Transform to specified qubits. Parameters: ----------- circuit : QuantumCircuit Quantum circuit. qubits : list List of qubit indices. Returns: -------- circuit : QuantumCircuit Circuit with iQFT applied. """ iqft = QFT(num_qubits=len(qubits), do_swaps=True).inverse() circuit.compose(iqft, qubits, inplace=True) return circuit
[docs] def decode_quantum_state(self, circuit): """ Decode quantum state back to classical data. Parameters: ----------- circuit : QuantumCircuit Quantum circuit to decode. Returns: -------- amplitudes : np.ndarray Complex array of amplitudes. """ statevector = Statevector.from_instruction(circuit) amplitudes = statevector.data return amplitudes
[docs] def qft_1d(self, data_1d): """ Perform 1D QFT on classical data. Parameters: ----------- data_1d : np.ndarray 1D complex array. Returns: -------- transformed_data : np.ndarray QFT result with proper normalization. """ # Encode data circuit, norm = self.encode_to_quantum_state(data_1d) # Apply iQFT qubits = list(range(self.n_qubits)) self.apply_qft(circuit, qubits) # Decode result amplitudes = self.decode_quantum_state(circuit) # Restore normalization (handle zero case) if norm == 0.0: return np.zeros_like(data_1d) else: # QFT includes 1/sqrt(N) factor in normalization # FFT convention does not have this N = len(data_1d) return amplitudes * norm * np.sqrt(N)
[docs] def iqft_1d(self, data_1d): """ Perform 1D iQFT on classical data. Parameters: ----------- data_1d : np.ndarray 1D complex array. Returns: -------- transformed_data : np.ndarray iQFT result with proper normalization. """ # Encode data circuit, norm = self.encode_to_quantum_state(data_1d) # Apply iQFT qubits = list(range(self.n_qubits)) self.apply_iqft(circuit, qubits) # Decode result amplitudes = self.decode_quantum_state(circuit) # Restore normalization (handle zero case) if norm == 0.0: return np.zeros_like(data_1d) else: # QFT includes 1/sqrt(N) factor in normalization # FFT convention does not have this N = len(data_1d) return amplitudes * norm / np.sqrt(N)
[docs] def qft_2d(self, data_2d, progress=False): """ Perform 2D QFT using row-column decomposition. Parameters: ----------- data_2d : np.ndarray 2D complex array. progress : bool Print progress messages. Default is False. Returns: -------- transformed_data : np.ndarray 2D QFT result. """ result = np.zeros_like(data_2d, dtype=complex) # QFT on rows if progress: print(f" Applying QFT to {data_2d.shape[0]} rows...") for i in range(data_2d.shape[0]): if progress and i % 32 == 0: print(f" Row {i}/{data_2d.shape[0]}") result[i, :] = self.qft_1d(data_2d[i, :]) # QFT on columns if progress: print(f" Applying QFT to {data_2d.shape[1]} columns...") for j in range(data_2d.shape[1]): if progress and j % 32 == 0: print(f" Column {j}/{data_2d.shape[1]}") result[:, j] = self.qft_1d(result[:, j]) return result
[docs] def iqft_2d(self, data_2d, progress=False): """ Perform 2D iQFT using row-column decomposition. Parameters: ----------- data_2d : np.ndarray 2D complex array. progress : bool Print progress message. Default is False. Returns: -------- transformed_data : np.ndarray 2D iQFT result. """ result = np.zeros_like(data_2d, dtype=complex) # iQFT on rows if progress: print(f" Applying iQFT to {data_2d.shape[0]} rows...") for i in range(data_2d.shape[0]): if progress and i % 32 == 0: print(f" Row {i}/{data_2d.shape[0]}") result[i, :] = self.iqft_1d(data_2d[i, :]) # iQFT on columns if progress: print(f" Applying iQFT to {data_2d.shape[1]} columns...") for j in range(data_2d.shape[1]): if progress and j % 32 == 0: print(f" Column {j}/{data_2d.shape[1]}") result[:, j] = self.iqft_1d(result[:, j]) return result