Source code for ingenii_quantum.hybrid_networks.edge_detection

import numpy as np
import torch

from itertools import product, permutations
from math import sqrt
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from tqdm import tqdm
from time import time

from .utils import roll_numpy, roll_torch

[docs] class EdgeDetectorBase: ''' Base class for quantum-inspired edge detection. This class implements both Pytorch-based and Qiskit-based quantum Hadamard edge detection algorithms for images and 3D volumes. Attributes: n_dimensions (int): Number of dimensions (2D or 3D). size (int): Size of the (NxN) blocks used for partitioning the image. backend (str): Computational backend ('torch' for PyTorch, 'aer_simulator' for Qiskit). shots (int): Number of shots for Qiskit experiments. total_qb (int): Total number of qubits used in the quantum circuit. anc_qb (int): Number of ancilla qubits. data_qb (int): Number of qubits required for data encoding. device (torch.device, optional): PyTorch device ('cpu' or 'cuda'), only for `torch` backend. ''' def __init__(self, n_dimensions: int, size: int, backend: str, shots: int): """ Initializes the EdgeDetectorBase class. Args: n_dimensions (int): Number of dimensions (2D or 3D). size (int): Size of the (NxN) blocks for partitioning the image. backend (str): 'torch' for PyTorch or 'aer_simulator' for Qiskit. shots (int): Number of shots for Qiskit experiments. Raises: ValueError: If an invalid backend is provided. """ self.n_dimensions = n_dimensions # 1. Circuit parameters: data qubits ancilla qubits self.size = size self.shape = (size,) * n_dimensions self.data_qb = int(np.log2(self.size**n_dimensions)) self.anc_qb = 1 self.total_qb = self.data_qb + self.anc_qb self.backend = backend # Running in Pytorch if backend == 'torch': # 1. set CUDA for PyTorch use_cuda = torch.cuda.is_available() if use_cuda: self.device = torch.device("cuda:0") torch.cuda.set_device(int("cuda:0".split(':')[1])) else: self.device = torch.device("cpu") # 2. Quantum operations: Hadamard and rolling Identity hadamard = 1/sqrt(2)*torch.tensor( [[1, 1], [1, -1]], dtype=torch.float32 ).to(self.device) self.hadamard_large = torch.kron( torch.eye(2**self.data_qb), hadamard ).reshape( 1, 2**self.total_qb, 2**self.total_qb ).to(self.device) # Initialize the amplitude permutation unitary self.D2n = torch.roll( torch.eye(2**self.total_qb, dtype=torch.float32), 1, 1 ).reshape( 1, 2**self.total_qb, 2**self.total_qb ).to(self.device) # Running in a Qiskit environment elif backend=='aer_simulator': self.backend = AerSimulator() elif type(backend) == str: raise ValueError('The only valid backend names are "torch" and "aer_simulator". You can also provide a Qiskit Backend directly.') self.shots = shots # Quantum operations: rolling Identity self.D2n = np.roll(np.identity(2**self.total_qb), 1, axis=1)
[docs] def run_box(self, box, tol=1e-3): """ Runs the edge detection algorithm for a single box of the image. This function executes a quantum Hadamard-based edge detection algorithm for a given image segment. Args: box (np.array): Input image segment. tol (float, optional): Threshold for detecting edges. Defaults to 1e-3. Returns: np.array: Edge-detected version of the input box. """ # Create quantum circuit qc = QuantumCircuit(self.total_qb) # Data encoding: amplitude encoding box = box.flatten() initial_state = np.zeros(2**self.data_qb) initial_state[:box.shape[0]] = box initial_state = initial_state/np.linalg.norm(initial_state) qc.initialize(initial_state, range(1, self.total_qb)) # Apply quantum operations qc.h(0) qc.unitary(self.D2n, range(self.total_qb)) qc.h(0) # Measure qc.measure_all() # Run quantum circuit is_simulator = self.backend.configuration().simulator # Check if we're using a simulator or quantum hardware with Session(backend=self.backend) as session: # Create a session with such backend sampler = Sampler(mode=session) sampler.options.default_shots = self.shots # If running on real hardware (not a simulator), apply error mitigation if not is_simulator: sampler.options.dynamical_decoupling.enable = True sampler.options.dynamical_decoupling.sequence_type = "XY4" sampler.options.twirling.enable_gates = True sampler.options.twirling.num_randomizations = "auto" # Transpile circuit for noisy basis gates passmanager = generate_preset_pass_manager( optimization_level=2, backend=self.backend ) qc_noise = passmanager.run(qc) pub = (qc_noise,) # Prepare input job = sampler.run([pub]) counts = job.result()[0].data.meas.get_counts() # Get statevector from counts # Calculate binary string of basis qubits # Let's calculate the ordered statevector new_counts = [ counts.get( ''.join(map(str, qbits)), 0 ) # Generate string link e.g. '000', '001', ... for qbits in product([0, 1], repeat=self.total_qb) ] statevector = np.array(new_counts)/np.sum(new_counts) # Get odd values from state final_state = statevector[range(1, 2**(self.data_qb + 1), 2)] # Select values larger than threshold edge_scan = np.zeros(final_state.shape) edge_scan[np.abs(final_state) > tol] = 1 # Revert to box shape result = edge_scan[:self.size**self.n_dimensions].reshape(self.shape) return result
[docs] def run_image_qiskit(self, data, tol=1e-3, reduce=True, verbose=False): """ Runs the edge detection algorithm for the entire image using Qiskit. Args: data (np.array): Input image. tol (float, optional): Threshold for detecting edges. Defaults to 1e-3. reduce (bool, optional): If True, reduces the image dimensions by half. Defaults to True. verbose (bool, optional): If True, displays progress using tqdm. Defaults to False. Returns: np.array: Edge-detected image. """ # 1. Get view of image # We split it into bits of (4x4x4) windows, shape_aux = roll_numpy( data, np.zeros((self.size,) * self.n_dimensions), dx=self.size, dy=self.size, dz=self.size if self.n_dimensions == 3 else None ) windows = windows.reshape((-1,) + (self.size,) * self.n_dimensions) # 2. Run every box thorugh the QC def get_box_result(box): if np.sum(box) > tol: return self.run_box(box, tol) else: return np.zeros(self.shape) if verbose: results = [ get_box_result(windows[i]) for i in tqdm(range(windows.shape[0])) ] else: results = [ get_box_result(windows[i]) for i in range(windows.shape[0]) ] # 3. Reshape image to original shape results = np.array(results).reshape(shape_aux) transpose_idxs_map = { 4: [0, 2, 1, 3], 5: [0, 1, 3, 2, 4], 6: [0, 3, 1, 4, 2, 5], 7: [0, 1, 4, 2, 5, 3, 6], 8: [0, 1, 2, 5, 3, 6, 4, 7], } transpose_idxs = transpose_idxs_map[len(results.shape)] reverted_image = results.transpose(transpose_idxs).reshape(data.shape) if not reduce: return reverted_image # 4. Reduce image size size_x = data.shape[-1] reverted_image_small = reverted_image[ :, :, :, :, range(1, size_x, 2) ][ :, :, :, range(1, size_x, 2), : ] if self.n_dimensions == 3: reverted_image_small = \ reverted_image_small[:, :, range(1, size_x, 2), :, :] return reverted_image_small
[docs] def run_image_torch(self, data, tol=1e-3, reduce=True, verbose=False): """ Runs edge detection for the entire image using PyTorch. Args: data (torch.Tensor): Input image. tol (float, optional): Threshold for detecting edges. Defaults to 1e-3. reduce (bool, optional): If True, reduces the image dimensions by half. Defaults to True. verbose (bool, optional): If True, displays progress using tqdm. Defaults to False. Returns: torch.Tensor: Edge-detected image. """ if verbose: start_time = time() if self.n_dimensions == 2: axis = (1, 2) reshape_idxs = (-1, 1, 1) normalized_reshape_idxs = (-1, self.size**self.n_dimensions) windows_reshape_idxs = (-1, self.size, self.size) else: axis = (2, 3, 4) samples = data.shape[0] reshape_idxs = (samples, -1, 1, 1, 1) normalized_reshape_idxs = \ (samples, -1, self.size**self.n_dimensions) windows_reshape_idxs = \ (samples, -1, self.size, self.size, self.size) # 1. Get view of image # We split it into bits of (4x4x4) windows, shape_aux = roll_torch( data, torch.zeros((self.size,) * self.n_dimensions), dx=self.size, dy=self.size, dz=self.size if self.n_dimensions == 3 else None ) windows = windows.reshape(windows_reshape_idxs).to(self.device) # 2. Data encoding: amplitude encoding norm_const = windows.square().sum(axis=axis) \ .sqrt().reshape(*reshape_idxs) norm_const[torch.abs(norm_const) < 1e-16] = 1 normalized_image = windows/norm_const # 3. Flatten the last 2 dimesions to obtain an initial state normalized_flatten = normalized_image.reshape(normalized_reshape_idxs) # 4. Kronecker product to increase the last dimension 1 qubit normalized_larger = torch.kron( torch.tensor(normalized_flatten, dtype=torch.float32), torch.tensor([1, 0]).to(self.device) ) self.D2n = torch.tensor(self.D2n, dtype = torch.float32).to(self.device) # 5. Apply quantum operations final_state_h = ( normalized_larger @ self.hadamard_large @ self.D2n
[docs] @ self.hadamard_large )[:, :, range(1, 2**(self.data_qb+1), 2)] # 6. Select values larger than threshold edge_scan_h = torch.zeros(final_state_h.shape).to(self.device) edge_scan_h[torch.abs(final_state_h) > tol] = 1 # 7. Reshape image to original shape permutations_idxs = { 2: [0, 1, 3, 2, 4], 3: [0, 1, 2, 5, 3, 6, 4, 7], } reverted_image = edge_scan_h \ .reshape(shape_aux) \ .permute(permutations_idxs[self.n_dimensions]) \ .reshape(data.shape) if self.n_dimensions == 3: reverted_image[:, :, :, :, 0] = 0 # 8. Reduce image size if reduce: size_x = data.shape[-1] reverted_image = reverted_image[ :, :, :, :, range(1, size_x, 2) ][ :, :, :, range(1, size_x, 2), : ] if self.n_dimensions == 3: reverted_image = \ reverted_image[:, :, range(1, size_x, 2), :, :] if verbose: print('Total execution time: ', time() - start_time) return reverted_image
############################################################################### # QUANTUM EDGE DETECTION ALGORITHM FOR 2D IMAGES ############################################################################### class EdgeDetector2D(EdgeDetectorBase): """ Quantum Hadamard Edge Detection algorithm for 2D images. This class implements both Qiskit-based and PyTorch-based quantum edge detection algorithms for two-dimensional images. Attributes: size (int): Size of the (NxN) blocks used for partitioning the image. backend (str): Computational backend ('torch' for PyTorch, 'aer_simulator' for Qiskit). shots (int): Number of shots for Qiskit experiments. """ def __init__(self, size, backend='aer_simulator', shots=1000): """ Initializes the 2D Quantum Hadamard Edge Detection algorithm. Args: size (int): Size of the (NxN) blocks for partitioning the image. backend (str, optional): 'torch' for PyTorch or 'aer_simulator' for Qiskit. Defaults to 'aer_simulator'. shots (int, optional): Number of shots for Qiskit experiments. Defaults to 1000. """ super().__init__( n_dimensions=2, size=size, backend=backend, shots=shots)
[docs] def run(self, data, tol=1e-3, reduce=True, verbose=False): """ Runs the edge detection algorithm with either Pytorch or Qiskit code Args: data (tensor/np.array): input data tol (tensor/np.array): Tolerance to be considered and edge reduce (bool): reduce the dimension by half at the end verbose (bool): If true, tqdm is used to show the evolution. Returns: np.array/tensor: Edge-detected image """ if self.backend == 'torch': return self.run_image_torch(data, tol, reduce, verbose) return self.run_image_qiskit(data, tol, reduce, verbose)
############################################################################### # QUANTUM EDGE DETECTION ALGORITHM FOR 3D VOLUMES ###############################################################################
[docs] class EdgeDetector3D(EdgeDetectorBase): """ Quantum Hadamard Edge Detection algorithm for 3D volumes. This class implements both Qiskit-based and PyTorch-based quantum edge detection algorithms for three-dimensional images. Attributes: size (int): Size of the (NxN) blocks used for partitioning the image. backend (str): Computational backend ('torch' for PyTorch, 'aer_simulator' for Qiskit). shots (int): Number of shots for Qiskit experiments. """ def __init__(self, size, backend='aer_simulator', shots=100): """ Initializes the 3D Quantum Hadamard Edge Detection algorithm. Args: size (int): Size of the (NxN) blocks for partitioning the image. backend (str, optional): 'torch' for PyTorch or 'aer_simulator' for Qiskit. Defaults to 'aer_simulator'. shots (int, optional): Number of shots for Qiskit experiments. Defaults to 100. """ super().__init__( n_dimensions=3, size=size, backend=backend, shots=shots)
[docs] def run(self, data, num_filters=6, tol=1e-3, reduce=True, verbose=False): ''' Run edge detection for different rotations of the image. Args: data (np.array): input data (samples, features, dim,dim,dim) num_filters (int): Number of rotations (permutations of the last 3 dimension) to output tol (float):Tolerance to consider an edge reduce (bool): reduce the size of the image at the end verbose (bool): If true, tqdm is used to show the evolution. Returns: np.array/tensor: Edge-detected image ''' if reduce: fin_shape = int(data.shape[-1] / 2) else: fin_shape = int(data.shape[-1]) perm = list(permutations([2, 3, 4])) if self.backend == 'torch': data_out = torch.zeros(( data.shape[0], data.shape[1]*num_filters, fin_shape, fin_shape, fin_shape)) else: data_out = np.zeros(( data.shape[0], data.shape[1]*num_filters, fin_shape, fin_shape, fin_shape)) for i in range(num_filters): p = [0, 1] + list(perm[i]) if self.backend == 'torch': data_out[:, data.shape[1]*i:data.shape[1]*(i+1), :, :, :] = \ self.run_image_torch( data.permute(p), tol, reduce, verbose).permute(p) else: data_out[:, data.shape[1]*i:data.shape[1]*(i+1), :, :, :] = \ self.run_image_qiskit( data.transpose(p), tol, reduce, verbose).transpose(p) return data_out