From 05ddfc943bb835419acc10ea2770b57be2b38c99 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 31 Mar 2025 22:14:42 +0900 Subject: [PATCH 01/30] refactor API structure --- graphix_ibmq/backend.py | 52 +++++ graphix_ibmq/compile_options.py | 10 + graphix_ibmq/compiler.py | 135 ++++++++++++ graphix_ibmq/executor.py | 35 +++ graphix_ibmq/job_handle.py | 16 ++ graphix_ibmq/result_utils.py | 9 + graphix_ibmq/runner.py | 374 -------------------------------- 7 files changed, 257 insertions(+), 374 deletions(-) create mode 100644 graphix_ibmq/backend.py create mode 100644 graphix_ibmq/compile_options.py create mode 100644 graphix_ibmq/compiler.py create mode 100644 graphix_ibmq/executor.py create mode 100644 graphix_ibmq/job_handle.py create mode 100644 graphix_ibmq/result_utils.py delete mode 100644 graphix_ibmq/runner.py diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py new file mode 100644 index 0000000..ebc1bfb --- /dev/null +++ b/graphix_ibmq/backend.py @@ -0,0 +1,52 @@ +from graphix.device_interface import DeviceBackend, CompileOptions, JobHandle +from graphix_ibmq.compiler import IBMQPatternCompiler +from graphix_ibmq.executor import IBMQJobExecutor +from graphix_ibmq.job_handle import IBMQJobHandle +from graphix_ibmq.compile_options import IBMQCompileOptions +from graphix_ibmq.result_utils import format_result + + +class IBMQBackend(DeviceBackend): + def __init__(self, instance: str = "ibm-q/open/main", resource: str = None): + super().__init__() + self.instance = instance + self.resource = resource + self._compiled_circuit = None + self._executor = None + self._register_dict = None + self._options = None + + def compile(self, options: CompileOptions = None) -> None: + if self.pattern is None: + raise ValueError("Pattern not set.") + if not isinstance(options, IBMQCompileOptions): + raise TypeError("Expected IBMQCompileOptions") + + self._options = options + compiler = IBMQPatternCompiler(self.pattern) + self._compiled_circuit = compiler.to_qiskit_circuit( + save_statevector=options.save_statevector, + layout_method=options.layout_method + ) + self._register_dict = compiler.register_dict + + def submit_job(self, shots: int = 1024) -> JobHandle: + if self._compiled_circuit is None: + raise RuntimeError("Pattern must be compiled before submission.") + + self._executor = IBMQJobExecutor(self._compiled_circuit) + self._executor.select_backend(self.instance, self.resource) + + job_result = self._executor.run(shots=shots, optimization_level=self._options.optimization_level) + return IBMQJobHandle(job_result) + + def retrieve_result(self, job_handle: JobHandle, raw_result: bool = False): + if not isinstance(job_handle, IBMQJobHandle): + raise TypeError("Expected IBMQJobHandle") + + result = job_handle.job.result() + counts = result[0].data.get_counts() + + if not raw_result: + return format_result(counts, self.pattern, self._register_dict) + return counts \ No newline at end of file diff --git a/graphix_ibmq/compile_options.py b/graphix_ibmq/compile_options.py new file mode 100644 index 0000000..0a1fa71 --- /dev/null +++ b/graphix_ibmq/compile_options.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from graphix.device_interface import CompileOptions + + +@dataclass +class IBMQCompileOptions(CompileOptions): + optimization_level: int = 1 + save_statevector: bool = False + layout_method: str = "dense" + \ No newline at end of file diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py new file mode 100644 index 0000000..c06d1a6 --- /dev/null +++ b/graphix_ibmq/compiler.py @@ -0,0 +1,135 @@ +import numpy as np +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile +from graphix_ibmq.clifford import CLIFFORD_CONJ, CLIFFORD_TO_QISKIT +from graphix.pattern import Pattern +from graphix.command import CommandKind +from graphix.fundamentals import Plane + +class IBMQPatternCompiler: + def __init__(self, pattern: Pattern): + self.pattern = pattern + self.register_dict = {} + + def to_qiskit_circuit(self, save_statevector: bool, layout_method: str): + from qiskit import QuantumCircuit + """convert the MBQC pattern to the qiskit cuicuit and add to attributes. + + Parameters + ---------- + save_statevector : bool, optional + whether to save the statevector before the measurements of output qubits. + """ + n = self.pattern.max_space() + N_node = self.pattern.n_node + + qr = QuantumRegister(n) + cr = ClassicalRegister(N_node) + circ = QuantumCircuit(qr, cr) + + empty_qubit = [i for i in range(n)] # list of free qubit indices + qubit_dict = {} # dictionary to record the correspondance of pattern nodes and circuit qubits + register_dict = {} # dictionary to record the correspondance of pattern nodes and classical registers + reg_idx = 0 # index of classical register + + def signal_process(op, circ_idx, signal): + if op == "X": + for s in signal: + if s in register_dict.keys(): + s_idx = register_dict[s] + with circ.if_test((cr[s_idx], 1)): + circ.x(circ_idx) + else: + if self.pattern.results[s] == 1: + circ.x(circ_idx) + if op == "Z": + for s in signal: + if s in register_dict.keys(): + s_idx = register_dict[s] + with circ.if_test((cr[s_idx], 1)): + circ.z(circ_idx) + else: + if self.pattern.results[s] == 1: + circ.z(circ_idx) + + for i in self.pattern.input_nodes: + circ_idx = empty_qubit[0] + empty_qubit.pop(0) + circ.reset(circ_idx) + circ.h(circ_idx) + qubit_dict[i] = circ_idx + + for cmd in self.pattern: + + if cmd.kind == CommandKind.N: + circ_idx = empty_qubit[0] + empty_qubit.pop(0) + circ.reset(circ_idx) + circ.h(circ_idx) + qubit_dict[cmd.node] = circ_idx + + if cmd.kind == CommandKind.E: + circ.cz(qubit_dict[cmd.nodes[0]], qubit_dict[cmd.nodes[1]]) + + if cmd.kind == CommandKind.M: + circ_idx = qubit_dict[cmd.node] + plane = cmd.plane + alpha = cmd.angle * np.pi + s_list = cmd.s_domain + t_list = cmd.t_domain + + if plane == Plane.XY: + # act p and h to implement non-Z-basis measurement + if alpha != 0: + signal_process("X", circ_idx, s_list) + circ.p(-alpha, circ_idx) # align |+_alpha> (or |+_-alpha>) with |+> + + signal_process("Z", circ_idx, t_list) + + circ.h(circ_idx) # align |+> with |0> + circ.measure(circ_idx, reg_idx) # measure and store the result + register_dict[cmd.node] = reg_idx + reg_idx += 1 + empty_qubit.append(circ_idx) # liberate the circuit qubit + + else: + raise NotImplementedError("Non-XY plane is not supported.") + + if cmd.kind == CommandKind.X: + circ_idx = qubit_dict[cmd.node] + s_list = cmd.domain + signal_process("X", circ_idx, s_list) + + if cmd.kind == CommandKind.Z: + circ_idx = qubit_dict[cmd.node] + s_list = cmd.domain + signal_process("Z", circ_idx, s_list) + + if cmd.kind == CommandKind.C: + circ_idx = qubit_dict[cmd.node] + cid = cmd.clifford + for op in CLIFFORD_TO_QISKIT[cid]: + exec(f"circ.{op}({circ_idx})") + + if save_statevector: + circ.save_statevector() + output_qubit = [] + for node in self.pattern.output_nodes: + circ_idx = qubit_dict[node] + circ.measure(circ_idx, reg_idx) + register_dict[node] = reg_idx + reg_idx += 1 + output_qubit.append(circ_idx) + + # self.circ_output = output_qubit + + else: + for node in self.pattern.output_nodes: + circ_idx = qubit_dict[node] + circ.measure(circ_idx, reg_idx) + register_dict[node] = reg_idx + reg_idx += 1 + + self.register_dict = register_dict + # self.circ = circ + + return circ \ No newline at end of file diff --git a/graphix_ibmq/executor.py b/graphix_ibmq/executor.py new file mode 100644 index 0000000..72fe3a5 --- /dev/null +++ b/graphix_ibmq/executor.py @@ -0,0 +1,35 @@ +from qiskit import transpile +from qiskit.result import Result +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 +from typing import Optional + +class IBMQJobExecutor: + def __init__(self, circuit): + self.circuit = circuit + self.backend = None + self.service = None + self.job_id = None + + def select_backend(self, instance: str, system: Optional[str] = None): + self.service = QiskitRuntimeService(instance=instance) + if system: + self.backend = self.service.backend(system) + else: + self.backend = self.service.least_busy(min_num_qubits=self.circuit.num_qubits, operational=True) + + def run(self, shots: int, optimization_level: int = 1) -> Result: + transpiled = transpile(self.circuit, backend=self.backend, optimization_level=optimization_level) + sampler = SamplerV2(service=self.service) + job = sampler.run([transpiled], shots=shots, backend=self.backend) + result = job.result() + self.job_id = job.job_id() + return result + + def simulate(self, shots: int = 1024, noise_model: NoiseModel = None) -> Result: + simulator = AerSimulator(noise_model=noise_model) if noise_model else AerSimulator() + transpiled = transpile(self.circuit, simulator) + job = simulator.run(transpiled, shots=shots) + return job.result() + diff --git a/graphix_ibmq/job_handle.py b/graphix_ibmq/job_handle.py new file mode 100644 index 0000000..13e5d9a --- /dev/null +++ b/graphix_ibmq/job_handle.py @@ -0,0 +1,16 @@ +from graphix.device_interface import JobHandle + + +class IBMQJobHandle(JobHandle): + def __init__(self, job): + self.job = job + + def get_id(self) -> str: + return self.job.job_id() + + def is_done(self) -> bool: + return self.job.status().name == "DONE" + + def cancel(self) -> None: + self.job.cancel() + \ No newline at end of file diff --git a/graphix_ibmq/result_utils.py b/graphix_ibmq/result_utils.py new file mode 100644 index 0000000..f01627a --- /dev/null +++ b/graphix_ibmq/result_utils.py @@ -0,0 +1,9 @@ +def format_result(result: dict[str, int], pattern, register_dict: dict[int, int]) -> dict[str, int]: + N_node = pattern.Nnode + output_keys = [register_dict[node] for node in pattern.output_nodes] + + formatted = {} + for bitstring, count in result.items(): + masked = "".join(bitstring[N_node - 1 - idx] for idx in output_keys) + formatted[masked] = formatted.get(masked, 0) + count + return formatted \ No newline at end of file diff --git a/graphix_ibmq/runner.py b/graphix_ibmq/runner.py deleted file mode 100644 index f29ae0d..0000000 --- a/graphix_ibmq/runner.py +++ /dev/null @@ -1,374 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from graphix.pattern import Pattern - -import numpy as np -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile -from qiskit_aer import AerSimulator -from qiskit_aer.noise import NoiseModel -from qiskit_ibm_runtime import QiskitRuntimeService, IBMBackend, SamplerV2 - -from graphix_ibmq.clifford import CLIFFORD_CONJ, CLIFFORD_TO_QISKIT - - -class IBMQBackend: - """Interface for MBQC pattern execution on IBM quantum devices. - - Attributes - ---------- - pattern: graphix.Pattern object - MBQC pattern to be run on the device - circ: qiskit.circuit.quantumcircuit.QuantumCircuit object - qiskit circuit corresponding to the pattern. - service: qiskit_ibm_runtime.qiskit_runtime_service.QiskitRuntimeService object - the runtime service object. - system: qiskit_ibm_runtime.ibm_backend.IBMBackend object - the system to be used for the execution. - """ - - def __init__(self, pattern: Pattern): - """ - - Parameters - ---------- - pattern: graphix.Pattern object - MBQC pattern to be run on the IBMQ device or Aer simulator. - """ - self.pattern = pattern - self.to_qiskit() - - def get_system(self, service: QiskitRuntimeService, system: str = None): - """Get the system to be used for the execution. - - Parameters - ---------- - service: qiskit_ibm_runtime.qiskit_runtime_service.QiskitRuntimeService object - the runtime service object. - system: str, optional - the system name to be used. If None, the least busy system is used. - """ - if not isinstance(service, QiskitRuntimeService): - raise ValueError("Invalid service object.") - self.service = service - if system is not None: - if system not in [system_cand.name for system_cand in self.service.backends()]: - raise ValueError(f"{system} is not available.") - self.system = self.service.backend(system) - else: - self.system = self.service.least_busy(min_num_qubits=self.pattern.max_space(), operational=True) - - print(f"Using system {self.system.name}") - - def to_qiskit(self, save_statevector: bool = False): - """convert the MBQC pattern to the qiskit cuicuit and add to attributes. - - Parameters - ---------- - save_statevector : bool, optional - whether to save the statevector before the measurements of output qubits. - """ - n = self.pattern.max_space() - N_node = self.pattern.Nnode - - qr = QuantumRegister(n) - cr = ClassicalRegister(N_node) - circ = QuantumCircuit(qr, cr) - - empty_qubit = [i for i in range(n)] # list of free qubit indices - qubit_dict = {} # dictionary to record the correspondance of pattern nodes and circuit qubits - register_dict = {} # dictionary to record the correspondance of pattern nodes and classical registers - reg_idx = 0 # index of classical register - - def signal_process(op, signal): - if op == "X": - for s in signal: - if s in register_dict.keys(): - s_idx = register_dict[s] - with circ.if_test((cr[s_idx], 1)): - circ.x(circ_idx) - else: - if self.pattern.results[s] == 1: - circ.x(circ_idx) - if op == "Z": - for s in signal: - if s in register_dict.keys(): - s_idx = register_dict[s] - with circ.if_test((cr[s_idx], 1)): - circ.z(circ_idx) - else: - if self.pattern.results[s] == 1: - circ.z(circ_idx) - - for i in self.pattern.input_nodes: - circ_idx = empty_qubit[0] - empty_qubit.pop(0) - circ.reset(circ_idx) - circ.h(circ_idx) - qubit_dict[i] = circ_idx - - for cmd in self.pattern: - - if cmd[0] == "N": - circ_idx = empty_qubit[0] - empty_qubit.pop(0) - circ.reset(circ_idx) - circ.h(circ_idx) - qubit_dict[cmd[1]] = circ_idx - - if cmd[0] == "E": - circ.cz(qubit_dict[cmd[1][0]], qubit_dict[cmd[1][1]]) - - if cmd[0] == "M": - circ_idx = qubit_dict[cmd[1]] - plane = cmd[2] - alpha = cmd[3] * np.pi - s_list = cmd[4] - t_list = cmd[5] - - if len(cmd) == 6: - if plane == "XY": - # act p and h to implement non-Z-basis measurement - if alpha != 0: - signal_process("X", s_list) - circ.p(-alpha, circ_idx) # align |+_alpha> (or |+_-alpha>) with |+> - - signal_process("Z", t_list) - - circ.h(circ_idx) # align |+> with |0> - circ.measure(circ_idx, reg_idx) # measure and store the result - register_dict[cmd[1]] = reg_idx - reg_idx += 1 - empty_qubit.append(circ_idx) # liberate the circuit qubit - - elif len(cmd) == 7: - cid = cmd[6] - for op in CLIFFORD_TO_QISKIT[CLIFFORD_CONJ[cid]]: - exec(f"circ.{op}({circ_idx})") - - if plane == "XY": - # act p and h to implement non-Z-basis measurement - if alpha != 0: - signal_process("X", s_list) - circ.p(-alpha, circ_idx) # align |+_alpha> (or |+_-alpha>) with |+> - - signal_process("Z", t_list) - - circ.h(circ_idx) # align |+> with |0> - circ.measure(circ_idx, reg_idx) # measure and store the result - register_dict[cmd[1]] = reg_idx - reg_idx += 1 - circ.measure(circ_idx, reg_idx) # measure and store the result - empty_qubit.append(circ_idx) # liberate the circuit qubit - - if cmd[0] == "X": - circ_idx = qubit_dict[cmd[1]] - s_list = cmd[2] - signal_process("X", s_list) - - if cmd[0] == "Z": - circ_idx = qubit_dict[cmd[1]] - s_list = cmd[2] - signal_process("Z", s_list) - - if cmd[0] == "C": - circ_idx = qubit_dict[cmd[1]] - cid = cmd[2] - for op in CLIFFORD_TO_QISKIT[cid]: - exec(f"circ.{op}({circ_idx})") - - if save_statevector: - circ.save_statevector() - output_qubit = [] - for node in self.pattern.output_nodes: - circ_idx = qubit_dict[node] - circ.measure(circ_idx, reg_idx) - register_dict[node] = reg_idx - reg_idx += 1 - output_qubit.append(circ_idx) - - self.circ = circ - self.circ_output = output_qubit - - else: - for node in self.pattern.output_nodes: - circ_idx = qubit_dict[node] - circ.measure(circ_idx, reg_idx) - register_dict[node] = reg_idx - reg_idx += 1 - - self.register_dict = register_dict - self.circ = circ - - def set_input(self, psi: list[list[complex]]): - """set the input state of the circuit. - The input states are set to the circuit qubits corresponding to the first n nodes prepared in the pattern. - - Parameters - ---------- - psi : list[list[complex]] - list of the input states for each input. - Each input state is a list of complex of length 2, representing the coefficient of |0> and |1>. - """ - n = len(self.pattern.input_nodes) - if n != len(psi): - raise ValueError("Invalid input state.") - input_order = {i: self.pattern.input_nodes[i] for i in range(n)} - - idx = 0 - for k, ope in enumerate(self.circ.data): - if ope[0].name == "reset": - if idx in input_order.keys(): - qubit_idx = ope[1][0]._index - i = input_order[idx] - self.circ.initialize(psi[i], qubit_idx) - self.circ.data[k + 1] = self.circ.data.pop(-1) - idx += 1 - if idx >= max(input_order.keys()) + 1: - break - - def transpile(self, system: IBMBackend = None, optimization_level: int = 1): - """transpile the circuit for the designated resource. - - Parameters - ---------- - system: qiskit_ibm_runtime.ibm_backend.IBMBackend object, optional - system to be used for transpilation. - optimization_level : int, optional - the optimization level of the transpilation. - """ - if system is None: - if not hasattr(self, "system"): - raise ValueError("No system is set.") - system = self.system - self.circ = transpile(self.circ, backend=system, optimization_level=optimization_level) - - def simulate(self, shots: int = 1024, noise_model: NoiseModel = None, format_result: bool = True): - """simulate the circuit with Aer. - - Parameters - ---------- - shots : int, optional - the number of shots. - noise_model : :class:`qiskit_aer.backends.aer_simulator.AerSimulator` object, optional - noise model to be used in the simulation. - format_result : bool, optional - whether to format the result so that only the result corresponding to the output qubit is taken out. - - Returns - ---------- - result : dict - the measurement result. - """ - if noise_model is not None: - if type(noise_model) is NoiseModel: - simulator = AerSimulator(noise_model=noise_model) - else: - try: - simulator = AerSimulator.from_backend(noise_model) - except: - raise ValueError("Invalid noise model.") - else: - simulator = AerSimulator() - circ_sim = transpile(self.circ, simulator) - job = simulator.run(circ_sim, shots=shots) - result = job.result() - if format_result: - result = self.format_result(result) - - return result - - def run(self, shots: int = 1024, format_result: bool = True, optimization_level: int = 1): - """Run the MBQC pattern on IBMQ devices - - Parameters - ---------- - shots : int, optional - the number of shots. - format_result : bool, optional - whether to format the result so that only the result corresponding to the output qubit is taken out. - optimization_level : int, optional - the optimization level of the transpilation. - - Returns - ------- - result : dict - the measurement result. - """ - self.transpile(optimization_level=optimization_level) - if not hasattr(self, "system"): - raise ValueError("No system is set.") - sampler = SamplerV2(backend=self.system) - job = sampler.run([self.circ], shots=shots) # Pass the circuit and shot count to the run method - print(f"Your job's id: {job.job_id()}") - result = job.result() - result = next( - ( - getattr(result[0].data, attr_name) - for attr_name in dir(result[0].data) - if attr_name.startswith("c") and attr_name[1:].isdigit() - ), - None, - ) - if format_result: - result = self.format_result(result) - - return result - - def format_result(self, result: dict[str, int]): - """Format the result so that only the result corresponding to the output qubit is taken out. - - Returns - ------- - masked_results : dict - Dictionary of formatted results. - """ - masked_results = {} - N_node = self.pattern.Nnode - - # Iterate over original measurement results - for key, value in result.get_counts().items(): - masked_key = "" - for idx in self.pattern.output_nodes: - reg_idx = self.register_dict[idx] - masked_key += key[N_node - reg_idx - 1] - if masked_key in masked_results: - masked_results[masked_key] += value - else: - masked_results[masked_key] = value - - return masked_results - - def retrieve_result(self, job_id: str, format_result: bool = True): - """Retrieve the measurement results. - - Parameters - ---------- - job_id : str - the id of the job. - format_result : bool, optional - whether to format the result so that only the result corresponding to the output qubit is taken out. - - Returns - ------- - result : dict - the measurement result. - """ - if not hasattr(self, "service"): - raise ValueError("No service is set.") - job = self.service.job(job_id) - result = job.result() - result = next( - ( - getattr(result[0].data, attr_name) - for attr_name in dir(result[0].data) - if attr_name.startswith("c") and attr_name[1:].isdigit() - ), - None, - ) - if format_result: - result = self.format_result(result) - - return result From cfc7d3d6c9c4f316759e7754515db9bed64ef87d Mon Sep 17 00:00:00 2001 From: d1ssk Date: Thu, 3 Apr 2025 17:46:25 +0900 Subject: [PATCH 02/30] add docstring --- graphix_ibmq/backend.py | 192 +++++++++++++++++++++++++++----- graphix_ibmq/compile_options.py | 13 ++- graphix_ibmq/compiler.py | 102 ++++++++++------- graphix_ibmq/executor.py | 35 ------ graphix_ibmq/job_handle.py | 16 --- graphix_ibmq/job_handler.py | 60 ++++++++++ graphix_ibmq/result_utils.py | 32 +++++- requirements.txt | 3 +- 8 files changed, 323 insertions(+), 130 deletions(-) delete mode 100644 graphix_ibmq/executor.py delete mode 100644 graphix_ibmq/job_handle.py create mode 100644 graphix_ibmq/job_handler.py diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index ebc1bfb..e5c0f58 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -1,52 +1,184 @@ -from graphix.device_interface import DeviceBackend, CompileOptions, JobHandle +from typing import Optional + +from graphix.device_interface import DeviceBackend, CompileOptions, JobHandler from graphix_ibmq.compiler import IBMQPatternCompiler -from graphix_ibmq.executor import IBMQJobExecutor -from graphix_ibmq.job_handle import IBMQJobHandle +from graphix_ibmq.job_handler import IBMQJobHandler from graphix_ibmq.compile_options import IBMQCompileOptions from graphix_ibmq.result_utils import format_result +from qiskit import QuantumCircuit +from qiskit_aer.noise import NoiseModel +from qiskit.providers.backend import BackendV2 +from qiskit_aer import AerSimulator +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import SamplerV2 as Sampler + class IBMQBackend(DeviceBackend): - def __init__(self, instance: str = "ibm-q/open/main", resource: str = None): + """IBMQ backend implementation for compiling and executing quantum patterns.""" + + def __init__(self) -> None: + """Initialize the IBMQ backend.""" super().__init__() - self.instance = instance - self.resource = resource - self._compiled_circuit = None - self._executor = None - self._register_dict = None - self._options = None - - def compile(self, options: CompileOptions = None) -> None: + self._compiler: Optional[IBMQPatternCompiler] = None + self._options: Optional[IBMQCompileOptions] = None + self._compiled_circuit: Optional[QuantumCircuit] = None + self._execution_mode: Optional[str] = None + + def compile(self, options: Optional[CompileOptions] = None) -> None: + """Compile the assigned pattern using IBMQ options. + + Parameters + ---------- + options : CompileOptions, optional + Compilation options. Must be of type IBMQCompileOptions. + """ if self.pattern is None: raise ValueError("Pattern not set.") - if not isinstance(options, IBMQCompileOptions): + if options is None: + self._options = IBMQCompileOptions() + elif not isinstance(options, IBMQCompileOptions): raise TypeError("Expected IBMQCompileOptions") + else: + self._options = options - self._options = options - compiler = IBMQPatternCompiler(self.pattern) - self._compiled_circuit = compiler.to_qiskit_circuit( - save_statevector=options.save_statevector, - layout_method=options.layout_method + self._compiler = IBMQPatternCompiler(self.pattern) + self._compiled_circuit = self._compiler.to_qiskit_circuit( + save_statevector=self._options.save_statevector, + layout_method=self._options.layout_method, ) - self._register_dict = compiler.register_dict - def submit_job(self, shots: int = 1024) -> JobHandle: + def set_simulator( + self, + noise_model: Optional[NoiseModel] = None, + based_on: Optional[BackendV2] = None, + ) -> None: + """Configure the backend to use a simulator. + + Parameters + ---------- + noise_model : NoiseModel, optional + Noise model to apply to the simulator. + based_on : BackendV2, optional + Backend to base the noise model on. + """ + if noise_model is None and based_on is not None: + noise_model = NoiseModel.from_backend(based_on) + + self._execution_mode = "simulation" + self._noise_model = noise_model + self._simulator = AerSimulator(noise_model=noise_model) + + def select_backend( + self, + name: Optional[str] = None, + least_busy: bool = False, + min_qubits: int = 1, + ) -> None: + """Select a hardware backend from IBMQ. + + Parameters + ---------- + name : str, optional + Specific backend name to use. + least_busy : bool, optional + If True, select the least busy backend that meets requirements. + min_qubits : int, optional + Minimum number of qubits required. + """ + from qiskit_ibm_runtime import QiskitRuntimeService + + self._execution_mode = "hardware" + service = QiskitRuntimeService() + + if least_busy or name is None: + self._resource = service.least_busy( + min_num_qubits=min_qubits, operational=True + ) + else: + self._resource = service.backend(name) + + def submit_job(self, shots: int = 1024) -> JobHandler: + """Submit the compiled circuit to either simulator or hardware backend. + + Parameters + ---------- + shots : int, optional + Number of execution shots. Defaults to 1024. + + Returns + ------- + JobHandler + A handle to monitor the job status and retrieve results. + + Raises + ------ + RuntimeError + If the pattern has not been compiled or execution mode is not set. + """ if self._compiled_circuit is None: raise RuntimeError("Pattern must be compiled before submission.") - self._executor = IBMQJobExecutor(self._compiled_circuit) - self._executor.select_backend(self.instance, self.resource) + if self._execution_mode is None: + raise RuntimeError( + "Execution mode is not configured. Use select_backend() or set_simulator()." + ) + + if self._execution_mode == "simulation": + pm = generate_preset_pass_manager( + backend=self._simulator, + optimization_level=self._options.optimization_level, + ) + transpiled = pm.run(self._compiled_circuit) + sampler = Sampler(mode=self._simulator) + job = sampler.run([transpiled], shots=shots) + return IBMQJobHandler(job) + + elif self._execution_mode == "hardware": + pm = generate_preset_pass_manager( + backend=self._resource, + optimization_level=self._options.optimization_level, + ) + transpiled = pm.run(self._compiled_circuit) + sampler = Sampler(mode=self._resource) + job = sampler.run([transpiled], shots=shots) + return IBMQJobHandler(job) + + else: + raise RuntimeError( + "Execution mode is not configured. Use select_backend() or set_simulator()." + ) + + def retrieve_result(self, job_handle: JobHandler, raw_result: bool = False): + """Retrieve the result from a completed job. + + Parameters + ---------- + job_handle : JobHandler + Handle to the executed job. + raw_result : bool, optional + If True, return raw counts; otherwise, return formatted results. + + Returns + ------- + dict or Any + Formatted result or raw counts from the job. - job_result = self._executor.run(shots=shots, optimization_level=self._options.optimization_level) - return IBMQJobHandle(job_result) + Raises + ------ + TypeError + If the job handle is not an instance of IBMQJobHandler. + """ + if not isinstance(job_handle, IBMQJobHandler): + raise TypeError("Expected IBMQJobHandler") - def retrieve_result(self, job_handle: JobHandle, raw_result: bool = False): - if not isinstance(job_handle, IBMQJobHandle): - raise TypeError("Expected IBMQJobHandle") + if not job_handle.is_done(): + print("Job not done.") + return None result = job_handle.job.result() - counts = result[0].data.get_counts() + counts = result[0].data.meas.get_counts() if not raw_result: - return format_result(counts, self.pattern, self._register_dict) - return counts \ No newline at end of file + return format_result(counts, self.pattern, self._compiler.register_dict) + return counts diff --git a/graphix_ibmq/compile_options.py b/graphix_ibmq/compile_options.py index 0a1fa71..a272f6d 100644 --- a/graphix_ibmq/compile_options.py +++ b/graphix_ibmq/compile_options.py @@ -4,7 +4,18 @@ @dataclass class IBMQCompileOptions(CompileOptions): + """Compilation options specific to IBMQ backends. + + Attributes + ---------- + optimization_level : int + Optimization level for Qiskit transpiler (0 to 3). + save_statevector : bool + Whether to save the statevector before measurement (for debugging/testing). + layout_method : str + Qubit layout method used by the transpiler (for future use). + """ + optimization_level: int = 1 save_statevector: bool = False layout_method: str = "dense" - \ No newline at end of file diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index c06d1a6..89f87d9 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -1,40 +1,64 @@ import numpy as np -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile -from graphix_ibmq.clifford import CLIFFORD_CONJ, CLIFFORD_TO_QISKIT +from typing import Optional +from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit + +from graphix_ibmq.clifford import CLIFFORD_TO_QISKIT from graphix.pattern import Pattern from graphix.command import CommandKind from graphix.fundamentals import Plane + class IBMQPatternCompiler: - def __init__(self, pattern: Pattern): + """Compiler that translates a Graphix Pattern into a Qiskit QuantumCircuit.""" + + def __init__(self, pattern: Pattern) -> None: + """ + Initialize the compiler with a given pattern. + + Parameters + ---------- + pattern : Pattern + The measurement-based quantum computation pattern. + """ self.pattern = pattern - self.register_dict = {} + self.register_dict: dict[int, int] = {} + self.circ_output: list[int] = [] - def to_qiskit_circuit(self, save_statevector: bool, layout_method: str): - from qiskit import QuantumCircuit - """convert the MBQC pattern to the qiskit cuicuit and add to attributes. + def to_qiskit_circuit( + self, save_statevector: bool, layout_method: str + ) -> QuantumCircuit: + """ + Convert the MBQC pattern into a Qiskit QuantumCircuit. Parameters ---------- - save_statevector : bool, optional - whether to save the statevector before the measurements of output qubits. + save_statevector : bool + Whether to save the statevector before output measurement (for testing). + layout_method : str + (Currently unused) Layout method for mapping. + + Returns + ------- + QuantumCircuit + The compiled Qiskit circuit. """ n = self.pattern.max_space() N_node = self.pattern.n_node qr = QuantumRegister(n) - cr = ClassicalRegister(N_node) + cr = ClassicalRegister(N_node, name="meas") circ = QuantumCircuit(qr, cr) - empty_qubit = [i for i in range(n)] # list of free qubit indices - qubit_dict = {} # dictionary to record the correspondance of pattern nodes and circuit qubits - register_dict = {} # dictionary to record the correspondance of pattern nodes and classical registers - reg_idx = 0 # index of classical register + empty_qubit = list(range(n)) # available qubit indices + qubit_dict: dict[int, int] = {} # pattern node -> circuit qubit + register_dict: dict[int, int] = {} # pattern node -> classical register + reg_idx = 0 - def signal_process(op, circ_idx, signal): + def signal_process(op: str, circ_idx: int, signal: list[int]) -> None: + """Apply classically-controlled X or Z gates based on measurement outcomes.""" if op == "X": for s in signal: - if s in register_dict.keys(): + if s in register_dict: s_idx = register_dict[s] with circ.if_test((cr[s_idx], 1)): circ.x(circ_idx) @@ -43,7 +67,7 @@ def signal_process(op, circ_idx, signal): circ.x(circ_idx) if op == "Z": for s in signal: - if s in register_dict.keys(): + if s in register_dict: s_idx = register_dict[s] with circ.if_test((cr[s_idx], 1)): circ.z(circ_idx) @@ -51,26 +75,25 @@ def signal_process(op, circ_idx, signal): if self.pattern.results[s] == 1: circ.z(circ_idx) + # Prepare input qubits for i in self.pattern.input_nodes: - circ_idx = empty_qubit[0] - empty_qubit.pop(0) + circ_idx = empty_qubit.pop(0) circ.reset(circ_idx) circ.h(circ_idx) qubit_dict[i] = circ_idx + # Compile pattern commands for cmd in self.pattern: - if cmd.kind == CommandKind.N: - circ_idx = empty_qubit[0] - empty_qubit.pop(0) + circ_idx = empty_qubit.pop(0) circ.reset(circ_idx) circ.h(circ_idx) qubit_dict[cmd.node] = circ_idx - if cmd.kind == CommandKind.E: + elif cmd.kind == CommandKind.E: circ.cz(qubit_dict[cmd.nodes[0]], qubit_dict[cmd.nodes[1]]) - if cmd.kind == CommandKind.M: + elif cmd.kind == CommandKind.M: circ_idx = qubit_dict[cmd.node] plane = cmd.plane alpha = cmd.angle * np.pi @@ -78,50 +101,45 @@ def signal_process(op, circ_idx, signal): t_list = cmd.t_domain if plane == Plane.XY: - # act p and h to implement non-Z-basis measurement if alpha != 0: signal_process("X", circ_idx, s_list) - circ.p(-alpha, circ_idx) # align |+_alpha> (or |+_-alpha>) with |+> - + circ.p(-alpha, circ_idx) signal_process("Z", circ_idx, t_list) - - circ.h(circ_idx) # align |+> with |0> - circ.measure(circ_idx, reg_idx) # measure and store the result + circ.h(circ_idx) + circ.measure(circ_idx, reg_idx) register_dict[cmd.node] = reg_idx reg_idx += 1 - empty_qubit.append(circ_idx) # liberate the circuit qubit - + empty_qubit.append(circ_idx) else: raise NotImplementedError("Non-XY plane is not supported.") - if cmd.kind == CommandKind.X: + elif cmd.kind == CommandKind.X: circ_idx = qubit_dict[cmd.node] s_list = cmd.domain signal_process("X", circ_idx, s_list) - if cmd.kind == CommandKind.Z: + elif cmd.kind == CommandKind.Z: circ_idx = qubit_dict[cmd.node] s_list = cmd.domain signal_process("Z", circ_idx, s_list) - if cmd.kind == CommandKind.C: + elif cmd.kind == CommandKind.C: circ_idx = qubit_dict[cmd.node] cid = cmd.clifford for op in CLIFFORD_TO_QISKIT[cid]: exec(f"circ.{op}({circ_idx})") + # Handle output measurements if save_statevector: circ.save_statevector() - output_qubit = [] + output_qubit: list[int] = [] for node in self.pattern.output_nodes: circ_idx = qubit_dict[node] circ.measure(circ_idx, reg_idx) register_dict[node] = reg_idx reg_idx += 1 output_qubit.append(circ_idx) - - # self.circ_output = output_qubit - + self.circ_output = output_qubit else: for node in self.pattern.output_nodes: circ_idx = qubit_dict[node] @@ -129,7 +147,5 @@ def signal_process(op, circ_idx, signal): register_dict[node] = reg_idx reg_idx += 1 - self.register_dict = register_dict - # self.circ = circ - - return circ \ No newline at end of file + self.register_dict = register_dict + return circ diff --git a/graphix_ibmq/executor.py b/graphix_ibmq/executor.py deleted file mode 100644 index 72fe3a5..0000000 --- a/graphix_ibmq/executor.py +++ /dev/null @@ -1,35 +0,0 @@ -from qiskit import transpile -from qiskit.result import Result -from qiskit_aer import AerSimulator -from qiskit_aer.noise import NoiseModel -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 -from typing import Optional - -class IBMQJobExecutor: - def __init__(self, circuit): - self.circuit = circuit - self.backend = None - self.service = None - self.job_id = None - - def select_backend(self, instance: str, system: Optional[str] = None): - self.service = QiskitRuntimeService(instance=instance) - if system: - self.backend = self.service.backend(system) - else: - self.backend = self.service.least_busy(min_num_qubits=self.circuit.num_qubits, operational=True) - - def run(self, shots: int, optimization_level: int = 1) -> Result: - transpiled = transpile(self.circuit, backend=self.backend, optimization_level=optimization_level) - sampler = SamplerV2(service=self.service) - job = sampler.run([transpiled], shots=shots, backend=self.backend) - result = job.result() - self.job_id = job.job_id() - return result - - def simulate(self, shots: int = 1024, noise_model: NoiseModel = None) -> Result: - simulator = AerSimulator(noise_model=noise_model) if noise_model else AerSimulator() - transpiled = transpile(self.circuit, simulator) - job = simulator.run(transpiled, shots=shots) - return job.result() - diff --git a/graphix_ibmq/job_handle.py b/graphix_ibmq/job_handle.py deleted file mode 100644 index 13e5d9a..0000000 --- a/graphix_ibmq/job_handle.py +++ /dev/null @@ -1,16 +0,0 @@ -from graphix.device_interface import JobHandle - - -class IBMQJobHandle(JobHandle): - def __init__(self, job): - self.job = job - - def get_id(self) -> str: - return self.job.job_id() - - def is_done(self) -> bool: - return self.job.status().name == "DONE" - - def cancel(self) -> None: - self.job.cancel() - \ No newline at end of file diff --git a/graphix_ibmq/job_handler.py b/graphix_ibmq/job_handler.py new file mode 100644 index 0000000..54e12fa --- /dev/null +++ b/graphix_ibmq/job_handler.py @@ -0,0 +1,60 @@ +from graphix.device_interface import JobHandler + + +class IBMQJobHandler(JobHandler): + """Job handler class for IBMQ devices and simulators.""" + + def __init__(self, job) -> None: + """ + Initialize with a Qiskit Runtime job object. + + Parameters + ---------- + job : Any + The job object returned from Qiskit Runtime or Aer Sampler. + """ + self.job = job + + def get_id(self) -> str: + """ + Get the unique identifier of the job. + + Returns + ------- + str + The job ID. + """ + return self.job.job_id() + + def is_done(self) -> bool: + """ + Check whether the job is completed. + + Returns + ------- + bool + True if the job is done, False otherwise. + """ + try: + # Simulator jobs typically use .status().name + return self.job.status().name == "DONE" + except AttributeError: + # Hardware jobs may return status as a string + return str(self.job.status()).upper() == "DONE" + + def cancel(self) -> None: + """ + Cancel the job if it's still running. + """ + self.job.cancel() + + def get_status(self) -> str: + """ + Get the current status of the job. + + Returns + ------- + str + Status string representing the job state. + """ + return self.job.status() diff --git a/graphix_ibmq/result_utils.py b/graphix_ibmq/result_utils.py index f01627a..bf7fe27 100644 --- a/graphix_ibmq/result_utils.py +++ b/graphix_ibmq/result_utils.py @@ -1,9 +1,33 @@ -def format_result(result: dict[str, int], pattern, register_dict: dict[int, int]) -> dict[str, int]: - N_node = pattern.Nnode +from typing import Dict + +from graphix.pattern import Pattern + + +def format_result( + result: Dict[str, int], pattern: Pattern, register_dict: Dict[int, int] +) -> Dict[str, int]: + """Format raw measurement results into output-only bitstrings. + + Parameters + ---------- + result : dict of str to int + Raw result counts as returned by Qiskit (full bitstrings). + pattern : Pattern + Graphix pattern object containing output node information. + register_dict : dict of int to int + Mapping from pattern node index to classical register index. + + Returns + ------- + formatted : dict of str to int + Dictionary of bitstrings only for output nodes and their counts. + """ + N_node = pattern.n_node output_keys = [register_dict[node] for node in pattern.output_nodes] - formatted = {} + formatted: Dict[str, int] = {} for bitstring, count in result.items(): masked = "".join(bitstring[N_node - 1 - idx] for idx in output_keys) formatted[masked] = formatted.get(masked, 0) + count - return formatted \ No newline at end of file + + return formatted diff --git a/requirements.txt b/requirements.txt index 485ac3f..ae9f036 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ numpy>=1.22,<1.26 qiskit>=1.0 -qiskit_ibm_runtime +qiskit_ibm_runtime==0.37.0 qiskit-aer +graphix From cc960476f76014f64c41187d4eb443546d1905e8 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 28 Apr 2025 18:37:24 +0900 Subject: [PATCH 03/30] add tests --- examples/gallery/qiskit_to_graphix.py | 2 +- graphix_ibmq/backend.py | 63 +++++----------- graphix_ibmq/compile_options.py | 31 +++++++- graphix_ibmq/compiler.py | 27 ++++--- graphix_ibmq/{job_handler.py => job.py} | 41 ++++++++++- requirements.txt | 2 +- tests/test_backend.py | 52 +++++++++++++ tests/test_compile_options.py | 13 ++++ tests/test_converter.py | 3 +- tests/test_ibmq_backend_temp.py | 98 +++++++++++++++++++++++++ tests/test_ibmq_interface.py | 58 --------------- tests/test_result_utils.py | 17 +++++ tests/test_simulation.py | 4 + 13 files changed, 283 insertions(+), 128 deletions(-) rename graphix_ibmq/{job_handler.py => job.py} (54%) create mode 100644 tests/test_backend.py create mode 100644 tests/test_compile_options.py create mode 100644 tests/test_ibmq_backend_temp.py delete mode 100644 tests/test_ibmq_interface.py create mode 100644 tests/test_result_utils.py create mode 100644 tests/test_simulation.py diff --git a/examples/gallery/qiskit_to_graphix.py b/examples/gallery/qiskit_to_graphix.py index d79ac7d..69d643f 100644 --- a/examples/gallery/qiskit_to_graphix.py +++ b/examples/gallery/qiskit_to_graphix.py @@ -9,7 +9,7 @@ # %% -from qiskit import QuantumCircuit, transpile +from qiskit import transpile from qiskit.circuit.random.utils import random_circuit qc = random_circuit(5, 2, seed=42) diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index e5c0f58..a690f14 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -1,10 +1,11 @@ -from typing import Optional +from __future__ import annotations -from graphix.device_interface import DeviceBackend, CompileOptions, JobHandler +from typing import Optional, TYPE_CHECKING + +from graphix.device_interface import DeviceBackend, CompileOptions, Job from graphix_ibmq.compiler import IBMQPatternCompiler -from graphix_ibmq.job_handler import IBMQJobHandler +from graphix_ibmq.job import IBMQJob from graphix_ibmq.compile_options import IBMQCompileOptions -from graphix_ibmq.result_utils import format_result from qiskit import QuantumCircuit from qiskit_aer.noise import NoiseModel @@ -13,6 +14,9 @@ from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import SamplerV2 as Sampler +if TYPE_CHECKING: + from graphix.pattern import Pattern + class IBMQBackend(DeviceBackend): """IBMQ backend implementation for compiling and executing quantum patterns.""" @@ -25,7 +29,7 @@ def __init__(self) -> None: self._compiled_circuit: Optional[QuantumCircuit] = None self._execution_mode: Optional[str] = None - def compile(self, options: Optional[CompileOptions] = None) -> None: + def compile(self, pattern : Pattern, options: Optional[CompileOptions] = None) -> None: """Compile the assigned pattern using IBMQ options. Parameters @@ -33,16 +37,16 @@ def compile(self, options: Optional[CompileOptions] = None) -> None: options : CompileOptions, optional Compilation options. Must be of type IBMQCompileOptions. """ - if self.pattern is None: - raise ValueError("Pattern not set.") if options is None: self._options = IBMQCompileOptions() elif not isinstance(options, IBMQCompileOptions): raise TypeError("Expected IBMQCompileOptions") else: self._options = options + + self._pattern = pattern - self._compiler = IBMQPatternCompiler(self.pattern) + self._compiler = IBMQPatternCompiler(pattern) self._compiled_circuit = self._compiler.to_qiskit_circuit( save_statevector=self._options.save_statevector, layout_method=self._options.layout_method, @@ -98,7 +102,7 @@ def select_backend( else: self._resource = service.backend(name) - def submit_job(self, shots: int = 1024) -> JobHandler: + def submit_job(self, shots: int = 1024) -> Job: """Submit the compiled circuit to either simulator or hardware backend. Parameters @@ -108,7 +112,7 @@ def submit_job(self, shots: int = 1024) -> JobHandler: Returns ------- - JobHandler + Job A handle to monitor the job status and retrieve results. Raises @@ -132,7 +136,7 @@ def submit_job(self, shots: int = 1024) -> JobHandler: transpiled = pm.run(self._compiled_circuit) sampler = Sampler(mode=self._simulator) job = sampler.run([transpiled], shots=shots) - return IBMQJobHandler(job) + return IBMQJob(job, self._compiler) elif self._execution_mode == "hardware": pm = generate_preset_pass_manager( @@ -142,43 +146,10 @@ def submit_job(self, shots: int = 1024) -> JobHandler: transpiled = pm.run(self._compiled_circuit) sampler = Sampler(mode=self._resource) job = sampler.run([transpiled], shots=shots) - return IBMQJobHandler(job) + return IBMQJob(job, self._compiler) else: raise RuntimeError( "Execution mode is not configured. Use select_backend() or set_simulator()." ) - - def retrieve_result(self, job_handle: JobHandler, raw_result: bool = False): - """Retrieve the result from a completed job. - - Parameters - ---------- - job_handle : JobHandler - Handle to the executed job. - raw_result : bool, optional - If True, return raw counts; otherwise, return formatted results. - - Returns - ------- - dict or Any - Formatted result or raw counts from the job. - - Raises - ------ - TypeError - If the job handle is not an instance of IBMQJobHandler. - """ - if not isinstance(job_handle, IBMQJobHandler): - raise TypeError("Expected IBMQJobHandler") - - if not job_handle.is_done(): - print("Job not done.") - return None - - result = job_handle.job.result() - counts = result[0].data.meas.get_counts() - - if not raw_result: - return format_result(counts, self.pattern, self._compiler.register_dict) - return counts + \ No newline at end of file diff --git a/graphix_ibmq/compile_options.py b/graphix_ibmq/compile_options.py index a272f6d..649a77c 100644 --- a/graphix_ibmq/compile_options.py +++ b/graphix_ibmq/compile_options.py @@ -15,7 +15,32 @@ class IBMQCompileOptions(CompileOptions): layout_method : str Qubit layout method used by the transpiler (for future use). """ + + def __init__( + self, + optimization_level: int = 3, + save_statevector: bool = False, + layout_method: str = "trivial", + ) -> None: + """Initialize the compilation options. - optimization_level: int = 1 - save_statevector: bool = False - layout_method: str = "dense" + Parameters + ---------- + optimization_level : int + Optimization level for Qiskit transpiler (0 to 3). + save_statevector : bool + Whether to save the statevector before measurement (for debugging/testing). + layout_method : str + Qubit layout method used by the transpiler (for future use). + """ + self.optimization_level = optimization_level + self.save_statevector = save_statevector + self.layout_method = layout_method + + def __repr__(self) -> str: + """Return a string representation of the compilation options.""" + return ( + f"IBMQCompileOptions(optimization_level={self.optimization_level}, " + f"save_statevector={self.save_statevector}, " + f"layout_method='{self.layout_method}')" + ) diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index 89f87d9..7acb88b 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -1,5 +1,4 @@ import numpy as np -from typing import Optional from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit from graphix_ibmq.clifford import CLIFFORD_TO_QISKIT @@ -20,9 +19,9 @@ def __init__(self, pattern: Pattern) -> None: pattern : Pattern The measurement-based quantum computation pattern. """ - self.pattern = pattern - self.register_dict: dict[int, int] = {} - self.circ_output: list[int] = [] + self._pattern = pattern + self._register_dict: dict[int, int] = {} + self._circ_output: list[int] = [] def to_qiskit_circuit( self, save_statevector: bool, layout_method: str @@ -42,8 +41,8 @@ def to_qiskit_circuit( QuantumCircuit The compiled Qiskit circuit. """ - n = self.pattern.max_space() - N_node = self.pattern.n_node + n = self._pattern.max_space() + N_node = self._pattern.n_node qr = QuantumRegister(n) cr = ClassicalRegister(N_node, name="meas") @@ -63,7 +62,7 @@ def signal_process(op: str, circ_idx: int, signal: list[int]) -> None: with circ.if_test((cr[s_idx], 1)): circ.x(circ_idx) else: - if self.pattern.results[s] == 1: + if self._pattern.results[s] == 1: circ.x(circ_idx) if op == "Z": for s in signal: @@ -72,18 +71,18 @@ def signal_process(op: str, circ_idx: int, signal: list[int]) -> None: with circ.if_test((cr[s_idx], 1)): circ.z(circ_idx) else: - if self.pattern.results[s] == 1: + if self._pattern.results[s] == 1: circ.z(circ_idx) # Prepare input qubits - for i in self.pattern.input_nodes: + for i in self._pattern.input_nodes: circ_idx = empty_qubit.pop(0) circ.reset(circ_idx) circ.h(circ_idx) qubit_dict[i] = circ_idx # Compile pattern commands - for cmd in self.pattern: + for cmd in self._pattern: if cmd.kind == CommandKind.N: circ_idx = empty_qubit.pop(0) circ.reset(circ_idx) @@ -133,19 +132,19 @@ def signal_process(op: str, circ_idx: int, signal: list[int]) -> None: if save_statevector: circ.save_statevector() output_qubit: list[int] = [] - for node in self.pattern.output_nodes: + for node in self._pattern.output_nodes: circ_idx = qubit_dict[node] circ.measure(circ_idx, reg_idx) register_dict[node] = reg_idx reg_idx += 1 output_qubit.append(circ_idx) - self.circ_output = output_qubit + self._circ_output = output_qubit else: - for node in self.pattern.output_nodes: + for node in self._pattern.output_nodes: circ_idx = qubit_dict[node] circ.measure(circ_idx, reg_idx) register_dict[node] = reg_idx reg_idx += 1 - self.register_dict = register_dict + self._register_dict = register_dict return circ diff --git a/graphix_ibmq/job_handler.py b/graphix_ibmq/job.py similarity index 54% rename from graphix_ibmq/job_handler.py rename to graphix_ibmq/job.py index 54e12fa..afdffaa 100644 --- a/graphix_ibmq/job_handler.py +++ b/graphix_ibmq/job.py @@ -1,10 +1,11 @@ -from graphix.device_interface import JobHandler +from graphix.device_interface import Job +from graphix_ibmq.result_utils import format_result -class IBMQJobHandler(JobHandler): +class IBMQJob(Job): """Job handler class for IBMQ devices and simulators.""" - def __init__(self, job) -> None: + def __init__(self, job, compiler) -> None: """ Initialize with a Qiskit Runtime job object. @@ -14,6 +15,7 @@ def __init__(self, job) -> None: The job object returned from Qiskit Runtime or Aer Sampler. """ self.job = job + self._compiler = compiler def get_id(self) -> str: """ @@ -26,6 +28,7 @@ def get_id(self) -> str: """ return self.job.job_id() + @property def is_done(self) -> bool: """ Check whether the job is completed. @@ -58,3 +61,35 @@ def get_status(self) -> str: Status string representing the job state. """ return self.job.status() + + def retrieve_result(self, raw_result: bool = False): + """Retrieve the result from a completed job. + + Parameters + ---------- + job : Job + Handle to the executed job. + raw_result : bool, optional + If True, return raw counts; otherwise, return formatted results. + + Returns + ------- + dict or Any + Formatted result or raw counts from the job. + + Raises + ------ + TypeError + If the job handle is not an instance of IBMQJob. + """ + + if not self.is_done: + print("Job not done.") + return None + + result = self.job.result() + counts = result[0].data.meas.get_counts() + + if not raw_result: + return format_result(counts, self._compiler._pattern, self._compiler._register_dict) + return counts diff --git a/requirements.txt b/requirements.txt index ae9f036..d83d2cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ numpy>=1.22,<1.26 qiskit>=1.0 qiskit_ibm_runtime==0.37.0 qiskit-aer -graphix +git+https://github.com/TeamGraphix/graphix.git@device_interface_abc#egg=graphix diff --git a/tests/test_backend.py b/tests/test_backend.py new file mode 100644 index 0000000..ed6e8d9 --- /dev/null +++ b/tests/test_backend.py @@ -0,0 +1,52 @@ +import pytest +from graphix_ibmq.backend import IBMQBackend +from graphix_ibmq.compile_options import IBMQCompileOptions +from qiskit import QuantumCircuit +from qiskit_aer import AerSimulator + +# モンキーパッチ用ダミーコンパイラ +class DummyCompiler: + def __init__(self, pattern): + self.pattern = pattern + def to_qiskit_circuit(self, save_statevector, layout_method): + # シンプルな 1-qubit 回路を返す + return QuantumCircuit(1) + +def test_compile_default(monkeypatch): + # IBMQPatternCompiler をダミーに置き換え + monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) + backend = IBMQBackend() + dummy_pattern = object() + + backend.compile(dummy_pattern) + # 内部状態の確認 + assert backend._pattern is dummy_pattern + assert isinstance(backend._compiled_circuit, QuantumCircuit) + assert isinstance(backend._options, IBMQCompileOptions) + +def test_compile_invalid_options(monkeypatch): + monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) + backend = IBMQBackend() + with pytest.raises(TypeError): + backend.compile(object(), options="not-an-options") + +def test_set_simulator(): + backend = IBMQBackend() + backend.set_simulator() + assert backend._execution_mode == "simulation" + assert isinstance(backend._simulator, AerSimulator) + assert backend._noise_model is None + +def test_submit_job_not_compiled(): + backend = IBMQBackend() + with pytest.raises(RuntimeError) as e: + backend.submit_job() + assert "Pattern must be compiled" in str(e.value) + +def test_submit_job_without_execution_mode(monkeypatch): + monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) + backend = IBMQBackend() + backend.compile(object()) + with pytest.raises(RuntimeError) as e: + backend.submit_job() + assert "Execution mode is not configured" in str(e.value) diff --git a/tests/test_compile_options.py b/tests/test_compile_options.py new file mode 100644 index 0000000..ad7ea47 --- /dev/null +++ b/tests/test_compile_options.py @@ -0,0 +1,13 @@ +import pytest +from graphix_ibmq.compile_options import IBMQCompileOptions + +def test_default_options(): + opts = IBMQCompileOptions() + assert opts.optimization_level == 3 + assert opts.save_statevector is False + assert opts.layout_method == "trivial" + +def test_repr(): + opts = IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method="dense") + expected = "IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method='dense')" + assert repr(opts) == expected diff --git a/tests/test_converter.py b/tests/test_converter.py index 7077cf2..b8c1ae7 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1,7 +1,6 @@ import unittest -import numpy as np -from qiskit import QuantumCircuit, transpile +from qiskit import transpile from qiskit.circuit.random.utils import random_circuit from qiskit.quantum_info import Statevector diff --git a/tests/test_ibmq_backend_temp.py b/tests/test_ibmq_backend_temp.py new file mode 100644 index 0000000..17d3c78 --- /dev/null +++ b/tests/test_ibmq_backend_temp.py @@ -0,0 +1,98 @@ +import unittest +import numpy as np +import networkx as nx +from graphix.transpiler import Circuit +from graphix_ibmq.backend import IBMQBackend +import qiskit.quantum_info as qi + +class TestIBMQBackend(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # IBMQBackendを一度初期化 + cls.backend = IBMQBackend() + + def test_backend_initialization(self): + """Backendが正しく初期化されるか""" + self.assertIsInstance(self.backend, IBMQBackend) + + def test_run_simple_pattern(self): + """簡単な2qubit QFTパターンでrunできるか""" + n = 2 + circuit = Circuit(n) + self._qft(circuit, n) + pattern = circuit.transpile().pattern + pattern.minimize_space() + + result = self.backend.run(pattern, shots=1024) + self.assertIsInstance(result, dict) + self.assertTrue(len(result) > 0) + + def test_result_matches_theory(self): + """実行結果が理論分布と大きくずれていないか""" + n = 2 + circuit = Circuit(n) + self._qft(circuit, n) + pattern = circuit.transpile().pattern + pattern.minimize_space() + + result = self.backend.run(pattern, shots=1024) + + # 理想結果を計算 + qc = circuit.to_qiskit() + ideal = qi.Statevector.from_instruction(qc) + probs = ideal.probabilities_dict() + + # 実機結果を正規化 + total_shots = sum(result.values()) + exp_probs = {k: v / total_shots for k, v in result.items()} + + # L1ノルム (差の総和) が小さいかチェック + l1_distance = sum(abs(exp_probs.get(k, 0) - probs.get(k, 0)) for k in set(probs) | set(exp_probs)) + self.assertLess(l1_distance, 0.3) # 許容誤差: 30% + + def test_invalid_option(self): + """不正な引数でエラーになるか""" + n = 2 + circuit = Circuit(n) + self._qft(circuit, n) + pattern = circuit.transpile().pattern + + with self.assertRaises(TypeError): + # わざと不正なオプションを渡す + self.backend.run(pattern, shots="invalid") + + @staticmethod + def _qft(circuit, n): + """QFT生成(テスト内関数)""" + for i in range(n): + TestIBMQBackend._qft_rotations(circuit, i) + TestIBMQBackend._swap_registers(circuit, n) + + @staticmethod + def _qft_rotations(circuit, n): + if n == circuit.width: + return + circuit.h(n) + for qubit in range(n+1, circuit.width): + theta = np.pi / 2 ** (qubit - n) + TestIBMQBackend._cp(circuit, theta, qubit, n) + + @staticmethod + def _cp(circuit, theta, control, target): + circuit.rz(control, theta / 2) + circuit.rz(target, theta / 2) + circuit.cnot(control, target) + circuit.rz(target, -theta / 2) + circuit.cnot(control, target) + + @staticmethod + def _swap_registers(circuit, n): + for qubit in range(n // 2): + TestIBMQBackend._swap(circuit, qubit, n - qubit - 1) + + @staticmethod + def _swap(circuit, a, b): + circuit.cnot(a, b) + circuit.cnot(b, a) + circuit.cnot(a, b) diff --git a/tests/test_ibmq_interface.py b/tests/test_ibmq_interface.py deleted file mode 100644 index 7a2f26b..0000000 --- a/tests/test_ibmq_interface.py +++ /dev/null @@ -1,58 +0,0 @@ -import unittest - -import numpy as np - -import random_circuit as rc -from graphix_ibmq.runner import IBMQBackend - - -def modify_statevector(statevector, output_qubit): - N = round(np.log2(len(statevector))) - new_statevector = np.zeros(2 ** len(output_qubit), dtype=complex) - for i in range(len(statevector)): - i_str = format(i, f"0{N}b") - new_idx = "" - for idx in output_qubit: - new_idx += i_str[N - idx - 1] - new_statevector[int(new_idx, 2)] += statevector[i] - return new_statevector - - -class TestIBMQInterface(unittest.TestCase): - def test_to_qiskit(self): - nqubits = 5 - depth = 5 - pairs = [(i, np.mod(i + 1, nqubits)) for i in range(nqubits)] - circuit = rc.generate_gate(nqubits, depth, pairs) - pattern = circuit.transpile().pattern - state = pattern.simulate_pattern() - - ibmq_backend = IBMQBackend(pattern) - ibmq_backend.to_qiskit(save_statevector=True) - sim_result = ibmq_backend.simulate(format_result=False) - state_qiskit = sim_result.get_statevector(ibmq_backend.circ) - state_qiskit_mod = modify_statevector(np.array(state_qiskit), ibmq_backend.circ_output) - - np.testing.assert_almost_equal(np.abs(np.dot(state_qiskit_mod.conjugate(), state.flatten())), 1) - - def test_to_qiskit_after_pauli_preprocess(self): - nqubits = 5 - depth = 5 - pairs = [(i, np.mod(i + 1, nqubits)) for i in range(nqubits)] - circuit = rc.generate_gate(nqubits, depth, pairs) - pattern = circuit.transpile().pattern - pattern.perform_pauli_measurements() - pattern.minimize_space() - state = pattern.simulate_pattern() - - ibmq_backend = IBMQBackend(pattern) - ibmq_backend.to_qiskit(save_statevector=True) - sim_result = ibmq_backend.simulate(format_result=False) - state_qiskit = sim_result.get_statevector(ibmq_backend.circ) - state_qiskit_mod = modify_statevector(np.array(state_qiskit), ibmq_backend.circ_output) - - np.testing.assert_almost_equal(np.abs(np.dot(state_qiskit_mod.conjugate(), state.flatten())), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_result_utils.py b/tests/test_result_utils.py new file mode 100644 index 0000000..2fec48d --- /dev/null +++ b/tests/test_result_utils.py @@ -0,0 +1,17 @@ +from graphix_ibmq.result_utils import format_result + +class DummyPattern: + def __init__(self, n_node, output_nodes): + self.n_node = n_node + self.output_nodes = output_nodes + +def test_format_result_basic(): + # raw bitstrings: "01" が 10 回, "10" が 5 回 + raw = {"01": 10, "10": 5} + # N_node = 2, 出力ノード [0,1] + patt = DummyPattern(n_node=2, output_nodes=[0, 1]) + # ノード→クラシカルレジスタのマッピング + register_dict = {0: 0, 1: 1} + formatted = format_result(raw, patt, register_dict) + # 期待:ビット列の上位ノード順に取り出し → "10":10, "01":5 + assert formatted == {"10": 10, "01": 5} diff --git a/tests/test_simulation.py b/tests/test_simulation.py new file mode 100644 index 0000000..dbbd6a3 --- /dev/null +++ b/tests/test_simulation.py @@ -0,0 +1,4 @@ +import pytest +from graphix_ibmq.backend import IBMQBackend + +# to be implemented From 94f5506bc12e0c2c13cdbd5fcfcac0ca8dc41c4a Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 28 Apr 2025 18:54:19 +0900 Subject: [PATCH 04/30] edit setup.py --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a19f1b4..1ef0110 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,14 @@ with open("graphix_ibmq/version.py") as fp: exec(fp.read(), version) -requirements = [requirement.strip() for requirement in open("requirements.txt").readlines()] +# requirements = [requirement.strip() for requirement in open("requirements.txt").readlines()] +requirements = [ + "numpy>=1.22,<1.26", + "qiskit>=1.0", + "qiskit_ibm_runtime==0.37.0", + "qiskit-aer", + "graphix", +] info = { "name": "graphix_ibmq", From edd098b5c7635794c273471783d5085053361c84 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 15:13:23 +0900 Subject: [PATCH 05/30] update tox.ini --- tests/test_ibmq_backend_temp.py | 98 --------------------------------- tox.ini | 5 +- 2 files changed, 3 insertions(+), 100 deletions(-) delete mode 100644 tests/test_ibmq_backend_temp.py diff --git a/tests/test_ibmq_backend_temp.py b/tests/test_ibmq_backend_temp.py deleted file mode 100644 index 17d3c78..0000000 --- a/tests/test_ibmq_backend_temp.py +++ /dev/null @@ -1,98 +0,0 @@ -import unittest -import numpy as np -import networkx as nx -from graphix.transpiler import Circuit -from graphix_ibmq.backend import IBMQBackend -import qiskit.quantum_info as qi - -class TestIBMQBackend(unittest.TestCase): - - @classmethod - def setUpClass(cls): - # IBMQBackendを一度初期化 - cls.backend = IBMQBackend() - - def test_backend_initialization(self): - """Backendが正しく初期化されるか""" - self.assertIsInstance(self.backend, IBMQBackend) - - def test_run_simple_pattern(self): - """簡単な2qubit QFTパターンでrunできるか""" - n = 2 - circuit = Circuit(n) - self._qft(circuit, n) - pattern = circuit.transpile().pattern - pattern.minimize_space() - - result = self.backend.run(pattern, shots=1024) - self.assertIsInstance(result, dict) - self.assertTrue(len(result) > 0) - - def test_result_matches_theory(self): - """実行結果が理論分布と大きくずれていないか""" - n = 2 - circuit = Circuit(n) - self._qft(circuit, n) - pattern = circuit.transpile().pattern - pattern.minimize_space() - - result = self.backend.run(pattern, shots=1024) - - # 理想結果を計算 - qc = circuit.to_qiskit() - ideal = qi.Statevector.from_instruction(qc) - probs = ideal.probabilities_dict() - - # 実機結果を正規化 - total_shots = sum(result.values()) - exp_probs = {k: v / total_shots for k, v in result.items()} - - # L1ノルム (差の総和) が小さいかチェック - l1_distance = sum(abs(exp_probs.get(k, 0) - probs.get(k, 0)) for k in set(probs) | set(exp_probs)) - self.assertLess(l1_distance, 0.3) # 許容誤差: 30% - - def test_invalid_option(self): - """不正な引数でエラーになるか""" - n = 2 - circuit = Circuit(n) - self._qft(circuit, n) - pattern = circuit.transpile().pattern - - with self.assertRaises(TypeError): - # わざと不正なオプションを渡す - self.backend.run(pattern, shots="invalid") - - @staticmethod - def _qft(circuit, n): - """QFT生成(テスト内関数)""" - for i in range(n): - TestIBMQBackend._qft_rotations(circuit, i) - TestIBMQBackend._swap_registers(circuit, n) - - @staticmethod - def _qft_rotations(circuit, n): - if n == circuit.width: - return - circuit.h(n) - for qubit in range(n+1, circuit.width): - theta = np.pi / 2 ** (qubit - n) - TestIBMQBackend._cp(circuit, theta, qubit, n) - - @staticmethod - def _cp(circuit, theta, control, target): - circuit.rz(control, theta / 2) - circuit.rz(target, theta / 2) - circuit.cnot(control, target) - circuit.rz(target, -theta / 2) - circuit.cnot(control, target) - - @staticmethod - def _swap_registers(circuit, n): - for qubit in range(n // 2): - TestIBMQBackend._swap(circuit, qubit, n - qubit - 1) - - @staticmethod - def _swap(circuit, a, b): - circuit.cnot(a, b) - circuit.cnot(b, a) - circuit.cnot(a, b) diff --git a/tox.ini b/tox.ini index 423fc80..fbde51e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] -envlist = py38, py39, py310, py311, lint +envlist = py39, py310, py311, lint [gh-actions] python = - 3.8: lint, py38 + 3.8: lint 3.9: py39 3.10: py310 3.11: py311 @@ -12,6 +12,7 @@ python = description = Run the unit tests deps = -r {toxinidir}/requirements.txt + git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix commands = pip install --upgrade pip pip install pytest From d704a07959acc45289aea11b84db0b53ec4d425b Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 15:17:25 +0900 Subject: [PATCH 06/30] update requirement.txt --- requirements.txt | 1 - tox.ini | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d83d2cf..45e7d8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ numpy>=1.22,<1.26 qiskit>=1.0 qiskit_ibm_runtime==0.37.0 qiskit-aer -git+https://github.com/TeamGraphix/graphix.git@device_interface_abc#egg=graphix diff --git a/tox.ini b/tox.ini index fbde51e..8bd4745 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] -envlist = py39, py310, py311, lint +envlist = py38, py39, py310, py311, lint [gh-actions] python = - 3.8: lint + 3.8: lint, py38 3.9: py39 3.10: py310 3.11: py311 From 7be645292f2b69ba3d0806ebcb0ace5a88b96328 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 15:33:28 +0900 Subject: [PATCH 07/30] add py3.12, remove py3.8 in test and support --- setup.py | 4 ++-- tox.ini | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 1ef0110..d4788e1 100644 --- a/setup.py +++ b/setup.py @@ -34,15 +34,15 @@ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Topic :: Scientific/Engineering :: Physics", ], - "python_requires": ">=3.8,<3.12", + "python_requires": ">=3.8,<3.13", "install_requires": requirements, "extras_require": {"test": ["graphix"]}, } diff --git a/tox.ini b/tox.ini index 8bd4745..4c42b52 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = py38, py39, py310, py311, lint +envlist = py39, py310, py311, py312, lint [gh-actions] python = - 3.8: lint, py38 - 3.9: py39 + 3.9: lint, py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] description = Run the unit tests @@ -20,7 +20,7 @@ commands = extras = test [testenv:lint] -basepython = python3.8 +basepython = python3.9 deps = black==22.8.0 commands = From 4d695d696e5c0e3cbb95acf0135262d0f678020d Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 15:36:02 +0900 Subject: [PATCH 08/30] edit ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 214b235..30cec2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-2022', 'macos-latest'] - python: ['3.8', '3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12'] name: "Python ${{ matrix.python }} / ${{ matrix.os }}" runs-on: ${{ matrix.os }} From edf5f22b0aabe8cb6c21d35397be2cc928e8b261 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 15:46:08 +0900 Subject: [PATCH 09/30] black --- graphix_ibmq/backend.py | 17 +++++------------ graphix_ibmq/compile_options.py | 2 +- graphix_ibmq/compiler.py | 4 +--- graphix_ibmq/job.py | 2 +- graphix_ibmq/result_utils.py | 4 +--- tests/test_backend.py | 6 ++++++ tests/test_compile_options.py | 2 ++ tests/test_result_utils.py | 2 ++ 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index a690f14..c9a7305 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -29,7 +29,7 @@ def __init__(self) -> None: self._compiled_circuit: Optional[QuantumCircuit] = None self._execution_mode: Optional[str] = None - def compile(self, pattern : Pattern, options: Optional[CompileOptions] = None) -> None: + def compile(self, pattern: Pattern, options: Optional[CompileOptions] = None) -> None: """Compile the assigned pattern using IBMQ options. Parameters @@ -43,7 +43,7 @@ def compile(self, pattern : Pattern, options: Optional[CompileOptions] = None) - raise TypeError("Expected IBMQCompileOptions") else: self._options = options - + self._pattern = pattern self._compiler = IBMQPatternCompiler(pattern) @@ -96,9 +96,7 @@ def select_backend( service = QiskitRuntimeService() if least_busy or name is None: - self._resource = service.least_busy( - min_num_qubits=min_qubits, operational=True - ) + self._resource = service.least_busy(min_num_qubits=min_qubits, operational=True) else: self._resource = service.backend(name) @@ -124,9 +122,7 @@ def submit_job(self, shots: int = 1024) -> Job: raise RuntimeError("Pattern must be compiled before submission.") if self._execution_mode is None: - raise RuntimeError( - "Execution mode is not configured. Use select_backend() or set_simulator()." - ) + raise RuntimeError("Execution mode is not configured. Use select_backend() or set_simulator().") if self._execution_mode == "simulation": pm = generate_preset_pass_manager( @@ -149,7 +145,4 @@ def submit_job(self, shots: int = 1024) -> Job: return IBMQJob(job, self._compiler) else: - raise RuntimeError( - "Execution mode is not configured. Use select_backend() or set_simulator()." - ) - \ No newline at end of file + raise RuntimeError("Execution mode is not configured. Use select_backend() or set_simulator().") diff --git a/graphix_ibmq/compile_options.py b/graphix_ibmq/compile_options.py index 649a77c..34ef6bc 100644 --- a/graphix_ibmq/compile_options.py +++ b/graphix_ibmq/compile_options.py @@ -15,7 +15,7 @@ class IBMQCompileOptions(CompileOptions): layout_method : str Qubit layout method used by the transpiler (for future use). """ - + def __init__( self, optimization_level: int = 3, diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index 7acb88b..fc32d96 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -23,9 +23,7 @@ def __init__(self, pattern: Pattern) -> None: self._register_dict: dict[int, int] = {} self._circ_output: list[int] = [] - def to_qiskit_circuit( - self, save_statevector: bool, layout_method: str - ) -> QuantumCircuit: + def to_qiskit_circuit(self, save_statevector: bool, layout_method: str) -> QuantumCircuit: """ Convert the MBQC pattern into a Qiskit QuantumCircuit. diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index afdffaa..b5dd77e 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -61,7 +61,7 @@ def get_status(self) -> str: Status string representing the job state. """ return self.job.status() - + def retrieve_result(self, raw_result: bool = False): """Retrieve the result from a completed job. diff --git a/graphix_ibmq/result_utils.py b/graphix_ibmq/result_utils.py index bf7fe27..e92df8e 100644 --- a/graphix_ibmq/result_utils.py +++ b/graphix_ibmq/result_utils.py @@ -3,9 +3,7 @@ from graphix.pattern import Pattern -def format_result( - result: Dict[str, int], pattern: Pattern, register_dict: Dict[int, int] -) -> Dict[str, int]: +def format_result(result: Dict[str, int], pattern: Pattern, register_dict: Dict[int, int]) -> Dict[str, int]: """Format raw measurement results into output-only bitstrings. Parameters diff --git a/tests/test_backend.py b/tests/test_backend.py index ed6e8d9..fcf19ff 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -8,10 +8,12 @@ class DummyCompiler: def __init__(self, pattern): self.pattern = pattern + def to_qiskit_circuit(self, save_statevector, layout_method): # シンプルな 1-qubit 回路を返す return QuantumCircuit(1) + def test_compile_default(monkeypatch): # IBMQPatternCompiler をダミーに置き換え monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) @@ -24,12 +26,14 @@ def test_compile_default(monkeypatch): assert isinstance(backend._compiled_circuit, QuantumCircuit) assert isinstance(backend._options, IBMQCompileOptions) + def test_compile_invalid_options(monkeypatch): monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) backend = IBMQBackend() with pytest.raises(TypeError): backend.compile(object(), options="not-an-options") + def test_set_simulator(): backend = IBMQBackend() backend.set_simulator() @@ -37,12 +41,14 @@ def test_set_simulator(): assert isinstance(backend._simulator, AerSimulator) assert backend._noise_model is None + def test_submit_job_not_compiled(): backend = IBMQBackend() with pytest.raises(RuntimeError) as e: backend.submit_job() assert "Pattern must be compiled" in str(e.value) + def test_submit_job_without_execution_mode(monkeypatch): monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) backend = IBMQBackend() diff --git a/tests/test_compile_options.py b/tests/test_compile_options.py index ad7ea47..c518537 100644 --- a/tests/test_compile_options.py +++ b/tests/test_compile_options.py @@ -1,12 +1,14 @@ import pytest from graphix_ibmq.compile_options import IBMQCompileOptions + def test_default_options(): opts = IBMQCompileOptions() assert opts.optimization_level == 3 assert opts.save_statevector is False assert opts.layout_method == "trivial" + def test_repr(): opts = IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method="dense") expected = "IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method='dense')" diff --git a/tests/test_result_utils.py b/tests/test_result_utils.py index 2fec48d..2fcddeb 100644 --- a/tests/test_result_utils.py +++ b/tests/test_result_utils.py @@ -1,10 +1,12 @@ from graphix_ibmq.result_utils import format_result + class DummyPattern: def __init__(self, n_node, output_nodes): self.n_node = n_node self.output_nodes = output_nodes + def test_format_result_basic(): # raw bitstrings: "01" が 10 回, "10" が 5 回 raw = {"01": 10, "10": 5} From 66154e7fe40355ff123278c1deceb1bff911923c Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 15:49:55 +0900 Subject: [PATCH 10/30] update tox.ini --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4c42b52..1796ce1 100644 --- a/tox.ini +++ b/tox.ini @@ -12,9 +12,11 @@ python = description = Run the unit tests deps = -r {toxinidir}/requirements.txt - git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix commands = pip install --upgrade pip + pip install --upgrade pip setuptools wheel + pip install --no-build-isolation \ + git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix pip install pytest pytest {toxinidir} extras = test From 56219000282e67e1f122ec420c36bdcc47335f87 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 16:16:38 +0900 Subject: [PATCH 11/30] update tox.ini --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 1796ce1..2f7498b 100644 --- a/tox.ini +++ b/tox.ini @@ -10,13 +10,13 @@ python = [testenv] description = Run the unit tests +setenv = + PIP_NO_BUILD_ISOLATION = 1 deps = -r {toxinidir}/requirements.txt + git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix commands = pip install --upgrade pip - pip install --upgrade pip setuptools wheel - pip install --no-build-isolation \ - git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix pip install pytest pytest {toxinidir} extras = test From 9ebab226fef8f20c5ecd2234cc040d40f1ca0d62 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 16:21:02 +0900 Subject: [PATCH 12/30] remove py3.12 from support and test --- setup.py | 3 +-- tox.ini | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index d4788e1..2f002f6 100644 --- a/setup.py +++ b/setup.py @@ -37,12 +37,11 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Topic :: Scientific/Engineering :: Physics", ], - "python_requires": ">=3.8,<3.13", + "python_requires": ">=3.8,<3.12", "install_requires": requirements, "extras_require": {"test": ["graphix"]}, } diff --git a/tox.ini b/tox.ini index 2f7498b..aa1642a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ [tox] -envlist = py39, py310, py311, py312, lint +envlist = py39, py310, py311, lint [gh-actions] python = 3.9: lint, py39 3.10: py310 3.11: py311 - 3.12: py312 [testenv] description = Run the unit tests From ddfaff6f00a8afd911ab987c74708d9b9a7ed85f Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 12 May 2025 16:21:37 +0900 Subject: [PATCH 13/30] remove py3.12 from support and test --- .github/workflows/ci.yml | 2 +- tox.ini | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30cec2f..b770d1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-2022', 'macos-latest'] - python: ['3.9', '3.10', '3.11', '3.12'] + python: ['3.9', '3.10', '3.11'] name: "Python ${{ matrix.python }} / ${{ matrix.os }}" runs-on: ${{ matrix.os }} diff --git a/tox.ini b/tox.ini index aa1642a..98fae00 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,6 @@ python = [testenv] description = Run the unit tests -setenv = - PIP_NO_BUILD_ISOLATION = 1 deps = -r {toxinidir}/requirements.txt git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix From ad49e67e558e639040b6094da9649a5e4f7510ed Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 26 May 2025 18:30:10 +0900 Subject: [PATCH 14/30] delete abstract class in graphix --- docs/source/tutorial.rst | 2 +- examples/gallery/aer_sim.py | 54 ++++++++++----------------------- examples/ibm_device.py | 45 ++++++++++++--------------- graphix_ibmq/backend.py | 15 ++++----- graphix_ibmq/compile_options.py | 3 +- graphix_ibmq/job.py | 3 +- 6 files changed, 46 insertions(+), 76 deletions(-) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index e7bec8c..7a47076 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -29,7 +29,7 @@ First, let us import relevant modules and define function we will use: .. code-block:: python from graphix import Circuit - from graphix_ibmq.runner import IBMQBackend + from graphix_ibmq.backend import IBMQBackend import qiskit.quantum_info as qi from qiskit.visualization import plot_histogram import numpy as np diff --git a/examples/gallery/aer_sim.py b/examples/gallery/aer_sim.py index 66dc993..fca179a 100644 --- a/examples/gallery/aer_sim.py +++ b/examples/gallery/aer_sim.py @@ -12,10 +12,9 @@ import matplotlib.pyplot as plt import networkx as nx import random -from graphix import Circuit -from graphix_ibmq.runner import IBMQBackend -from qiskit.tools.visualization import plot_histogram -from qiskit_aer.noise import NoiseModel, depolarizing_error +from graphix.transpiler import Circuit +from graphix_ibmq.backend import IBMQBackend +from qiskit.visualization import plot_histogram def cp(circuit, theta, control, target): @@ -84,23 +83,29 @@ def swap(circuit, a, b): pattern.minimize_space() # convert to qiskit circuit -backend = IBMQBackend(pattern) -print(type(backend.circ)) +backend = IBMQBackend() +backend.compile(pattern) +print(type(backend._compiled_circuit)) #%% # We can now simulate the circuit with Aer. # run and get counts -result = backend.simulate() +backend.set_simulator() +job = backend.submit_job(shots=1024) +result = job.retrieve_result() #%% # We can also simulate the circuit with noise model # create an empty noise model +from qiskit_aer.noise import NoiseModel, depolarizing_error + +# add depolarizing error to all single qubit gates noise_model = NoiseModel() -# add depolarizing error to all single qubit u1, u2, u3 gates error = depolarizing_error(0.01, 1) -noise_model.add_all_qubit_quantum_error(error, ["u1", "u2", "u3"]) +noise_model.add_all_qubit_quantum_error(error, ["id", "rz", "sx", "x", "u1"]) +backend.set_simulator(noise_model=noise_model) # print noise model info print(noise_model) @@ -109,7 +114,8 @@ def swap(circuit, a, b): # Now we can run the simulation with noise model # run and get counts -result_noise = backend.simulate(noise_model=noise_model) +job = backend.submit_job(shots=1024) +result_noise = job.retrieve_result() #%% @@ -137,31 +143,3 @@ def swap(circuit, a, b): legend = ax.legend(fontsize=18) legend = ax.legend(loc='upper left') # %% - - -#%% -# Example demonstrating how to run a pattern on an IBM Quantum device. All explanations are provided as comments. - -# First, load the IBMQ account using an API token. -""" -from qiskit_ibm_runtime import QiskitRuntimeService -service = QiskitRuntimeService(channel="ibm_quantum", token="your_ibm_token", instance="ibm-q/open/main") -""" - -# Then, select the quantum system on which to run the circuit. -# If no system is specified, the least busy system will be automatically selected. -""" -backend.get_system(service, "ibm_kyoto") -""" - -# Finally, transpile the quantum circuit for the chosen system and execute it. -""" -backend.transpile() -result = backend.run(shots=128) -""" - -# To retrieve the result at a later time, use the code below. -""" -result = backend.retrieve_result("your_job_id") -""" -# %% diff --git a/examples/ibm_device.py b/examples/ibm_device.py index 2f3bf01..17ffb1e 100644 --- a/examples/ibm_device.py +++ b/examples/ibm_device.py @@ -12,10 +12,9 @@ import matplotlib.pyplot as plt import networkx as nx import random -from graphix import Circuit -from graphix_ibmq.runner import IBMQBackend -from qiskit_ibm_provider import IBMProvider -from qiskit.tools.visualization import plot_histogram +from graphix.transpiler import Circuit +from graphix_ibmq.backend import IBMQBackend +from qiskit.visualization import plot_histogram from qiskit.providers.fake_provider import FakeLagos @@ -68,7 +67,7 @@ def swap(circuit, a, b): swap(circuit, 0, 2) # transpile and plot the graph -pattern = circuit.transpile() +pattern = circuit.transpile().pattern nodes, edges = pattern.get_graph() g = nx.Graph() g.add_nodes_from(nodes) @@ -84,45 +83,39 @@ def swap(circuit, a, b): pattern.minimize_space() # convert to qiskit circuit -backend = IBMQBackend(pattern) -backend.to_qiskit() -print(type(backend.circ)) +backend = IBMQBackend() +backend.compile(pattern) +print(type(backend._compiled_circuit)) #%% # load the account with API token -IBMProvider.save_account(token='MY API TOKEN') +from qiskit_ibm_runtime import QiskitRuntimeService +QiskitRuntimeService.save_account(channel="ibm_quantum", token="API TOKEN", overwrite=True) # get the device backend -instance_name = 'ibm-q/open/main' -backend_name = "ibm_lagos" -backend.get_backend(instance=instance_name,resource=backend_name) - -#%% -# Get provider and the backend. - -instance_name = "ibm-q/open/main" -backend_name = "ibm_lagos" - -backend.get_backend(instance=instance_name, resource=backend_name) +backend.select_device() #%% # We can now execute the circuit on the device backend. -result = backend.run() +job = backend.submit_job(shots=1024) #%% -# Retrieve the job if needed +# Retrieve the job result -# result = backend.retrieve_result("Job ID") +if job.is_done: + result = job.retrieve_result() #%% -# We can simulate the circuit with noise model based on the device we used +# We can simulate the circuit with device-based noise model. # get the noise model of the device backend -backend_noisemodel = FakeLagos() +from qiskit_ibm_runtime.fake_provider import FakeManilaV2 +backend.set_simulator(based_on=FakeManilaV2()) # execute noisy simulation and get counts -result_noise = backend.simulate(noise_model=backend_noisemodel) +job = backend.submit_job(shots=1024) +result_noise = job.retrieve_result() #%% # Now let us compare the results with theoretical output diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index c9a7305..6a062ab 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -2,7 +2,6 @@ from typing import Optional, TYPE_CHECKING -from graphix.device_interface import DeviceBackend, CompileOptions, Job from graphix_ibmq.compiler import IBMQPatternCompiler from graphix_ibmq.job import IBMQJob from graphix_ibmq.compile_options import IBMQCompileOptions @@ -18,7 +17,7 @@ from graphix.pattern import Pattern -class IBMQBackend(DeviceBackend): +class IBMQBackend: """IBMQ backend implementation for compiling and executing quantum patterns.""" def __init__(self) -> None: @@ -29,7 +28,7 @@ def __init__(self) -> None: self._compiled_circuit: Optional[QuantumCircuit] = None self._execution_mode: Optional[str] = None - def compile(self, pattern: Pattern, options: Optional[CompileOptions] = None) -> None: + def compile(self, pattern: Pattern, options: Optional[IBMQCompileOptions] = None) -> None: """Compile the assigned pattern using IBMQ options. Parameters @@ -73,7 +72,7 @@ def set_simulator( self._noise_model = noise_model self._simulator = AerSimulator(noise_model=noise_model) - def select_backend( + def select_device( self, name: Optional[str] = None, least_busy: bool = False, @@ -84,9 +83,9 @@ def select_backend( Parameters ---------- name : str, optional - Specific backend name to use. + Specific device name to use. least_busy : bool, optional - If True, select the least busy backend that meets requirements. + If True, select the least busy device that meets requirements. min_qubits : int, optional Minimum number of qubits required. """ @@ -100,7 +99,9 @@ def select_backend( else: self._resource = service.backend(name) - def submit_job(self, shots: int = 1024) -> Job: + print(f"Selected device: {self._resource.name}") + + def submit_job(self, shots: int = 1024) -> IBMQJob: """Submit the compiled circuit to either simulator or hardware backend. Parameters diff --git a/graphix_ibmq/compile_options.py b/graphix_ibmq/compile_options.py index 34ef6bc..1de76cf 100644 --- a/graphix_ibmq/compile_options.py +++ b/graphix_ibmq/compile_options.py @@ -1,9 +1,8 @@ from dataclasses import dataclass -from graphix.device_interface import CompileOptions @dataclass -class IBMQCompileOptions(CompileOptions): +class IBMQCompileOptions: """Compilation options specific to IBMQ backends. Attributes diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index b5dd77e..87b0c7c 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -1,8 +1,7 @@ -from graphix.device_interface import Job from graphix_ibmq.result_utils import format_result -class IBMQJob(Job): +class IBMQJob: """Job handler class for IBMQ devices and simulators.""" def __init__(self, job, compiler) -> None: From c4748e7e04496034fb65228d33d9310ee15f0e2b Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 26 May 2025 18:39:59 +0900 Subject: [PATCH 15/30] update test --- tests/test_backend.py | 5 +---- tests/test_result_utils.py | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index fcf19ff..b069c78 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -4,24 +4,21 @@ from qiskit import QuantumCircuit from qiskit_aer import AerSimulator -# モンキーパッチ用ダミーコンパイラ class DummyCompiler: def __init__(self, pattern): self.pattern = pattern def to_qiskit_circuit(self, save_statevector, layout_method): - # シンプルな 1-qubit 回路を返す return QuantumCircuit(1) def test_compile_default(monkeypatch): - # IBMQPatternCompiler をダミーに置き換え monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) backend = IBMQBackend() dummy_pattern = object() backend.compile(dummy_pattern) - # 内部状態の確認 + assert backend._pattern is dummy_pattern assert isinstance(backend._compiled_circuit, QuantumCircuit) assert isinstance(backend._options, IBMQCompileOptions) diff --git a/tests/test_result_utils.py b/tests/test_result_utils.py index 2fcddeb..172c22f 100644 --- a/tests/test_result_utils.py +++ b/tests/test_result_utils.py @@ -8,12 +8,8 @@ def __init__(self, n_node, output_nodes): def test_format_result_basic(): - # raw bitstrings: "01" が 10 回, "10" が 5 回 raw = {"01": 10, "10": 5} - # N_node = 2, 出力ノード [0,1] patt = DummyPattern(n_node=2, output_nodes=[0, 1]) - # ノード→クラシカルレジスタのマッピング register_dict = {0: 0, 1: 1} formatted = format_result(raw, patt, register_dict) - # 期待:ビット列の上位ノード順に取り出し → "10":10, "01":5 assert formatted == {"10": 10, "01": 5} From 9d9adf11a7d8c56acaa05de16f61fc45b72de5a0 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 26 May 2025 20:50:47 +0900 Subject: [PATCH 16/30] update test --- CHANGELOG.md | 1 + tests/test_backend.py | 1 + tests/test_compiler.py | 47 ++++++++++++++++++++++++++++++++++++++++ tests/test_simulation.py | 4 ---- 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 tests/test_compiler.py delete mode 100644 tests/test_simulation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d811d..f795315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated to support Qiskit 1.0. +- Modified the APIs for Qiskit simulation and execution on IBMQ hardware. ### Fixed diff --git a/tests/test_backend.py b/tests/test_backend.py index b069c78..735326e 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -4,6 +4,7 @@ from qiskit import QuantumCircuit from qiskit_aer import AerSimulator + class DummyCompiler: def __init__(self, pattern): self.pattern = pattern diff --git a/tests/test_compiler.py b/tests/test_compiler.py new file mode 100644 index 0000000..f60580e --- /dev/null +++ b/tests/test_compiler.py @@ -0,0 +1,47 @@ +import pytest +import numpy as np +from qiskit import transpile +from qiskit_aer import AerSimulator +from graphix_ibmq.compiler import IBMQPatternCompiler +import random_circuit as rc + + +def reduce_statevector_to_outputs(statevector: np.ndarray, output_qubits: list) -> np.ndarray: + """Reduce a full statevector to the subspace corresponding to output_qubits.""" + n = round(np.log2(len(statevector))) + reduced = np.zeros(2 ** len(output_qubits), dtype=complex) + for i, amp in enumerate(statevector): + bin_str = format(i, f"0{n}b") + reduced_idx = "".join([bin_str[n - idx - 1] for idx in output_qubits]) + reduced[int(reduced_idx, 2)] += amp + return reduced + + +@pytest.mark.parametrize( + "nqubits, depth", + [ + (3, 3), + (4, 4), + (5, 5), + ], +) +def test_ibmq_compiler_statevector_equivalence(nqubits, depth): + """Test that IBMQPatternCompiler circuit reproduces the same statevector as MBQC simulation.""" + sim = AerSimulator() + + for _ in range(5): # repeat with different random circuits + pairs = [(i, (i + 1) % nqubits) for i in range(nqubits)] + circuit = rc.generate_gate(nqubits, depth, pairs) + pattern = circuit.transpile().pattern + mbqc_state = pattern.simulate_pattern() + + compiler = IBMQPatternCompiler(pattern) + qc = compiler.to_qiskit_circuit(save_statevector=True, layout_method=None) + + transpiled = transpile(qc, sim) + result = sim.run(transpiled).result() + qiskit_state = np.array(result.get_statevector(qc)) + qiskit_reduced = reduce_statevector_to_outputs(qiskit_state, compiler._circ_output) + + fidelity = np.abs(np.dot(qiskit_reduced.conjugate(), mbqc_state.flatten())) + assert np.isclose(fidelity, 1.0, atol=1e-6), f"Fidelity mismatch: {fidelity}" diff --git a/tests/test_simulation.py b/tests/test_simulation.py deleted file mode 100644 index dbbd6a3..0000000 --- a/tests/test_simulation.py +++ /dev/null @@ -1,4 +0,0 @@ -import pytest -from graphix_ibmq.backend import IBMQBackend - -# to be implemented From b1538224e8f64c6145342eb634d173cb94247cd1 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Fri, 20 Jun 2025 21:31:19 +0900 Subject: [PATCH 17/30] refactor according to the review --- graphix_ibmq/backend.py | 44 ++++++++++++++++----------- graphix_ibmq/compile_options.py | 25 ++++------------ graphix_ibmq/compiler.py | 32 ++++++++++++++------ graphix_ibmq/converter.py | 2 ++ graphix_ibmq/job.py | 53 ++++++++++++++------------------- graphix_ibmq/result_utils.py | 15 ++++++---- tests/test_backend.py | 4 +-- 7 files changed, 91 insertions(+), 84 deletions(-) diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index 6a062ab..16e488b 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -1,34 +1,43 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING - from graphix_ibmq.compiler import IBMQPatternCompiler from graphix_ibmq.job import IBMQJob from graphix_ibmq.compile_options import IBMQCompileOptions -from qiskit import QuantumCircuit from qiskit_aer.noise import NoiseModel -from qiskit.providers.backend import BackendV2 from qiskit_aer import AerSimulator from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import SamplerV2 as Sampler +from typing import TYPE_CHECKING + if TYPE_CHECKING: from graphix.pattern import Pattern + from qiskit import QuantumCircuit + from qiskit.providers.backend import BackendV2 class IBMQBackend: """IBMQ backend implementation for compiling and executing quantum patterns.""" + _options: IBMQCompileOptions + _compiled_circuit: QuantumCircuit | None + _execution_mode: str | None + _noise_model: NoiseModel | None + _simulator: AerSimulator | None + _resource: BackendV2 | None + _compiler: IBMQPatternCompiler | None + def __init__(self) -> None: - """Initialize the IBMQ backend.""" - super().__init__() - self._compiler: Optional[IBMQPatternCompiler] = None - self._options: Optional[IBMQCompileOptions] = None - self._compiled_circuit: Optional[QuantumCircuit] = None - self._execution_mode: Optional[str] = None - - def compile(self, pattern: Pattern, options: Optional[IBMQCompileOptions] = None) -> None: + self._compiler = None + self._options = IBMQCompileOptions() + self._compiled_circuit = None + self._execution_mode = None + self._noise_model = None + self._simulator = None + self._resource = None + + def compile(self, pattern: Pattern, options: IBMQCompileOptions | None = None) -> None: """Compile the assigned pattern using IBMQ options. Parameters @@ -43,8 +52,6 @@ def compile(self, pattern: Pattern, options: Optional[IBMQCompileOptions] = None else: self._options = options - self._pattern = pattern - self._compiler = IBMQPatternCompiler(pattern) self._compiled_circuit = self._compiler.to_qiskit_circuit( save_statevector=self._options.save_statevector, @@ -53,8 +60,8 @@ def compile(self, pattern: Pattern, options: Optional[IBMQCompileOptions] = None def set_simulator( self, - noise_model: Optional[NoiseModel] = None, - based_on: Optional[BackendV2] = None, + noise_model: NoiseModel | None = None, + based_on: BackendV2 | None = None, ) -> None: """Configure the backend to use a simulator. @@ -74,7 +81,7 @@ def set_simulator( def select_device( self, - name: Optional[str] = None, + name: str | None = None, least_busy: bool = False, min_qubits: int = 1, ) -> None: @@ -125,6 +132,9 @@ def submit_job(self, shots: int = 1024) -> IBMQJob: if self._execution_mode is None: raise RuntimeError("Execution mode is not configured. Use select_backend() or set_simulator().") + if not hasattr(self, "_options") or self._options is None: + raise RuntimeError("Compile options are not set.") + if self._execution_mode == "simulation": pm = generate_preset_pass_manager( backend=self._simulator, diff --git a/graphix_ibmq/compile_options.py b/graphix_ibmq/compile_options.py index 1de76cf..1fc0972 100644 --- a/graphix_ibmq/compile_options.py +++ b/graphix_ibmq/compile_options.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass @@ -15,26 +17,9 @@ class IBMQCompileOptions: Qubit layout method used by the transpiler (for future use). """ - def __init__( - self, - optimization_level: int = 3, - save_statevector: bool = False, - layout_method: str = "trivial", - ) -> None: - """Initialize the compilation options. - - Parameters - ---------- - optimization_level : int - Optimization level for Qiskit transpiler (0 to 3). - save_statevector : bool - Whether to save the statevector before measurement (for debugging/testing). - layout_method : str - Qubit layout method used by the transpiler (for future use). - """ - self.optimization_level = optimization_level - self.save_statevector = save_statevector - self.layout_method = layout_method + optimization_level: int = 3 + save_statevector: bool = False + layout_method: str = "trivial" def __repr__(self) -> str: """Return a string representation of the compilation options.""" diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index fc32d96..1b3afe1 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -1,14 +1,29 @@ +from __future__ import annotations + import numpy as np from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from graphix_ibmq.clifford import CLIFFORD_TO_QISKIT -from graphix.pattern import Pattern from graphix.command import CommandKind from graphix.fundamentals import Plane +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from graphix.pattern import Pattern + class IBMQPatternCompiler: - """Compiler that translates a Graphix Pattern into a Qiskit QuantumCircuit.""" + """Compiler that translates a Graphix Pattern into a Qiskit QuantumCircuit. + + Attributes + ---------- + _pattern : Pattern + The measurement-based quantum computation pattern to compile. + _register_dict : dict[int, int] + Mapping from pattern node indices to classical register indices. + _circ_output : list[int] + List of output qubit indices in the compiled circuit. + """ def __init__(self, pattern: Pattern) -> None: """ @@ -40,10 +55,10 @@ def to_qiskit_circuit(self, save_statevector: bool, layout_method: str) -> Quant The compiled Qiskit circuit. """ n = self._pattern.max_space() - N_node = self._pattern.n_node + n_node = self._pattern.n_node qr = QuantumRegister(n) - cr = ClassicalRegister(N_node, name="meas") + cr = ClassicalRegister(n_node, name="meas") circ = QuantumCircuit(qr, cr) empty_qubit = list(range(n)) # available qubit indices @@ -51,7 +66,7 @@ def to_qiskit_circuit(self, save_statevector: bool, layout_method: str) -> Quant register_dict: dict[int, int] = {} # pattern node -> classical register reg_idx = 0 - def signal_process(op: str, circ_idx: int, signal: list[int]) -> None: + def signal_process(op: str, circ_idx: int, signal: set[int]) -> None: """Apply classically-controlled X or Z gates based on measurement outcomes.""" if op == "X": for s in signal: @@ -122,9 +137,8 @@ def signal_process(op: str, circ_idx: int, signal: list[int]) -> None: elif cmd.kind == CommandKind.C: circ_idx = qubit_dict[cmd.node] - cid = cmd.clifford - for op in CLIFFORD_TO_QISKIT[cid]: - exec(f"circ.{op}({circ_idx})") + for method_name in cmd.qasm3: + getattr(circ, method_name)(circ_idx) # Handle output measurements if save_statevector: diff --git a/graphix_ibmq/converter.py b/graphix_ibmq/converter.py index 83c8422..243927a 100644 --- a/graphix_ibmq/converter.py +++ b/graphix_ibmq/converter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from graphix import Circuit from qiskit import QuantumCircuit, transpile diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index 87b0c7c..05dc5bf 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -1,21 +1,33 @@ +from __future__ import annotations + from graphix_ibmq.result_utils import format_result +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from qiskit_ibm_runtime.fake_provider.local_runtime_job import LocalRuntimeJob + from qiskit_ibm_runtime.runtime_job_v2 import RuntimeJobV2 + from graphix_ibmq.compiler import IBMQPatternCompiler + class IBMQJob: """Job handler class for IBMQ devices and simulators.""" - def __init__(self, job, compiler) -> None: + def __init__( + self, + job: LocalRuntimeJob | RuntimeJobV2, + compiler: IBMQPatternCompiler, + ) -> None: """ - Initialize with a Qiskit Runtime job object. - Parameters - ---------- - job : Any - The job object returned from Qiskit Runtime or Aer Sampler. + ---------- + job : LocalRuntimeJob | RuntimeJobV2 + The job object returned from Qiskit Runtime or real v2 runtime. """ self.job = job self._compiler = compiler + @property def get_id(self) -> str: """ Get the unique identifier of the job. @@ -37,29 +49,10 @@ def is_done(self) -> bool: bool True if the job is done, False otherwise. """ - try: - # Simulator jobs typically use .status().name - return self.job.status().name == "DONE" - except AttributeError: - # Hardware jobs may return status as a string - return str(self.job.status()).upper() == "DONE" - - def cancel(self) -> None: - """ - Cancel the job if it's still running. - """ - self.job.cancel() - - def get_status(self) -> str: - """ - Get the current status of the job. - - Returns - ------- - str - Status string representing the job state. - """ - return self.job.status() + status = self.job.status() + if isinstance(status, str): + return status.upper() == "DONE" + return status.name == "DONE" def retrieve_result(self, raw_result: bool = False): """Retrieve the result from a completed job. @@ -83,7 +76,7 @@ def retrieve_result(self, raw_result: bool = False): """ if not self.is_done: - print("Job not done.") + # job not completed yet; skip retrieval return None result = self.job.result() diff --git a/graphix_ibmq/result_utils.py b/graphix_ibmq/result_utils.py index e92df8e..ec85a75 100644 --- a/graphix_ibmq/result_utils.py +++ b/graphix_ibmq/result_utils.py @@ -1,9 +1,12 @@ -from typing import Dict +from __future__ import annotations -from graphix.pattern import Pattern +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from graphix.pattern import Pattern -def format_result(result: Dict[str, int], pattern: Pattern, register_dict: Dict[int, int]) -> Dict[str, int]: + +def format_result(result: dict[str, int], pattern: Pattern, register_dict: dict[int, int]) -> dict[str, int]: """Format raw measurement results into output-only bitstrings. Parameters @@ -20,12 +23,12 @@ def format_result(result: Dict[str, int], pattern: Pattern, register_dict: Dict[ formatted : dict of str to int Dictionary of bitstrings only for output nodes and their counts. """ - N_node = pattern.n_node + n_node = pattern.n_node output_keys = [register_dict[node] for node in pattern.output_nodes] - formatted: Dict[str, int] = {} + formatted: dict[str, int] = {} for bitstring, count in result.items(): - masked = "".join(bitstring[N_node - 1 - idx] for idx in output_keys) + masked = "".join(bitstring[n_node - 1 - idx] for idx in output_keys) formatted[masked] = formatted.get(masked, 0) + count return formatted diff --git a/tests/test_backend.py b/tests/test_backend.py index 735326e..ffec71f 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -7,7 +7,7 @@ class DummyCompiler: def __init__(self, pattern): - self.pattern = pattern + self._pattern = pattern def to_qiskit_circuit(self, save_statevector, layout_method): return QuantumCircuit(1) @@ -20,7 +20,7 @@ def test_compile_default(monkeypatch): backend.compile(dummy_pattern) - assert backend._pattern is dummy_pattern + assert backend._compiler._pattern is dummy_pattern assert isinstance(backend._compiled_circuit, QuantumCircuit) assert isinstance(backend._options, IBMQCompileOptions) From 409ae3c339723e4e7f2385effd0deb232ae0b4cf Mon Sep 17 00:00:00 2001 From: d1ssk Date: Thu, 26 Jun 2025 19:31:53 +0900 Subject: [PATCH 18/30] unpin dependencies --- .github/workflows/ci.yml | 2 +- requirements.txt | 4 ++-- setup.py | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b770d1f..30cec2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-2022', 'macos-latest'] - python: ['3.9', '3.10', '3.11'] + python: ['3.9', '3.10', '3.11', '3.12'] name: "Python ${{ matrix.python }} / ${{ matrix.os }}" runs-on: ${{ matrix.os }} diff --git a/requirements.txt b/requirements.txt index 45e7d8a..e08d3a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy>=1.22,<1.26 +numpy qiskit>=1.0 -qiskit_ibm_runtime==0.37.0 +qiskit_ibm_runtime qiskit-aer diff --git a/setup.py b/setup.py index 2f002f6..a756615 100644 --- a/setup.py +++ b/setup.py @@ -9,9 +9,9 @@ # requirements = [requirement.strip() for requirement in open("requirements.txt").readlines()] requirements = [ - "numpy>=1.22,<1.26", + "numpy", "qiskit>=1.0", - "qiskit_ibm_runtime==0.37.0", + "qiskit_ibm_runtime", "qiskit-aer", "graphix", ] @@ -37,11 +37,12 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Topic :: Scientific/Engineering :: Physics", ], - "python_requires": ">=3.8,<3.12", + "python_requires": ">=3.8,<3.13", "install_requires": requirements, "extras_require": {"test": ["graphix"]}, } From 9cade0e2a8e80c2f4925a76fc9a9eec384f93c41 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Tue, 8 Jul 2025 18:29:15 +0900 Subject: [PATCH 19/30] edit setup and ci --- requirements.txt | 1 + setup.py | 9 +-------- tox.ini | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index e08d3a6..8920504 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ numpy qiskit>=1.0 qiskit_ibm_runtime qiskit-aer +graphix diff --git a/setup.py b/setup.py index a756615..8344a5a 100644 --- a/setup.py +++ b/setup.py @@ -7,14 +7,7 @@ with open("graphix_ibmq/version.py") as fp: exec(fp.read(), version) -# requirements = [requirement.strip() for requirement in open("requirements.txt").readlines()] -requirements = [ - "numpy", - "qiskit>=1.0", - "qiskit_ibm_runtime", - "qiskit-aer", - "graphix", -] +requirements = [requirement.strip() for requirement in open("requirements.txt").readlines()] info = { "name": "graphix_ibmq", diff --git a/tox.ini b/tox.ini index 98fae00..adc0d41 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,17 @@ [tox] -envlist = py39, py310, py311, lint +envlist = py39, py310, py311, py312, lint [gh-actions] python = 3.9: lint, py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] description = Run the unit tests deps = -r {toxinidir}/requirements.txt - git+https://github.com/TeamGraphix/graphix@device_interface_abc#egg=graphix commands = pip install --upgrade pip pip install pytest From 5396ce41eb2af65022d2a94c4943d65204d41609 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Tue, 8 Jul 2025 18:39:38 +0900 Subject: [PATCH 20/30] remove python version specification in ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30cec2f..8bb1eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-2022', 'macos-latest'] - python: ['3.9', '3.10', '3.11', '3.12'] name: "Python ${{ matrix.python }} / ${{ matrix.os }}" runs-on: ${{ matrix.os }} From fcbfe34dcfef936be521f122e0246ac21bc20e4a Mon Sep 17 00:00:00 2001 From: d1ssk Date: Wed, 9 Jul 2025 10:44:29 +0900 Subject: [PATCH 21/30] remove python setup in ci.yml --- .github/workflows/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bb1eb9..aeb9371 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,11 +33,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python }} - - name: Install tox run: pip install tox tox-gh-actions From f4353bdb06b1f911c7557b712b4169d89f38a18c Mon Sep 17 00:00:00 2001 From: d1ssk Date: Wed, 9 Jul 2025 10:51:24 +0900 Subject: [PATCH 22/30] recover ci.yml --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aeb9371..30cec2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-2022', 'macos-latest'] + python: ['3.9', '3.10', '3.11', '3.12'] name: "Python ${{ matrix.python }} / ${{ matrix.os }}" runs-on: ${{ matrix.os }} @@ -33,6 +34,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install tox run: pip install tox tox-gh-actions From d2b60355573c6f0da2a94d2eeadc629819c52814 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 14 Jul 2025 15:46:26 +0900 Subject: [PATCH 23/30] refactor --- examples/gallery/aer_sim.py | 7 +- examples/ibm_device.py | 7 +- graphix_ibmq/backend.py | 179 +++++++++------------- graphix_ibmq/compiler.py | 295 +++++++++++++++++++++--------------- graphix_ibmq/job.py | 74 ++++----- tests/random_circuit.py | 42 ----- tests/test_backend.py | 74 +++++---- tests/test_compiler.py | 10 +- 8 files changed, 333 insertions(+), 355 deletions(-) delete mode 100644 tests/random_circuit.py diff --git a/examples/gallery/aer_sim.py b/examples/gallery/aer_sim.py index fca179a..4edd4d2 100644 --- a/examples/gallery/aer_sim.py +++ b/examples/gallery/aer_sim.py @@ -84,15 +84,14 @@ def swap(circuit, a, b): # convert to qiskit circuit backend = IBMQBackend() -backend.compile(pattern) -print(type(backend._compiled_circuit)) +compiled = backend.compile(pattern) #%% # We can now simulate the circuit with Aer. # run and get counts backend.set_simulator() -job = backend.submit_job(shots=1024) +job = backend.submit_job(compiled, shots=1024) result = job.retrieve_result() #%% @@ -114,7 +113,7 @@ def swap(circuit, a, b): # Now we can run the simulation with noise model # run and get counts -job = backend.submit_job(shots=1024) +job = backend.submit_job(compiled, shots=1024) result_noise = job.retrieve_result() diff --git a/examples/ibm_device.py b/examples/ibm_device.py index 17ffb1e..ed06b89 100644 --- a/examples/ibm_device.py +++ b/examples/ibm_device.py @@ -84,8 +84,7 @@ def swap(circuit, a, b): # convert to qiskit circuit backend = IBMQBackend() -backend.compile(pattern) -print(type(backend._compiled_circuit)) +compiled = backend.compile(pattern) #%% # load the account with API token @@ -98,7 +97,7 @@ def swap(circuit, a, b): #%% # We can now execute the circuit on the device backend. -job = backend.submit_job(shots=1024) +job = backend.submit_job(compiled, shots=1024) #%% # Retrieve the job result @@ -114,7 +113,7 @@ def swap(circuit, a, b): backend.set_simulator(based_on=FakeManilaV2()) # execute noisy simulation and get counts -job = backend.submit_job(shots=1024) +job = backend.submit_job(compiled, shots=1024) result_noise = job.retrieve_result() #%% diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index 16e488b..e257f08 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -1,159 +1,126 @@ from __future__ import annotations +from typing import TYPE_CHECKING -from graphix_ibmq.compiler import IBMQPatternCompiler -from graphix_ibmq.job import IBMQJob -from graphix_ibmq.compile_options import IBMQCompileOptions - -from qiskit_aer.noise import NoiseModel from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import SamplerV2 as Sampler +from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService -from typing import TYPE_CHECKING +from graphix_ibmq.compile_options import IBMQCompileOptions +from graphix_ibmq.compiler import IBMQPatternCompiler, IBMQCompiledCircuit +from graphix_ibmq.job import IBMQJob if TYPE_CHECKING: from graphix.pattern import Pattern - from qiskit import QuantumCircuit - from qiskit.providers.backend import BackendV2 + from qiskit.providers.backend import BackendV2, Backend class IBMQBackend: - """IBMQ backend implementation for compiling and executing quantum patterns.""" + """ + Manages compilation and execution on IBMQ simulators or hardware. - _options: IBMQCompileOptions - _compiled_circuit: QuantumCircuit | None - _execution_mode: str | None - _noise_model: NoiseModel | None - _simulator: AerSimulator | None - _resource: BackendV2 | None - _compiler: IBMQPatternCompiler | None + This class configures the execution target and provides methods to compile + a graphix Pattern and submit it as a job. + """ def __init__(self) -> None: - self._compiler = None self._options = IBMQCompileOptions() - self._compiled_circuit = None - self._execution_mode = None - self._noise_model = None - self._simulator = None - self._resource = None + # The target backend, either a simulator or a real hardware device. + self._backend: Backend | None = None - def compile(self, pattern: Pattern, options: IBMQCompileOptions | None = None) -> None: - """Compile the assigned pattern using IBMQ options. + def compile(self, pattern: Pattern, options: IBMQCompileOptions | None = None) -> IBMQCompiledCircuit: + """ + Compiles the given pattern into a Qiskit QuantumCircuit. Parameters ---------- - options : CompileOptions, optional - Compilation options. Must be of type IBMQCompileOptions. + pattern : Pattern + The graphix pattern to compile. + options : IBMQCompileOptions, optional + Compilation options. If not provided, default options are used. + + Returns + ------- + IBMQCompiledCircuit + An object containing the compiled circuit and related metadata. """ if options is None: self._options = IBMQCompileOptions() elif not isinstance(options, IBMQCompileOptions): - raise TypeError("Expected IBMQCompileOptions") + raise TypeError("options must be an instance of IBMQCompileOptions") else: self._options = options - self._compiler = IBMQPatternCompiler(pattern) - self._compiled_circuit = self._compiler.to_qiskit_circuit( - save_statevector=self._options.save_statevector, - layout_method=self._options.layout_method, - ) + compiler = IBMQPatternCompiler(pattern) + return compiler.compile(save_statevector=self._options.save_statevector) - def set_simulator( - self, - noise_model: NoiseModel | None = None, - based_on: BackendV2 | None = None, - ) -> None: - """Configure the backend to use a simulator. + def set_simulator(self, noise_model: NoiseModel | None = None, from_backend: BackendV2 | None = None) -> None: + """ + Configures the backend to use a local Aer simulator. Parameters ---------- noise_model : NoiseModel, optional - Noise model to apply to the simulator. - based_on : BackendV2, optional - Backend to base the noise model on. + A custom noise model for the simulation. + from_backend : BackendV2, optional + A hardware backend to base the noise model on. Ignored if `noise_model` is provided. """ - if noise_model is None and based_on is not None: - noise_model = NoiseModel.from_backend(based_on) + if noise_model is None and from_backend is not None: + noise_model = NoiseModel.from_backend(from_backend) - self._execution_mode = "simulation" - self._noise_model = noise_model - self._simulator = AerSimulator(noise_model=noise_model) + self._backend = AerSimulator(noise_model=noise_model) + print("Backend set to local AerSimulator.") - def select_device( - self, - name: str | None = None, - least_busy: bool = False, - min_qubits: int = 1, - ) -> None: - """Select a hardware backend from IBMQ. + def set_hardware(self, name: str | None = None, least_busy: bool = False, min_qubits: int = 1) -> None: + """ + Selects a real hardware backend from IBM Quantum. Parameters ---------- name : str, optional - Specific device name to use. - least_busy : bool, optional - If True, select the least busy device that meets requirements. - min_qubits : int, optional - Minimum number of qubits required. + The specific name of the device (e.g., 'ibm_brisbane'). + least_busy : bool + If True, selects the least busy device meeting the criteria. + min_qubits : int + The minimum number of qubits required. """ - from qiskit_ibm_runtime import QiskitRuntimeService - - self._execution_mode = "hardware" service = QiskitRuntimeService() - if least_busy or name is None: - self._resource = service.least_busy(min_num_qubits=min_qubits, operational=True) + if name: + backend = service.backend(name) else: - self._resource = service.backend(name) + backend = service.least_busy(min_num_qubits=min_qubits, operational=True) - print(f"Selected device: {self._resource.name}") + self._backend = backend + # Note: In a production library, consider using the `logging` module instead of `print`. + print(f"Selected hardware backend: {self._backend.name}") - def submit_job(self, shots: int = 1024) -> IBMQJob: - """Submit the compiled circuit to either simulator or hardware backend. + def submit_job(self, compiled_circuit: IBMQCompiledCircuit, shots: int = 1024) -> IBMQJob: + """ + Submits the compiled circuit to the configured backend for execution. Parameters ---------- + compiled_circuit : IBMQCompiledCircuit + The compiled circuit object from the `compile` method. shots : int, optional - Number of execution shots. Defaults to 1024. + The number of execution shots. Defaults to 1024. Returns ------- - Job - A handle to monitor the job status and retrieve results. - - Raises - ------ - RuntimeError - If the pattern has not been compiled or execution mode is not set. + IBMQJob + A job object to monitor execution and retrieve results. """ - if self._compiled_circuit is None: - raise RuntimeError("Pattern must be compiled before submission.") - - if self._execution_mode is None: - raise RuntimeError("Execution mode is not configured. Use select_backend() or set_simulator().") - - if not hasattr(self, "_options") or self._options is None: - raise RuntimeError("Compile options are not set.") - - if self._execution_mode == "simulation": - pm = generate_preset_pass_manager( - backend=self._simulator, - optimization_level=self._options.optimization_level, - ) - transpiled = pm.run(self._compiled_circuit) - sampler = Sampler(mode=self._simulator) - job = sampler.run([transpiled], shots=shots) - return IBMQJob(job, self._compiler) - - elif self._execution_mode == "hardware": - pm = generate_preset_pass_manager( - backend=self._resource, - optimization_level=self._options.optimization_level, - ) - transpiled = pm.run(self._compiled_circuit) - sampler = Sampler(mode=self._resource) - job = sampler.run([transpiled], shots=shots) - return IBMQJob(job, self._compiler) + if self._backend is None: + raise RuntimeError("Backend not set. Call 'set_simulator()' or 'set_hardware()' before submitting a job.") - else: - raise RuntimeError("Execution mode is not configured. Use select_backend() or set_simulator().") + pass_manager = generate_preset_pass_manager( + backend=self._backend, + optimization_level=self._options.optimization_level, + ) + transpiled_circuit = pass_manager.run(compiled_circuit.circuit) + + sampler = Sampler(mode=self._backend) + job = sampler.run([transpiled_circuit], shots=shots) + + return IBMQJob(job, compiled_circuit) diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index 1b3afe1..5f5ca05 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -1,4 +1,5 @@ from __future__ import annotations +from dataclasses import dataclass import numpy as np from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit @@ -13,21 +14,9 @@ class IBMQPatternCompiler: - """Compiler that translates a Graphix Pattern into a Qiskit QuantumCircuit. - - Attributes - ---------- - _pattern : Pattern - The measurement-based quantum computation pattern to compile. - _register_dict : dict[int, int] - Mapping from pattern node indices to classical register indices. - _circ_output : list[int] - List of output qubit indices in the compiled circuit. - """ - def __init__(self, pattern: Pattern) -> None: """ - Initialize the compiler with a given pattern. + Initializes the compiler with a given pattern. Parameters ---------- @@ -35,128 +24,182 @@ def __init__(self, pattern: Pattern) -> None: The measurement-based quantum computation pattern. """ self._pattern = pattern - self._register_dict: dict[int, int] = {} - self._circ_output: list[int] = [] + self._circuit: QuantumCircuit | None = None + self._classical_register: ClassicalRegister | None = None + + # Mappings from pattern node index to circuit/register indices + self._qubit_map: dict[int, int] = {} + self._creg_map: dict[int, int] = {} + + self._available_qubits: list[int] = [] + self._next_creg_idx: int = 0 - def to_qiskit_circuit(self, save_statevector: bool, layout_method: str) -> QuantumCircuit: + def compile(self, save_statevector: bool = False) -> IBMQCompiledCircuit: """ - Convert the MBQC pattern into a Qiskit QuantumCircuit. + Converts the MBQC pattern into a Qiskit QuantumCircuit. Parameters ---------- save_statevector : bool - Whether to save the statevector before output measurement (for testing). - layout_method : str - (Currently unused) Layout method for mapping. + If True, saves the statevector before output measurement. Returns ------- - QuantumCircuit - The compiled Qiskit circuit. + IBMQCompiledCircuit + A data class containing the compiled circuit and associated metadata. """ - n = self._pattern.max_space() - n_node = self._pattern.n_node - - qr = QuantumRegister(n) - cr = ClassicalRegister(n_node, name="meas") - circ = QuantumCircuit(qr, cr) - - empty_qubit = list(range(n)) # available qubit indices - qubit_dict: dict[int, int] = {} # pattern node -> circuit qubit - register_dict: dict[int, int] = {} # pattern node -> classical register - reg_idx = 0 - - def signal_process(op: str, circ_idx: int, signal: set[int]) -> None: - """Apply classically-controlled X or Z gates based on measurement outcomes.""" - if op == "X": - for s in signal: - if s in register_dict: - s_idx = register_dict[s] - with circ.if_test((cr[s_idx], 1)): - circ.x(circ_idx) - else: - if self._pattern.results[s] == 1: - circ.x(circ_idx) - if op == "Z": - for s in signal: - if s in register_dict: - s_idx = register_dict[s] - with circ.if_test((cr[s_idx], 1)): - circ.z(circ_idx) - else: - if self._pattern.results[s] == 1: - circ.z(circ_idx) - - # Prepare input qubits - for i in self._pattern.input_nodes: - circ_idx = empty_qubit.pop(0) - circ.reset(circ_idx) - circ.h(circ_idx) - qubit_dict[i] = circ_idx - - # Compile pattern commands + self._initialize_circuit() + self._process_commands() + output_qubits = self._finalize_circuit(save_statevector) + + return IBMQCompiledCircuit( + circuit=self._circuit, + pattern=self._pattern, + register_dict=self._creg_map, + circ_output=output_qubits, + ) + + def _initialize_circuit(self) -> None: + """Initializes the quantum circuit, registers, and state variables.""" + num_qubits = self._pattern.max_space() + num_nodes = self._pattern.n_node + + qr = QuantumRegister(num_qubits) + self._classical_register = ClassicalRegister(num_nodes, name="meas") + self._circuit = QuantumCircuit(qr, self._classical_register) + + self._available_qubits = list(range(num_qubits)) + self._qubit_map = {} + self._creg_map = {} + self._next_creg_idx = 0 + + # Prepare input qubits by applying a Hadamard gate. + for node_idx in self._pattern.input_nodes: + circ_idx = self._allocate_qubit(node_idx) + self._circuit.h(circ_idx) + + def _process_commands(self) -> None: + """Iterates through and processes all commands in the pattern.""" + command_handlers = { + CommandKind.N: self._apply_n, + CommandKind.E: self._apply_e, + CommandKind.M: self._apply_m, + CommandKind.X: self._apply_x, + CommandKind.Z: self._apply_z, + CommandKind.C: self._apply_c, + } for cmd in self._pattern: - if cmd.kind == CommandKind.N: - circ_idx = empty_qubit.pop(0) - circ.reset(circ_idx) - circ.h(circ_idx) - qubit_dict[cmd.node] = circ_idx - - elif cmd.kind == CommandKind.E: - circ.cz(qubit_dict[cmd.nodes[0]], qubit_dict[cmd.nodes[1]]) - - elif cmd.kind == CommandKind.M: - circ_idx = qubit_dict[cmd.node] - plane = cmd.plane - alpha = cmd.angle * np.pi - s_list = cmd.s_domain - t_list = cmd.t_domain - - if plane == Plane.XY: - if alpha != 0: - signal_process("X", circ_idx, s_list) - circ.p(-alpha, circ_idx) - signal_process("Z", circ_idx, t_list) - circ.h(circ_idx) - circ.measure(circ_idx, reg_idx) - register_dict[cmd.node] = reg_idx - reg_idx += 1 - empty_qubit.append(circ_idx) - else: - raise NotImplementedError("Non-XY plane is not supported.") - - elif cmd.kind == CommandKind.X: - circ_idx = qubit_dict[cmd.node] - s_list = cmd.domain - signal_process("X", circ_idx, s_list) - - elif cmd.kind == CommandKind.Z: - circ_idx = qubit_dict[cmd.node] - s_list = cmd.domain - signal_process("Z", circ_idx, s_list) - - elif cmd.kind == CommandKind.C: - circ_idx = qubit_dict[cmd.node] - for method_name in cmd.qasm3: - getattr(circ, method_name)(circ_idx) - - # Handle output measurements + handler = command_handlers.get(cmd.kind) + if handler: + handler(cmd) + + def _allocate_qubit(self, node_idx: int) -> int: + """Allocates a qubit from the pool, resets it, and maps it to a node.""" + if not self._available_qubits: + raise RuntimeError("No available qubits to allocate.") + circ_idx = self._available_qubits.pop(0) + self._circuit.reset(circ_idx) + self._qubit_map[node_idx] = circ_idx + return circ_idx + + def _release_qubit(self, circ_idx: int) -> None: + """Releases a qubit, making it available for reuse.""" + self._available_qubits.append(circ_idx) + + def _apply_n(self, cmd) -> None: + """Handles the N command: create a new qubit in the |+> state.""" + circ_idx = self._allocate_qubit(cmd.node) + self._circuit.h(circ_idx) + + def _apply_e(self, cmd) -> None: + """Handles the E command: apply a CZ gate between two qubits.""" + qubit1 = self._qubit_map[cmd.nodes[0]] + qubit2 = self._qubit_map[cmd.nodes[1]] + self._circuit.cz(qubit1, qubit2) + + def _apply_m(self, cmd) -> None: + """Handles the M command: perform a measurement.""" + if cmd.plane != Plane.XY: + raise NotImplementedError("Non-XY plane measurements are not supported.") + + circ_idx = self._qubit_map[cmd.node] + + self._apply_classical_feedforward("X", circ_idx, cmd.s_domain) + self._apply_classical_feedforward("Z", circ_idx, cmd.t_domain) + + if cmd.angle != 0: + self._circuit.p(-cmd.angle * np.pi, circ_idx) + + self._circuit.h(circ_idx) + self._circuit.measure(circ_idx, self._next_creg_idx) + + self._creg_map[cmd.node] = self._next_creg_idx + self._next_creg_idx += 1 + self._release_qubit(circ_idx) + + def _apply_x(self, cmd) -> None: + """Handles the X command: apply a Pauli X correction.""" + circ_idx = self._qubit_map[cmd.node] + self._apply_classical_feedforward("X", circ_idx, cmd.domain) + + def _apply_z(self, cmd) -> None: + """Handles the Z command: apply a Pauli Z correction.""" + circ_idx = self._qubit_map[cmd.node] + self._apply_classical_feedforward("Z", circ_idx, cmd.domain) + + def _apply_c(self, cmd) -> None: + """Handles the C command: apply a custom Qiskit circuit method.""" + circ_idx = self._qubit_map[cmd.node] + for method_name in cmd.qasm3: + getattr(self._circuit, method_name)(circ_idx) + + def _apply_classical_feedforward(self, op: str, target_qubit: int, domain: set[int]) -> None: + """Applies classically-controlled X or Z gates based on measurement outcomes.""" + gate_map = {"X": self._circuit.x, "Z": self._circuit.z} + if op not in gate_map: + return + + apply_gate = gate_map[op] + + for node_idx in domain: + if node_idx in self._creg_map: + creg_idx = self._creg_map[node_idx] + with self._circuit.if_test((self._classical_register[creg_idx], 1)): + apply_gate(target_qubit) + elif self._pattern.results.get(node_idx) == 1: + apply_gate(target_qubit) + + def _finalize_circuit(self, save_statevector: bool) -> list[int]: + """Handles output measurements and optional statevector saving.""" + output_qubits = [self._qubit_map[node] for node in self._pattern.output_nodes] + if save_statevector: - circ.save_statevector() - output_qubit: list[int] = [] - for node in self._pattern.output_nodes: - circ_idx = qubit_dict[node] - circ.measure(circ_idx, reg_idx) - register_dict[node] = reg_idx - reg_idx += 1 - output_qubit.append(circ_idx) - self._circ_output = output_qubit - else: - for node in self._pattern.output_nodes: - circ_idx = qubit_dict[node] - circ.measure(circ_idx, reg_idx) - register_dict[node] = reg_idx - reg_idx += 1 - - self._register_dict = register_dict - return circ + self._circuit.save_statevector() + + for node in self._pattern.output_nodes: + circ_idx = self._qubit_map[node] + self._circuit.measure(circ_idx, self._next_creg_idx) + self._creg_map[node] = self._next_creg_idx + self._next_creg_idx += 1 + + return output_qubits if save_statevector else [] + + +@dataclass +class IBMQCompiledCircuit: + """A compiled circuit with its associated pattern and register mapping. + + Attributes + ---------- + circuit : QuantumCircuit + The Qiskit quantum circuit generated from the pattern. + register_dict : dict[int, int] + Mapping from pattern node indices to classical register indices. + circ_output : list[int] + List of output qubit indices in the compiled circuit. + """ + + circuit: QuantumCircuit + pattern: Pattern + register_dict: dict[int, int] + circ_output: list[int] diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index 05dc5bf..47c6871 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -1,36 +1,32 @@ from __future__ import annotations +from dataclasses import dataclass +from typing import TYPE_CHECKING +from qiskit.providers.jobstatus import JobStatus from graphix_ibmq.result_utils import format_result -from typing import TYPE_CHECKING - if TYPE_CHECKING: from qiskit_ibm_runtime.fake_provider.local_runtime_job import LocalRuntimeJob from qiskit_ibm_runtime.runtime_job_v2 import RuntimeJobV2 - from graphix_ibmq.compiler import IBMQPatternCompiler + from graphix_ibmq.compiler import IBMQCompiledCircuit +@dataclass class IBMQJob: - """Job handler class for IBMQ devices and simulators.""" + """ + A handler for jobs submitted to IBMQ devices and simulators. - def __init__( - self, - job: LocalRuntimeJob | RuntimeJobV2, - compiler: IBMQPatternCompiler, - ) -> None: - """ - Parameters - ---------- - job : LocalRuntimeJob | RuntimeJobV2 - The job object returned from Qiskit Runtime or real v2 runtime. - """ - self.job = job - self._compiler = compiler + This class wraps a Qiskit job object and the corresponding compiled circuit, + providing methods to check the job's status and retrieve formatted results. + """ + + job: LocalRuntimeJob | RuntimeJobV2 + compiled_circuit: IBMQCompiledCircuit @property - def get_id(self) -> str: + def id(self) -> str: """ - Get the unique identifier of the job. + Returns the unique identifier of the job. Returns ------- @@ -42,46 +38,42 @@ def get_id(self) -> str: @property def is_done(self) -> bool: """ - Check whether the job is completed. + Checks if the job has completed execution. Returns ------- bool True if the job is done, False otherwise. """ - status = self.job.status() - if isinstance(status, str): - return status.upper() == "DONE" - return status.name == "DONE" + return self.job.status() == JobStatus.DONE + + def retrieve_result(self, raw_result: bool = False) -> dict | None: + """ + Retrieves the result from a completed job. - def retrieve_result(self, raw_result: bool = False): - """Retrieve the result from a completed job. + If the job is not yet complete, this method returns None. Parameters ---------- - job : Job - Handle to the executed job. raw_result : bool, optional - If True, return raw counts; otherwise, return formatted results. + If True, returns the raw measurement counts dictionary. + If False (default), returns results formatted by the graphix pattern. Returns ------- - dict or Any - Formatted result or raw counts from the job. - - Raises - ------ - TypeError - If the job handle is not an instance of IBMQJob. + dict or None + A dictionary containing the formatted results or raw counts. + Returns None if the job has not yet finished. """ - if not self.is_done: - # job not completed yet; skip retrieval return None + # Result from SamplerV2 contains a list of pub_results. + # We assume a single circuit was run, so we take the first element [0]. result = self.job.result() counts = result[0].data.meas.get_counts() - if not raw_result: - return format_result(counts, self._compiler._pattern, self._compiler._register_dict) - return counts + if raw_result: + return counts + + return format_result(counts, self.compiled_circuit.pattern, self.compiled_circuit.register_dict) diff --git a/tests/random_circuit.py b/tests/random_circuit.py deleted file mode 100644 index e3c27c2..0000000 --- a/tests/random_circuit.py +++ /dev/null @@ -1,42 +0,0 @@ -import numpy as np -from graphix.transpiler import Circuit - - -def first_rotation(circuit, nqubits): - for qubit in range(nqubits): - circuit.rx(qubit, np.random.rand()) - - -def mid_rotation(circuit, nqubits): - for qubit in range(nqubits): - circuit.rx(qubit, np.random.rand()) - circuit.rz(qubit, np.random.rand()) - - -def last_rotation(circuit, nqubits): - for qubit in range(nqubits): - circuit.rz(qubit, np.random.rand()) - - -def entangler(circuit, pairs): - for a, b in pairs: - circuit.cnot(a, b) - - -def entangler_rzz(circuit, pairs): - for a, b in pairs: - circuit.rzz(a, b, np.random.rand()) - - -def generate_gate(nqubits, depth, pairs, use_rzz=False): - circuit = Circuit(nqubits) - first_rotation(circuit, nqubits) - entangler(circuit, pairs) - for k in range(depth - 1): - mid_rotation(circuit, nqubits) - if use_rzz: - entangler_rzz(circuit, pairs) - else: - entangler(circuit, pairs) - last_rotation(circuit, nqubits) - return circuit diff --git a/tests/test_backend.py b/tests/test_backend.py index ffec71f..fa667bb 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,56 +1,76 @@ import pytest +from unittest.mock import MagicMock + from graphix_ibmq.backend import IBMQBackend from graphix_ibmq.compile_options import IBMQCompileOptions + +from graphix_ibmq.compiler import IBMQCompiledCircuit from qiskit import QuantumCircuit from qiskit_aer import AerSimulator - -class DummyCompiler: +class MockCompiler: def __init__(self, pattern): self._pattern = pattern - def to_qiskit_circuit(self, save_statevector, layout_method): - return QuantumCircuit(1) + def compile(self, save_statevector: bool): + return IBMQCompiledCircuit( + circuit=QuantumCircuit(1), + pattern=self._pattern, + register_dict={}, + circ_output=[], + ) - -def test_compile_default(monkeypatch): - monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) +def test_compile_with_default_options(monkeypatch): + monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", MockCompiler) backend = IBMQBackend() dummy_pattern = object() - backend.compile(dummy_pattern) + compiled_circuit = backend.compile(dummy_pattern) - assert backend._compiler._pattern is dummy_pattern - assert isinstance(backend._compiled_circuit, QuantumCircuit) + assert isinstance(compiled_circuit, IBMQCompiledCircuit) + assert compiled_circuit.pattern is dummy_pattern assert isinstance(backend._options, IBMQCompileOptions) -def test_compile_invalid_options(monkeypatch): - monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) +def test_compile_with_invalid_options(): backend = IBMQBackend() - with pytest.raises(TypeError): - backend.compile(object(), options="not-an-options") + with pytest.raises(TypeError, match="options must be an instance of IBMQCompileOptions"): + backend.compile(object(), options="not-a-valid-option") def test_set_simulator(): backend = IBMQBackend() backend.set_simulator() - assert backend._execution_mode == "simulation" - assert isinstance(backend._simulator, AerSimulator) - assert backend._noise_model is None + + assert isinstance(backend._backend, AerSimulator) + assert backend._backend.options.noise_model is None -def test_submit_job_not_compiled(): +def test_set_hardware(monkeypatch): backend = IBMQBackend() - with pytest.raises(RuntimeError) as e: - backend.submit_job() - assert "Pattern must be compiled" in str(e.value) + + mock_service_instance = MagicMock() + mock_backend_obj = MagicMock() + mock_backend_obj.name = "mock_hardware_backend" + mock_service_instance.backend.return_value = mock_backend_obj + + monkeypatch.setattr( + "graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance + ) + + backend.set_hardware(name="mock_hardware_backend") + + assert backend._backend is mock_backend_obj + mock_service_instance.backend.assert_called_once_with("mock_hardware_backend") -def test_submit_job_without_execution_mode(monkeypatch): - monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", DummyCompiler) +def test_submit_job_without_backend_configured(): backend = IBMQBackend() - backend.compile(object()) - with pytest.raises(RuntimeError) as e: - backend.submit_job() - assert "Execution mode is not configured" in str(e.value) + dummy_compiled_circuit = IBMQCompiledCircuit( + circuit=QuantumCircuit(1), pattern=object(), register_dict={}, circ_output=[] + ) + + with pytest.raises(RuntimeError) as exc_info: + backend.submit_job(dummy_compiled_circuit) + + assert "Backend not set" in str(exc_info.value) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index f60580e..8ea6245 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -3,7 +3,7 @@ from qiskit import transpile from qiskit_aer import AerSimulator from graphix_ibmq.compiler import IBMQPatternCompiler -import random_circuit as rc +import graphix.random_objects as rc def reduce_statevector_to_outputs(statevector: np.ndarray, output_qubits: list) -> np.ndarray: @@ -30,18 +30,18 @@ def test_ibmq_compiler_statevector_equivalence(nqubits, depth): sim = AerSimulator() for _ in range(5): # repeat with different random circuits - pairs = [(i, (i + 1) % nqubits) for i in range(nqubits)] - circuit = rc.generate_gate(nqubits, depth, pairs) + circuit = rc.rand_circuit(nqubits, depth) pattern = circuit.transpile().pattern mbqc_state = pattern.simulate_pattern() compiler = IBMQPatternCompiler(pattern) - qc = compiler.to_qiskit_circuit(save_statevector=True, layout_method=None) + compiled = compiler.compile(save_statevector=True) + qc = compiled.circuit transpiled = transpile(qc, sim) result = sim.run(transpiled).result() qiskit_state = np.array(result.get_statevector(qc)) - qiskit_reduced = reduce_statevector_to_outputs(qiskit_state, compiler._circ_output) + qiskit_reduced = reduce_statevector_to_outputs(qiskit_state, compiled.circ_output) fidelity = np.abs(np.dot(qiskit_reduced.conjugate(), mbqc_state.flatten())) assert np.isclose(fidelity, 1.0, atol=1e-6), f"Fidelity mismatch: {fidelity}" From b655ce01f28016c87ba80c51f0529748ff077925 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Mon, 14 Jul 2025 15:49:28 +0900 Subject: [PATCH 24/30] black --- tests/test_backend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index fa667bb..9b5ecc4 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -8,6 +8,7 @@ from qiskit import QuantumCircuit from qiskit_aer import AerSimulator + class MockCompiler: def __init__(self, pattern): self._pattern = pattern @@ -20,6 +21,7 @@ def compile(self, save_statevector: bool): circ_output=[], ) + def test_compile_with_default_options(monkeypatch): monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", MockCompiler) backend = IBMQBackend() @@ -54,9 +56,7 @@ def test_set_hardware(monkeypatch): mock_backend_obj.name = "mock_hardware_backend" mock_service_instance.backend.return_value = mock_backend_obj - monkeypatch.setattr( - "graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance - ) + monkeypatch.setattr("graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance) backend.set_hardware(name="mock_hardware_backend") @@ -69,8 +69,8 @@ def test_submit_job_without_backend_configured(): dummy_compiled_circuit = IBMQCompiledCircuit( circuit=QuantumCircuit(1), pattern=object(), register_dict={}, circ_output=[] ) - + with pytest.raises(RuntimeError) as exc_info: backend.submit_job(dummy_compiled_circuit) - + assert "Backend not set" in str(exc_info.value) From aa713a2f4fd7ed19c4a19fc7ae5bf86683475e2d Mon Sep 17 00:00:00 2001 From: d1ssk Date: Tue, 29 Jul 2025 01:06:21 +0900 Subject: [PATCH 25/30] Redesign IBMQBackend for improved type safety --- examples/gallery/aer_sim.py | 5 +- examples/ibm_device.py | 9 +-- graphix_ibmq/backend.py | 121 ++++++++++++++++++++--------------- graphix_ibmq/compiler.py | 63 ++++++++---------- graphix_ibmq/job.py | 2 +- graphix_ibmq/result_utils.py | 4 +- tests/test_backend.py | 120 ++++++++++++++++++++++------------ 7 files changed, 185 insertions(+), 139 deletions(-) diff --git a/examples/gallery/aer_sim.py b/examples/gallery/aer_sim.py index 4edd4d2..2e01b70 100644 --- a/examples/gallery/aer_sim.py +++ b/examples/gallery/aer_sim.py @@ -83,14 +83,13 @@ def swap(circuit, a, b): pattern.minimize_space() # convert to qiskit circuit -backend = IBMQBackend() +backend = IBMQBackend.from_simulator() compiled = backend.compile(pattern) #%% # We can now simulate the circuit with Aer. # run and get counts -backend.set_simulator() job = backend.submit_job(compiled, shots=1024) result = job.retrieve_result() @@ -104,7 +103,7 @@ def swap(circuit, a, b): noise_model = NoiseModel() error = depolarizing_error(0.01, 1) noise_model.add_all_qubit_quantum_error(error, ["id", "rz", "sx", "x", "u1"]) -backend.set_simulator(noise_model=noise_model) +backend = IBMQBackend.from_simulator(noise_model=noise_model) # print noise model info print(noise_model) diff --git a/examples/ibm_device.py b/examples/ibm_device.py index ed06b89..32f6330 100644 --- a/examples/ibm_device.py +++ b/examples/ibm_device.py @@ -83,7 +83,7 @@ def swap(circuit, a, b): pattern.minimize_space() # convert to qiskit circuit -backend = IBMQBackend() +backend = IBMQBackend.from_simulator() compiled = backend.compile(pattern) #%% @@ -92,11 +92,11 @@ def swap(circuit, a, b): QiskitRuntimeService.save_account(channel="ibm_quantum", token="API TOKEN", overwrite=True) # get the device backend -backend.select_device() +backend = IBMQBackend.from_hardware() #%% # We can now execute the circuit on the device backend. - +compiled = backend.compile(pattern) job = backend.submit_job(compiled, shots=1024) #%% @@ -110,9 +110,10 @@ def swap(circuit, a, b): # get the noise model of the device backend from qiskit_ibm_runtime.fake_provider import FakeManilaV2 -backend.set_simulator(based_on=FakeManilaV2()) +backend = IBMQBackend.from_simulator(from_backend=FakeManilaV2()) # execute noisy simulation and get counts +compiled = backend.compile(pattern) job = backend.submit_job(compiled, shots=1024) result_noise = job.retrieve_result() diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index e257f08..7a9a044 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -1,5 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING +import logging from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel @@ -14,66 +15,65 @@ from graphix.pattern import Pattern from qiskit.providers.backend import BackendV2, Backend +logger = logging.getLogger(__name__) + class IBMQBackend: """ Manages compilation and execution on IBMQ simulators or hardware. This class configures the execution target and provides methods to compile - a graphix Pattern and submit it as a job. + a graphix Pattern and submit it as a job. Instances should be created using + the `from_simulator` or `from_hardware` classmethods. """ - def __init__(self) -> None: - self._options = IBMQCompileOptions() - # The target backend, either a simulator or a real hardware device. - self._backend: Backend | None = None - - def compile(self, pattern: Pattern, options: IBMQCompileOptions | None = None) -> IBMQCompiledCircuit: - """ - Compiles the given pattern into a Qiskit QuantumCircuit. - - Parameters - ---------- - pattern : Pattern - The graphix pattern to compile. - options : IBMQCompileOptions, optional - Compilation options. If not provided, default options are used. - - Returns - ------- - IBMQCompiledCircuit - An object containing the compiled circuit and related metadata. - """ - if options is None: - self._options = IBMQCompileOptions() - elif not isinstance(options, IBMQCompileOptions): - raise TypeError("options must be an instance of IBMQCompileOptions") - else: - self._options = options - - compiler = IBMQPatternCompiler(pattern) - return compiler.compile(save_statevector=self._options.save_statevector) - - def set_simulator(self, noise_model: NoiseModel | None = None, from_backend: BackendV2 | None = None) -> None: - """ - Configures the backend to use a local Aer simulator. + def __init__(self, backend: Backend | None = None, options: IBMQCompileOptions | None = None) -> None: + if backend is None or options is None: + raise TypeError( + "IBMQBackend cannot be instantiated directly. " + "Please use the classmethods `IBMQBackend.from_simulator()` " + "or `IBMQBackend.from_hardware()`." + ) + self._backend: Backend = backend + self._options: IBMQCompileOptions = options + + @classmethod + def from_simulator( + cls, + noise_model: NoiseModel | None = None, + from_backend: BackendV2 | None = None, + options: IBMQCompileOptions | None = None, + ) -> IBMQBackend: + """Creates an instance with a local Aer simulator as the backend. Parameters ---------- noise_model : NoiseModel, optional A custom noise model for the simulation. from_backend : BackendV2, optional - A hardware backend to base the noise model on. Ignored if `noise_model` is provided. + A hardware backend to base the noise model on. + Ignored if `noise_model` is provided. + options : IBMQCompileOptions, optional + Compilation and execution options. """ if noise_model is None and from_backend is not None: noise_model = NoiseModel.from_backend(from_backend) - self._backend = AerSimulator(noise_model=noise_model) - print("Backend set to local AerSimulator.") + aer_backend = AerSimulator(noise_model=noise_model) + compile_options = options if options is not None else IBMQCompileOptions() - def set_hardware(self, name: str | None = None, least_busy: bool = False, min_qubits: int = 1) -> None: - """ - Selects a real hardware backend from IBM Quantum. + logger.info("Backend set to local AerSimulator.") + return cls(backend=aer_backend, options=compile_options) + + @classmethod + def from_hardware( + cls, + name: str | None = None, + least_busy: bool = False, + min_qubits: int = 1, + options: IBMQCompileOptions | None = None, + ) -> IBMQBackend: + """Creates an instance with a real IBM Quantum hardware device as the backend. Parameters ---------- @@ -83,17 +83,41 @@ def set_hardware(self, name: str | None = None, least_busy: bool = False, min_qu If True, selects the least busy device meeting the criteria. min_qubits : int The minimum number of qubits required. + options : IBMQCompileOptions, optional + Compilation and execution options. """ service = QiskitRuntimeService() - if name: - backend = service.backend(name) + hw_backend = service.backend(name) else: - backend = service.least_busy(min_num_qubits=min_qubits, operational=True) + hw_backend = service.least_busy(min_num_qubits=min_qubits, operational=True) + + compile_options = options if options is not None else IBMQCompileOptions() + + logger.info("Selected hardware backend: %s", hw_backend.name) + return cls(backend=hw_backend, options=compile_options) + + @staticmethod + def compile(pattern: Pattern, save_statevector: bool = False) -> IBMQCompiledCircuit: + """Compiles a graphix pattern into a Qiskit QuantumCircuit. + + This method is provided as a staticmethod because it does not depend + on the backend's state. + + Parameters + ---------- + pattern : Pattern + The graphix pattern to compile. + save_statevector : bool + If True, saves the statevector before the final measurement. - self._backend = backend - # Note: In a production library, consider using the `logging` module instead of `print`. - print(f"Selected hardware backend: {self._backend.name}") + Returns + ------- + IBMQCompiledCircuit + An object containing the compiled circuit and related metadata. + """ + compiler = IBMQPatternCompiler(pattern) + return compiler.compile(save_statevector=save_statevector) def submit_job(self, compiled_circuit: IBMQCompiledCircuit, shots: int = 1024) -> IBMQJob: """ @@ -111,9 +135,6 @@ def submit_job(self, compiled_circuit: IBMQCompiledCircuit, shots: int = 1024) - IBMQJob A job object to monitor execution and retrieve results. """ - if self._backend is None: - raise RuntimeError("Backend not set. Call 'set_simulator()' or 'set_hardware()' before submitting a job.") - pass_manager = generate_preset_pass_manager( backend=self._backend, optimization_level=self._options.optimization_level, diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index 5f5ca05..4c3057b 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -4,10 +4,10 @@ import numpy as np from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from graphix.command import CommandKind +from graphix.command import CommandKind, N, M, E, X, Z, C from graphix.fundamentals import Plane -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Mapping, Sequence, Iterable if TYPE_CHECKING: from graphix.pattern import Pattern @@ -24,16 +24,23 @@ def __init__(self, pattern: Pattern) -> None: The measurement-based quantum computation pattern. """ self._pattern = pattern - self._circuit: QuantumCircuit | None = None - self._classical_register: ClassicalRegister | None = None - # Mappings from pattern node index to circuit/register indices + num_qubits = self._pattern.max_space() + num_nodes = self._pattern.n_node + + qr = QuantumRegister(num_qubits) + self._classical_register = ClassicalRegister(num_nodes, name="meas") + self._circuit = QuantumCircuit(qr, self._classical_register) + + self._available_qubits = list(range(num_qubits)) self._qubit_map: dict[int, int] = {} self._creg_map: dict[int, int] = {} - - self._available_qubits: list[int] = [] self._next_creg_idx: int = 0 + for node_idx in self._pattern.input_nodes: + circ_idx = self._allocate_qubit(node_idx) + self._circuit.h(circ_idx) + def compile(self, save_statevector: bool = False) -> IBMQCompiledCircuit: """ Converts the MBQC pattern into a Qiskit QuantumCircuit. @@ -48,7 +55,6 @@ def compile(self, save_statevector: bool = False) -> IBMQCompiledCircuit: IBMQCompiledCircuit A data class containing the compiled circuit and associated metadata. """ - self._initialize_circuit() self._process_commands() output_qubits = self._finalize_circuit(save_statevector) @@ -59,25 +65,6 @@ def compile(self, save_statevector: bool = False) -> IBMQCompiledCircuit: circ_output=output_qubits, ) - def _initialize_circuit(self) -> None: - """Initializes the quantum circuit, registers, and state variables.""" - num_qubits = self._pattern.max_space() - num_nodes = self._pattern.n_node - - qr = QuantumRegister(num_qubits) - self._classical_register = ClassicalRegister(num_nodes, name="meas") - self._circuit = QuantumCircuit(qr, self._classical_register) - - self._available_qubits = list(range(num_qubits)) - self._qubit_map = {} - self._creg_map = {} - self._next_creg_idx = 0 - - # Prepare input qubits by applying a Hadamard gate. - for node_idx in self._pattern.input_nodes: - circ_idx = self._allocate_qubit(node_idx) - self._circuit.h(circ_idx) - def _process_commands(self) -> None: """Iterates through and processes all commands in the pattern.""" command_handlers = { @@ -106,18 +93,18 @@ def _release_qubit(self, circ_idx: int) -> None: """Releases a qubit, making it available for reuse.""" self._available_qubits.append(circ_idx) - def _apply_n(self, cmd) -> None: + def _apply_n(self, cmd: N) -> None: """Handles the N command: create a new qubit in the |+> state.""" circ_idx = self._allocate_qubit(cmd.node) self._circuit.h(circ_idx) - def _apply_e(self, cmd) -> None: + def _apply_e(self, cmd: E) -> None: """Handles the E command: apply a CZ gate between two qubits.""" qubit1 = self._qubit_map[cmd.nodes[0]] qubit2 = self._qubit_map[cmd.nodes[1]] self._circuit.cz(qubit1, qubit2) - def _apply_m(self, cmd) -> None: + def _apply_m(self, cmd: M) -> None: """Handles the M command: perform a measurement.""" if cmd.plane != Plane.XY: raise NotImplementedError("Non-XY plane measurements are not supported.") @@ -137,23 +124,23 @@ def _apply_m(self, cmd) -> None: self._next_creg_idx += 1 self._release_qubit(circ_idx) - def _apply_x(self, cmd) -> None: + def _apply_x(self, cmd: X) -> None: """Handles the X command: apply a Pauli X correction.""" circ_idx = self._qubit_map[cmd.node] self._apply_classical_feedforward("X", circ_idx, cmd.domain) - def _apply_z(self, cmd) -> None: + def _apply_z(self, cmd: Z) -> None: """Handles the Z command: apply a Pauli Z correction.""" circ_idx = self._qubit_map[cmd.node] self._apply_classical_feedforward("Z", circ_idx, cmd.domain) - def _apply_c(self, cmd) -> None: + def _apply_c(self, cmd: C) -> None: """Handles the C command: apply a custom Qiskit circuit method.""" circ_idx = self._qubit_map[cmd.node] for method_name in cmd.qasm3: getattr(self._circuit, method_name)(circ_idx) - def _apply_classical_feedforward(self, op: str, target_qubit: int, domain: set[int]) -> None: + def _apply_classical_feedforward(self, op: str, target_qubit: int, domain: Iterable[int]) -> None: """Applies classically-controlled X or Z gates based on measurement outcomes.""" gate_map = {"X": self._circuit.x, "Z": self._circuit.z} if op not in gate_map: @@ -193,13 +180,13 @@ class IBMQCompiledCircuit: ---------- circuit : QuantumCircuit The Qiskit quantum circuit generated from the pattern. - register_dict : dict[int, int] + register_dict : Mapping[int, int] Mapping from pattern node indices to classical register indices. - circ_output : list[int] + circ_output : Sequence[int] List of output qubit indices in the compiled circuit. """ circuit: QuantumCircuit pattern: Pattern - register_dict: dict[int, int] - circ_output: list[int] + register_dict: Mapping[int, int] + circ_output: Sequence[int] diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index 47c6871..b61c587 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -47,7 +47,7 @@ def is_done(self) -> bool: """ return self.job.status() == JobStatus.DONE - def retrieve_result(self, raw_result: bool = False) -> dict | None: + def retrieve_result(self, raw_result: bool = False) -> dict[str, int] | None: """ Retrieves the result from a completed job. diff --git a/graphix_ibmq/result_utils.py b/graphix_ibmq/result_utils.py index ec85a75..597e1f8 100644 --- a/graphix_ibmq/result_utils.py +++ b/graphix_ibmq/result_utils.py @@ -1,12 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Mapping if TYPE_CHECKING: from graphix.pattern import Pattern -def format_result(result: dict[str, int], pattern: Pattern, register_dict: dict[int, int]) -> dict[str, int]: +def format_result(result: Mapping[str, int], pattern: Pattern, register_dict: Mapping[int, int]) -> dict[str, int]: """Format raw measurement results into output-only bitstrings. Parameters diff --git a/tests/test_backend.py b/tests/test_backend.py index 9b5ecc4..4ed4697 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,76 +1,114 @@ import pytest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from graphix_ibmq.backend import IBMQBackend from graphix_ibmq.compile_options import IBMQCompileOptions - from graphix_ibmq.compiler import IBMQCompiledCircuit from qiskit import QuantumCircuit from qiskit_aer import AerSimulator +# A dummy pattern object for tests +DUMMY_PATTERN = object() -class MockCompiler: - def __init__(self, pattern): - self._pattern = pattern - - def compile(self, save_statevector: bool): - return IBMQCompiledCircuit( - circuit=QuantumCircuit(1), - pattern=self._pattern, - register_dict={}, - circ_output=[], - ) - - -def test_compile_with_default_options(monkeypatch): - monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", MockCompiler) - backend = IBMQBackend() - dummy_pattern = object() - compiled_circuit = backend.compile(dummy_pattern) +def test_compile_is_static_and_works(monkeypatch): + """ + Verify that `compile` is a static method and works without an instance. + """ + # Arrange + mock_compiler_instance = MagicMock() + mock_compiled_circuit = IBMQCompiledCircuit( + circuit=QuantumCircuit(1), pattern=DUMMY_PATTERN, register_dict={}, circ_output=[] + ) + mock_compiler_instance.compile.return_value = mock_compiled_circuit - assert isinstance(compiled_circuit, IBMQCompiledCircuit) - assert compiled_circuit.pattern is dummy_pattern - assert isinstance(backend._options, IBMQCompileOptions) + # Mock the compiler class to return our mock instance + mock_compiler_class = MagicMock(return_value=mock_compiler_instance) + monkeypatch.setattr("graphix_ibmq.backend.IBMQPatternCompiler", mock_compiler_class) + # Act + compiled_circuit = IBMQBackend.compile(DUMMY_PATTERN) -def test_compile_with_invalid_options(): - backend = IBMQBackend() - with pytest.raises(TypeError, match="options must be an instance of IBMQCompileOptions"): - backend.compile(object(), options="not-a-valid-option") + # Assert + assert compiled_circuit is mock_compiled_circuit + mock_compiler_class.assert_called_once_with(DUMMY_PATTERN) + mock_compiler_instance.compile.assert_called_once() -def test_set_simulator(): - backend = IBMQBackend() - backend.set_simulator() +def test_from_simulator_creates_instance(): + """ + Verify that the `from_simulator` factory method creates a valid instance. + """ + # Act + backend = IBMQBackend.from_simulator() + # Assert + assert isinstance(backend, IBMQBackend) assert isinstance(backend._backend, AerSimulator) assert backend._backend.options.noise_model is None -def test_set_hardware(monkeypatch): - backend = IBMQBackend() - +def test_from_hardware_creates_instance(monkeypatch): + """ + Verify that the `from_hardware` factory method creates a valid instance. + """ + # Arrange mock_service_instance = MagicMock() mock_backend_obj = MagicMock() mock_backend_obj.name = "mock_hardware_backend" mock_service_instance.backend.return_value = mock_backend_obj - monkeypatch.setattr("graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance) - backend.set_hardware(name="mock_hardware_backend") + # Act + backend = IBMQBackend.from_hardware(name="mock_hardware_backend") + # Assert + assert isinstance(backend, IBMQBackend) assert backend._backend is mock_backend_obj mock_service_instance.backend.assert_called_once_with("mock_hardware_backend") -def test_submit_job_without_backend_configured(): - backend = IBMQBackend() +def test_direct_instantiation_raises_error(): + """ + Verify that calling IBMQBackend() directly raises a TypeError. + """ + # Act & Assert + with pytest.raises(TypeError, match="cannot be instantiated directly"): + IBMQBackend() + + +def test_submit_job_calls_sampler(monkeypatch): + """ + Verify that `submit_job` correctly uses the configured backend and calls the sampler. + """ + # Arrange + # 1. Create a valid backend instance using a factory + mock_qiskit_backend = MagicMock() + # Use patch to temporarily replace the classmethod's implementation + with patch.object( + IBMQBackend, "from_hardware", return_value=IBMQBackend(mock_qiskit_backend, IBMQCompileOptions()) + ): + backend = IBMQBackend.from_hardware() + + # 2. Mock the transpiler and sampler + mock_pass_manager = MagicMock() + mock_pass_manager.run.return_value = QuantumCircuit(1) # Return a dummy transpiled circuit + monkeypatch.setattr("graphix_ibmq.backend.generate_preset_pass_manager", MagicMock(return_value=mock_pass_manager)) + + mock_sampler_instance = MagicMock() + mock_job = MagicMock() + mock_sampler_instance.run.return_value = mock_job + monkeypatch.setattr("graphix_ibmq.backend.Sampler", MagicMock(return_value=mock_sampler_instance)) + + # 3. Create a dummy compiled circuit to submit dummy_compiled_circuit = IBMQCompiledCircuit( - circuit=QuantumCircuit(1), pattern=object(), register_dict={}, circ_output=[] + circuit=QuantumCircuit(1), pattern=DUMMY_PATTERN, register_dict={}, circ_output=[] ) - with pytest.raises(RuntimeError) as exc_info: - backend.submit_job(dummy_compiled_circuit) + # Act + job_result = backend.submit_job(dummy_compiled_circuit) - assert "Backend not set" in str(exc_info.value) + # Assert + mock_pass_manager.run.assert_called_once_with(dummy_compiled_circuit.circuit) + mock_sampler_instance.run.assert_called_once() + assert job_result.job is mock_job From 8b533a114ed82f16da24f1d816c153a7c706e3ad Mon Sep 17 00:00:00 2001 From: d1ssk Date: Fri, 15 Aug 2025 21:48:26 +0900 Subject: [PATCH 26/30] reflect the review --- graphix_ibmq/backend.py | 12 +++++------- graphix_ibmq/compiler.py | 7 +++++-- graphix_ibmq/job.py | 8 ++++---- requirements.txt | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/graphix_ibmq/backend.py b/graphix_ibmq/backend.py index 7a9a044..63aeaa9 100644 --- a/graphix_ibmq/backend.py +++ b/graphix_ibmq/backend.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from graphix.pattern import Pattern - from qiskit.providers.backend import BackendV2, Backend + from qiskit.providers.backend import BackendV2 logger = logging.getLogger(__name__) @@ -27,14 +27,14 @@ class IBMQBackend: the `from_simulator` or `from_hardware` classmethods. """ - def __init__(self, backend: Backend | None = None, options: IBMQCompileOptions | None = None) -> None: + def __init__(self, backend: BackendV2 | None = None, options: IBMQCompileOptions | None = None) -> None: if backend is None or options is None: raise TypeError( "IBMQBackend cannot be instantiated directly. " "Please use the classmethods `IBMQBackend.from_simulator()` " "or `IBMQBackend.from_hardware()`." ) - self._backend: Backend = backend + self._backend: BackendV2 = backend self._options: IBMQCompileOptions = options @classmethod @@ -69,7 +69,6 @@ def from_simulator( def from_hardware( cls, name: str | None = None, - least_busy: bool = False, min_qubits: int = 1, options: IBMQCompileOptions | None = None, ) -> IBMQBackend: @@ -78,9 +77,8 @@ def from_hardware( Parameters ---------- name : str, optional - The specific name of the device (e.g., 'ibm_brisbane'). - least_busy : bool - If True, selects the least busy device meeting the criteria. + The specific name of the device (e.g., 'ibm_brisbane'). If None, + the least busy backend with at least `min_qubits` will be selected. min_qubits : int The minimum number of qubits required. options : IBMQCompileOptions, optional diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index 4c3057b..e1589b3 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -6,6 +6,7 @@ from graphix.command import CommandKind, N, M, E, X, Z, C from graphix.fundamentals import Plane +from qiskit.circuit.classical import expr from typing import TYPE_CHECKING, Mapping, Sequence, Iterable @@ -137,7 +138,7 @@ def _apply_z(self, cmd: Z) -> None: def _apply_c(self, cmd: C) -> None: """Handles the C command: apply a custom Qiskit circuit method.""" circ_idx = self._qubit_map[cmd.node] - for method_name in cmd.qasm3: + for method_name in cmd.clifford.qasm3: getattr(self._circuit, method_name)(circ_idx) def _apply_classical_feedforward(self, op: str, target_qubit: int, domain: Iterable[int]) -> None: @@ -151,7 +152,9 @@ def _apply_classical_feedforward(self, op: str, target_qubit: int, domain: Itera for node_idx in domain: if node_idx in self._creg_map: creg_idx = self._creg_map[node_idx] - with self._circuit.if_test((self._classical_register[creg_idx], 1)): + clbit = self._classical_register[creg_idx] + cond = expr.equal(clbit, True) + with self._circuit.if_test(cond): apply_gate(target_qubit) elif self._pattern.results.get(node_idx) == 1: apply_gate(target_qubit) diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index b61c587..18d2488 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -1,6 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from qiskit.providers.jobstatus import JobStatus from graphix_ibmq.result_utils import format_result @@ -33,7 +33,7 @@ def id(self) -> str: str The job ID. """ - return self.job.job_id() + return cast(str, self.job.job_id()) @property def is_done(self) -> bool: @@ -45,7 +45,7 @@ def is_done(self) -> bool: bool True if the job is done, False otherwise. """ - return self.job.status() == JobStatus.DONE + return cast(bool, self.job.status() == JobStatus.DONE) def retrieve_result(self, raw_result: bool = False) -> dict[str, int] | None: """ @@ -71,7 +71,7 @@ def retrieve_result(self, raw_result: bool = False) -> dict[str, int] | None: # Result from SamplerV2 contains a list of pub_results. # We assume a single circuit was run, so we take the first element [0]. result = self.job.result() - counts = result[0].data.meas.get_counts() + counts = cast(dict[str, int], result[0].data.meas.get_counts()) if raw_result: return counts diff --git a/requirements.txt b/requirements.txt index 8920504..ad46ef2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy -qiskit>=1.0 +qiskit>=1.0,<2 qiskit_ibm_runtime qiskit-aer graphix From 71f4359d8af4cdca55a190900f79e0616a6f655b Mon Sep 17 00:00:00 2001 From: d1ssk Date: Thu, 28 Aug 2025 21:54:20 +0900 Subject: [PATCH 27/30] Ensure type safety --- graphix_ibmq/compiler.py | 30 ++++++++++++++++-------------- graphix_ibmq/job.py | 10 ++++++---- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index e1589b3..fd92b06 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -4,15 +4,16 @@ import numpy as np from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from graphix.command import CommandKind, N, M, E, X, Z, C +from graphix.command import Command, CommandKind, N, M, E, X, Z, C from graphix.fundamentals import Plane from qiskit.circuit.classical import expr -from typing import TYPE_CHECKING, Mapping, Sequence, Iterable +from typing import TYPE_CHECKING, Callable, Mapping, Sequence, Iterable if TYPE_CHECKING: from graphix.pattern import Pattern +CommandHandler = Callable[[Command], None] class IBMQPatternCompiler: def __init__(self, pattern: Pattern) -> None: @@ -68,18 +69,19 @@ def compile(self, save_statevector: bool = False) -> IBMQCompiledCircuit: def _process_commands(self) -> None: """Iterates through and processes all commands in the pattern.""" - command_handlers = { - CommandKind.N: self._apply_n, - CommandKind.E: self._apply_e, - CommandKind.M: self._apply_m, - CommandKind.X: self._apply_x, - CommandKind.Z: self._apply_z, - CommandKind.C: self._apply_c, - } - for cmd in self._pattern: - handler = command_handlers.get(cmd.kind) - if handler: - handler(cmd) + for cmd in self._pattern: # Iterable[Command] + if isinstance(cmd, N): + self._apply_n(cmd) + elif isinstance(cmd, E): + self._apply_e(cmd) + elif isinstance(cmd, M): + self._apply_m(cmd) + elif isinstance(cmd, X): + self._apply_x(cmd) + elif isinstance(cmd, Z): + self._apply_z(cmd) + elif isinstance(cmd, C): + self._apply_c(cmd) def _allocate_qubit(self, node_idx: int) -> int: """Allocates a qubit from the pool, resets it, and maps it to a node.""" diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index 18d2488..47da843 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -1,6 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from qiskit.providers.jobstatus import JobStatus from graphix_ibmq.result_utils import format_result @@ -33,7 +33,8 @@ def id(self) -> str: str The job ID. """ - return cast(str, self.job.job_id()) + job_id: str = self.job.job_id() + return job_id @property def is_done(self) -> bool: @@ -45,7 +46,8 @@ def is_done(self) -> bool: bool True if the job is done, False otherwise. """ - return cast(bool, self.job.status() == JobStatus.DONE) + is_done: bool = self.job.status() is JobStatus.DONE + return is_done def retrieve_result(self, raw_result: bool = False) -> dict[str, int] | None: """ @@ -71,7 +73,7 @@ def retrieve_result(self, raw_result: bool = False) -> dict[str, int] | None: # Result from SamplerV2 contains a list of pub_results. # We assume a single circuit was run, so we take the first element [0]. result = self.job.result() - counts = cast(dict[str, int], result[0].data.meas.get_counts()) + counts: dict[str, int] = result[0].data.meas.get_counts() if raw_result: return counts From c1074ae022aed425d7dcf69817142858b0ff93d0 Mon Sep 17 00:00:00 2001 From: d1ssk Date: Thu, 28 Aug 2025 21:55:38 +0900 Subject: [PATCH 28/30] black --- graphix_ibmq/compiler.py | 3 ++- graphix_ibmq/job.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/graphix_ibmq/compiler.py b/graphix_ibmq/compiler.py index fd92b06..8751d4e 100644 --- a/graphix_ibmq/compiler.py +++ b/graphix_ibmq/compiler.py @@ -4,7 +4,7 @@ import numpy as np from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from graphix.command import Command, CommandKind, N, M, E, X, Z, C +from graphix.command import Command, N, M, E, X, Z, C from graphix.fundamentals import Plane from qiskit.circuit.classical import expr @@ -15,6 +15,7 @@ CommandHandler = Callable[[Command], None] + class IBMQPatternCompiler: def __init__(self, pattern: Pattern) -> None: """ diff --git a/graphix_ibmq/job.py b/graphix_ibmq/job.py index 47da843..fca438b 100644 --- a/graphix_ibmq/job.py +++ b/graphix_ibmq/job.py @@ -33,7 +33,7 @@ def id(self) -> str: str The job ID. """ - job_id: str = self.job.job_id() + job_id: str = self.job.job_id() return job_id @property From c6f94d142081d4264075689199da2b53eab0c45a Mon Sep 17 00:00:00 2001 From: d1ssk Date: Fri, 5 Sep 2025 17:37:34 +0900 Subject: [PATCH 29/30] add test fot job.py --- tests/test_backend.py | 31 +++++++-- tests/test_compile_options.py | 5 +- tests/test_compiler.py | 8 ++- tests/test_job.py | 116 ++++++++++++++++++++++++++++++++++ tests/test_job_integration.py | 49 ++++++++++++++ 5 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 tests/test_job.py create mode 100644 tests/test_job_integration.py diff --git a/tests/test_backend.py b/tests/test_backend.py index 4ed4697..010cba9 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -18,7 +18,10 @@ def test_compile_is_static_and_works(monkeypatch): # Arrange mock_compiler_instance = MagicMock() mock_compiled_circuit = IBMQCompiledCircuit( - circuit=QuantumCircuit(1), pattern=DUMMY_PATTERN, register_dict={}, circ_output=[] + circuit=QuantumCircuit(1), + pattern=DUMMY_PATTERN, + register_dict={}, + circ_output=[], ) mock_compiler_instance.compile.return_value = mock_compiled_circuit @@ -57,7 +60,9 @@ def test_from_hardware_creates_instance(monkeypatch): mock_backend_obj = MagicMock() mock_backend_obj.name = "mock_hardware_backend" mock_service_instance.backend.return_value = mock_backend_obj - monkeypatch.setattr("graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance) + monkeypatch.setattr( + "graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance + ) # Act backend = IBMQBackend.from_hardware(name="mock_hardware_backend") @@ -86,23 +91,35 @@ def test_submit_job_calls_sampler(monkeypatch): mock_qiskit_backend = MagicMock() # Use patch to temporarily replace the classmethod's implementation with patch.object( - IBMQBackend, "from_hardware", return_value=IBMQBackend(mock_qiskit_backend, IBMQCompileOptions()) + IBMQBackend, + "from_hardware", + return_value=IBMQBackend(mock_qiskit_backend, IBMQCompileOptions()), ): backend = IBMQBackend.from_hardware() # 2. Mock the transpiler and sampler mock_pass_manager = MagicMock() - mock_pass_manager.run.return_value = QuantumCircuit(1) # Return a dummy transpiled circuit - monkeypatch.setattr("graphix_ibmq.backend.generate_preset_pass_manager", MagicMock(return_value=mock_pass_manager)) + mock_pass_manager.run.return_value = QuantumCircuit( + 1 + ) # Return a dummy transpiled circuit + monkeypatch.setattr( + "graphix_ibmq.backend.generate_preset_pass_manager", + MagicMock(return_value=mock_pass_manager), + ) mock_sampler_instance = MagicMock() mock_job = MagicMock() mock_sampler_instance.run.return_value = mock_job - monkeypatch.setattr("graphix_ibmq.backend.Sampler", MagicMock(return_value=mock_sampler_instance)) + monkeypatch.setattr( + "graphix_ibmq.backend.Sampler", MagicMock(return_value=mock_sampler_instance) + ) # 3. Create a dummy compiled circuit to submit dummy_compiled_circuit = IBMQCompiledCircuit( - circuit=QuantumCircuit(1), pattern=DUMMY_PATTERN, register_dict={}, circ_output=[] + circuit=QuantumCircuit(1), + pattern=DUMMY_PATTERN, + register_dict={}, + circ_output=[], ) # Act diff --git a/tests/test_compile_options.py b/tests/test_compile_options.py index c518537..f1783bb 100644 --- a/tests/test_compile_options.py +++ b/tests/test_compile_options.py @@ -1,4 +1,3 @@ -import pytest from graphix_ibmq.compile_options import IBMQCompileOptions @@ -10,6 +9,8 @@ def test_default_options(): def test_repr(): - opts = IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method="dense") + opts = IBMQCompileOptions( + optimization_level=2, save_statevector=True, layout_method="dense" + ) expected = "IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method='dense')" assert repr(opts) == expected diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 8ea6245..ad2f0cb 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -6,7 +6,9 @@ import graphix.random_objects as rc -def reduce_statevector_to_outputs(statevector: np.ndarray, output_qubits: list) -> np.ndarray: +def reduce_statevector_to_outputs( + statevector: np.ndarray, output_qubits: list +) -> np.ndarray: """Reduce a full statevector to the subspace corresponding to output_qubits.""" n = round(np.log2(len(statevector))) reduced = np.zeros(2 ** len(output_qubits), dtype=complex) @@ -41,7 +43,9 @@ def test_ibmq_compiler_statevector_equivalence(nqubits, depth): transpiled = transpile(qc, sim) result = sim.run(transpiled).result() qiskit_state = np.array(result.get_statevector(qc)) - qiskit_reduced = reduce_statevector_to_outputs(qiskit_state, compiled.circ_output) + qiskit_reduced = reduce_statevector_to_outputs( + qiskit_state, compiled.circ_output + ) fidelity = np.abs(np.dot(qiskit_reduced.conjugate(), mbqc_state.flatten())) assert np.isclose(fidelity, 1.0, atol=1e-6), f"Fidelity mismatch: {fidelity}" diff --git a/tests/test_job.py b/tests/test_job.py new file mode 100644 index 0000000..94d2d58 --- /dev/null +++ b/tests/test_job.py @@ -0,0 +1,116 @@ +import pytest + +import graphix_ibmq.job as job_module + +from graphix_ibmq.job import IBMQJob +from qiskit.providers.jobstatus import JobStatus + + +class FakeMeas: + def __init__(self, counts: dict[str, int]): + self._counts = counts + + def get_counts(self) -> dict[str, int]: + return dict(self._counts) + + +class FakeData: + def __init__(self, counts: dict[str, int]): + self.meas = FakeMeas(counts) + + +class FakePubResult: + def __init__(self, counts: dict[str, int]): + self.data = FakeData(counts) + + +class FakeJobDone: + def __init__(self, job_id: str, counts: dict[str, int]): + self._job_id = job_id + self._pub_results = [FakePubResult(counts)] + + def job_id(self) -> str: + return self._job_id + + def status(self): + return JobStatus.DONE + + def result(self): + return self._pub_results + + +class FakeJobRunning(FakeJobDone): + def status(self): + return JobStatus.RUNNING + + +class FakeCompiledCircuit: + def __init__(self): + self.pattern = object() + self.register_dict = {"out": [0, 1, 2]} + + +@pytest.fixture +def counts(): + return {"000": 10, "011": 5, "101": 7} + + +def test_id_property_returns_job_id(counts): + fake = FakeJobDone("ABC123", counts) + job = IBMQJob(job=fake, compiled_circuit=FakeCompiledCircuit()) + assert job.id == "ABC123" + + +def test_is_done_true_false(counts): + job_done = IBMQJob( + job=FakeJobDone("ID", counts), compiled_circuit=FakeCompiledCircuit() + ) + job_run = IBMQJob( + job=FakeJobRunning("ID", counts), compiled_circuit=FakeCompiledCircuit() + ) + assert job_done.is_done is True + assert job_run.is_done is False + + +def test_retrieve_result_returns_none_if_not_done(counts): + job = IBMQJob( + job=FakeJobRunning("ID", counts), compiled_circuit=FakeCompiledCircuit() + ) + assert job.retrieve_result() is None + assert job.retrieve_result(raw_result=True) is None + + +def test_retrieve_result_raw_counts_path_hits_get_counts(counts, monkeypatch): + called = {"format_called": False} + + def fake_format_result(_counts, _pattern, _reg): + called["format_called"] = True + return {"should_not_be_returned": 1} + + monkeypatch.setattr(job_module, "format_result", fake_format_result) + + job = IBMQJob(job=FakeJobDone("ID", counts), compiled_circuit=FakeCompiledCircuit()) + out = job.retrieve_result(raw_result=True) + assert out == counts + assert called["format_called"] is False + + +def test_retrieve_result_formatted_path_calls_format_result(counts, monkeypatch): + received = {} + + def fake_format_result(_counts, _pattern, _reg): + received["counts"] = _counts + received["pattern"] = _pattern + received["register_dict"] = _reg + return {"formatted": sum(_counts.values())} + + monkeypatch.setattr(job_module, "format_result", fake_format_result) + + fake_compiled = FakeCompiledCircuit() + job = IBMQJob(job=FakeJobDone("ID", counts), compiled_circuit=fake_compiled) + out = job.retrieve_result(raw_result=False) + + assert out == {"formatted": sum(counts.values())} + assert received["counts"] == counts + assert received["pattern"] is fake_compiled.pattern + assert received["register_dict"] is fake_compiled.register_dict diff --git a/tests/test_job_integration.py b/tests/test_job_integration.py new file mode 100644 index 0000000..4e703e2 --- /dev/null +++ b/tests/test_job_integration.py @@ -0,0 +1,49 @@ +# tests/test_job_integration.py +import time +import pytest + +pytest.importorskip("qiskit_aer") +pytest.importorskip("qiskit_ibm_runtime") +pytest.importorskip("graphix") +pytest.importorskip("graphix_ibmq") + +from graphix.transpiler import Circuit +from graphix_ibmq.backend import IBMQBackend +from graphix_ibmq.job import IBMQJob + + +@pytest.mark.integration +def test_retrieve_result_real_qiskit_aer_chain(tmp_path): + # 1) 1qubit H + circ = Circuit(1) + circ.h(0) + + pattern = circ.transpile().pattern + pattern.minimize_space() + + backend = IBMQBackend.from_simulator() + compiled = backend.compile(pattern) + + shots = 256 + ibmq_job = backend.submit_job(compiled, shots=shots) + + job = IBMQJob(job=ibmq_job.job, compiled_circuit=compiled) + + deadline = time.time() + 30 + while not job.is_done and time.time() < deadline: + time.sleep(0.05) + assert job.is_done, "Job did not reach DONE within timeout" + + counts_raw = job.retrieve_result(raw_result=True) + assert isinstance(counts_raw, dict) + + out_bits = len(compiled.register_dict.get("out", [])) + if out_bits == 0 and len(counts_raw) > 0: + out_bits = len(next(iter(counts_raw.keys()))) + assert out_bits >= 1 + assert all(isinstance(k, str) and len(k) == out_bits for k in counts_raw.keys()) + assert sum(counts_raw.values()) == shots + + counts_formatted = job.retrieve_result(raw_result=False) + assert isinstance(counts_formatted, dict) + assert len(counts_formatted) >= 1 From ae3882f4b1aabed7e88ce58f32368b04a3655fed Mon Sep 17 00:00:00 2001 From: d1ssk Date: Fri, 5 Sep 2025 17:41:28 +0900 Subject: [PATCH 30/30] black --- tests/test_backend.py | 12 +++--------- tests/test_compile_options.py | 4 +--- tests/test_compiler.py | 8 ++------ tests/test_job.py | 12 +++--------- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 010cba9..b13cef7 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -60,9 +60,7 @@ def test_from_hardware_creates_instance(monkeypatch): mock_backend_obj = MagicMock() mock_backend_obj.name = "mock_hardware_backend" mock_service_instance.backend.return_value = mock_backend_obj - monkeypatch.setattr( - "graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance - ) + monkeypatch.setattr("graphix_ibmq.backend.QiskitRuntimeService", lambda: mock_service_instance) # Act backend = IBMQBackend.from_hardware(name="mock_hardware_backend") @@ -99,9 +97,7 @@ def test_submit_job_calls_sampler(monkeypatch): # 2. Mock the transpiler and sampler mock_pass_manager = MagicMock() - mock_pass_manager.run.return_value = QuantumCircuit( - 1 - ) # Return a dummy transpiled circuit + mock_pass_manager.run.return_value = QuantumCircuit(1) # Return a dummy transpiled circuit monkeypatch.setattr( "graphix_ibmq.backend.generate_preset_pass_manager", MagicMock(return_value=mock_pass_manager), @@ -110,9 +106,7 @@ def test_submit_job_calls_sampler(monkeypatch): mock_sampler_instance = MagicMock() mock_job = MagicMock() mock_sampler_instance.run.return_value = mock_job - monkeypatch.setattr( - "graphix_ibmq.backend.Sampler", MagicMock(return_value=mock_sampler_instance) - ) + monkeypatch.setattr("graphix_ibmq.backend.Sampler", MagicMock(return_value=mock_sampler_instance)) # 3. Create a dummy compiled circuit to submit dummy_compiled_circuit = IBMQCompiledCircuit( diff --git a/tests/test_compile_options.py b/tests/test_compile_options.py index f1783bb..5e530b9 100644 --- a/tests/test_compile_options.py +++ b/tests/test_compile_options.py @@ -9,8 +9,6 @@ def test_default_options(): def test_repr(): - opts = IBMQCompileOptions( - optimization_level=2, save_statevector=True, layout_method="dense" - ) + opts = IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method="dense") expected = "IBMQCompileOptions(optimization_level=2, save_statevector=True, layout_method='dense')" assert repr(opts) == expected diff --git a/tests/test_compiler.py b/tests/test_compiler.py index ad2f0cb..8ea6245 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -6,9 +6,7 @@ import graphix.random_objects as rc -def reduce_statevector_to_outputs( - statevector: np.ndarray, output_qubits: list -) -> np.ndarray: +def reduce_statevector_to_outputs(statevector: np.ndarray, output_qubits: list) -> np.ndarray: """Reduce a full statevector to the subspace corresponding to output_qubits.""" n = round(np.log2(len(statevector))) reduced = np.zeros(2 ** len(output_qubits), dtype=complex) @@ -43,9 +41,7 @@ def test_ibmq_compiler_statevector_equivalence(nqubits, depth): transpiled = transpile(qc, sim) result = sim.run(transpiled).result() qiskit_state = np.array(result.get_statevector(qc)) - qiskit_reduced = reduce_statevector_to_outputs( - qiskit_state, compiled.circ_output - ) + qiskit_reduced = reduce_statevector_to_outputs(qiskit_state, compiled.circ_output) fidelity = np.abs(np.dot(qiskit_reduced.conjugate(), mbqc_state.flatten())) assert np.isclose(fidelity, 1.0, atol=1e-6), f"Fidelity mismatch: {fidelity}" diff --git a/tests/test_job.py b/tests/test_job.py index 94d2d58..1e2c1c9 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -62,20 +62,14 @@ def test_id_property_returns_job_id(counts): def test_is_done_true_false(counts): - job_done = IBMQJob( - job=FakeJobDone("ID", counts), compiled_circuit=FakeCompiledCircuit() - ) - job_run = IBMQJob( - job=FakeJobRunning("ID", counts), compiled_circuit=FakeCompiledCircuit() - ) + job_done = IBMQJob(job=FakeJobDone("ID", counts), compiled_circuit=FakeCompiledCircuit()) + job_run = IBMQJob(job=FakeJobRunning("ID", counts), compiled_circuit=FakeCompiledCircuit()) assert job_done.is_done is True assert job_run.is_done is False def test_retrieve_result_returns_none_if_not_done(counts): - job = IBMQJob( - job=FakeJobRunning("ID", counts), compiled_circuit=FakeCompiledCircuit() - ) + job = IBMQJob(job=FakeJobRunning("ID", counts), compiled_circuit=FakeCompiledCircuit()) assert job.retrieve_result() is None assert job.retrieve_result(raw_result=True) is None