diff --git a/docs/source/conf.py b/docs/source/conf.py index 7e08490..d13fbb6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,10 +7,10 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'graphix_ibmq' -copyright = '2023, Team Graphix' -author = 'Daichi Sasaki, Shinichi Sunami' -release = '0.0.1' +project = "graphix_ibmq" +copyright = "2023, Team Graphix" +author = "Daichi Sasaki, Shinichi Sunami" +release = "0.0.1" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -59,5 +59,5 @@ def setup(app): "examples_dirs": "../../examples/gallery", # path to your example scripts "gallery_dirs": "gallery", # path to where to save gallery generated output #'expected_failing_examples': ['../../examples/ibm_device.py'], - 'filename_pattern': '/' + "filename_pattern": "/", } diff --git a/examples/gallery/aer_sim.py b/examples/gallery/aer_sim.py index 66dc993..322c60e 100644 --- a/examples/gallery/aer_sim.py +++ b/examples/gallery/aer_sim.py @@ -7,16 +7,18 @@ First, let us import relevant modules and define additional gates and function we'll use: """ -#%% -import numpy as np + +# %% + import matplotlib.pyplot as plt import networkx as nx -import random +import numpy as np 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_ibmq.runner import IBMQBackend + def cp(circuit, theta, control, target): """Controlled phase gate, decomposed""" @@ -34,7 +36,10 @@ def swap(circuit, a, b): circuit.cnot(a, b) -#%% +rng = np.random.default_rng(seed=100) + + +# %% # Now let us define a circuit to apply QFT to three-qubit state and transpile into MBQC measurement pattern using graphix. circuit = Circuit(3) @@ -42,11 +47,10 @@ def swap(circuit, a, b): circuit.h(i) psi = {} -random.seed(100) # prepare random state for each input qubit for i in range(3): - theta = random.uniform(0, np.pi) - phi = random.uniform(0, 2 * np.pi) + theta = rng.uniform(0, np.pi) + phi = rng.uniform(0, 2 * np.pi) circuit.ry(i, theta) circuit.rz(i, phi) psi[i] = [np.cos(theta / 2), np.sin(theta / 2) * np.exp(1j * phi)] @@ -72,11 +76,10 @@ def swap(circuit, a, b): g = nx.Graph() g.add_nodes_from(nodes) g.add_edges_from(edges) -np.random.seed(100) nx.draw(g) plt.show() -#%% +# %% # Now let us convert the pattern to qiskit circuit. # minimize the space to save memory during aer simulation @@ -87,13 +90,13 @@ def swap(circuit, a, b): backend = IBMQBackend(pattern) print(type(backend.circ)) -#%% +# %% # We can now simulate the circuit with Aer. # run and get counts result = backend.simulate() -#%% +# %% # We can also simulate the circuit with noise model # create an empty noise model @@ -105,17 +108,17 @@ def swap(circuit, a, b): # print noise model info print(noise_model) -#%% +# %% # Now we can run the simulation with noise model # run and get counts result_noise = backend.simulate(noise_model=noise_model) -#%% +# %% # Now let us compare the results -# calculate the analytical +# calculate the analytical state = [0] * 8 omega = np.exp(1j * np.pi / 4) @@ -129,17 +132,23 @@ def swap(circuit, a, b): count_theory[f"{i:03b}"] = 1024 * np.abs(state[i]) ** 2 # plot and compare the results -fig, ax = plt.subplots(figsize=(7,5)) -plot_histogram([count_theory, result, result_noise], - legend=["theoretical probability", "execution result", "Aer simulation w/ noise model"], - ax=ax, - bar_labels=False) +fig, ax = plt.subplots(figsize=(7, 5)) +plot_histogram( + [count_theory, result, result_noise], + legend=[ + "theoretical probability", + "execution result", + "Aer simulation w/ noise model", + ], + ax=ax, + bar_labels=False, +) legend = ax.legend(fontsize=18) -legend = ax.legend(loc='upper left') +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. diff --git a/examples/gallery/qiskit_to_graphix.py b/examples/gallery/qiskit_to_graphix.py index d79ac7d..aae29e4 100644 --- a/examples/gallery/qiskit_to_graphix.py +++ b/examples/gallery/qiskit_to_graphix.py @@ -7,9 +7,8 @@ First, let us import relevant modules and define quantum circuit we want to convert: """ - # %% -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/examples/ibm_device.py b/examples/ibm_device.py index 2f3bf01..01bab81 100644 --- a/examples/ibm_device.py +++ b/examples/ibm_device.py @@ -7,17 +7,20 @@ First, let us import relevant modules and define additional gates and function we'll use: """ -#%% -import numpy as np + +# %% + +import os + import matplotlib.pyplot as plt import networkx as nx -import random +import numpy as np from graphix import Circuit -from graphix_ibmq.runner import IBMQBackend -from qiskit_ibm_provider import IBMProvider -from qiskit.tools.visualization import plot_histogram from qiskit.providers.fake_provider import FakeLagos +from qiskit.tools.visualization import plot_histogram +from qiskit_ibm_provider import IBMProvider +from graphix_ibmq.runner import IBMQBackend def cp(circuit, theta, control, target): @@ -36,7 +39,10 @@ def swap(circuit, a, b): circuit.cnot(a, b) -#%% +rng = np.random.default_rng(seed=100) +token = os.environ["API_TOKEN"] + +# %% # Now let us define a circuit to apply QFT to three-qubit state. circuit = Circuit(3) @@ -46,8 +52,8 @@ def swap(circuit, a, b): psi = {} # prepare random state for each input qubit for i in range(3): - theta = random.uniform(0, np.pi) - phi = random.uniform(0, 2 * np.pi) + theta = rng.uniform(0, np.pi) + phi = rng.uniform(0, 2 * np.pi) circuit.ry(i, theta) circuit.rz(i, phi) psi[i] = [np.cos(theta / 2), np.sin(theta / 2) * np.exp(1j * phi)] @@ -73,11 +79,10 @@ def swap(circuit, a, b): g = nx.Graph() g.add_nodes_from(nodes) g.add_edges_from(edges) -np.random.seed(100) nx.draw(g) plt.show() -#%% +# %% # Now let us convert the pattern to qiskit circuit. # minimize the space to save memory during aer simulation. @@ -88,16 +93,16 @@ def swap(circuit, a, b): backend.to_qiskit() print(type(backend.circ)) -#%% +# %% # load the account with API token -IBMProvider.save_account(token='MY API TOKEN') +IBMProvider.save_account(token=token) # get the device backend -instance_name = 'ibm-q/open/main' +instance_name = "ibm-q/open/main" backend_name = "ibm_lagos" -backend.get_backend(instance=instance_name,resource=backend_name) +backend.get_backend(instance=instance_name, resource=backend_name) -#%% +# %% # Get provider and the backend. instance_name = "ibm-q/open/main" @@ -105,17 +110,17 @@ def swap(circuit, a, b): backend.get_backend(instance=instance_name, resource=backend_name) -#%% +# %% # We can now execute the circuit on the device backend. result = backend.run() -#%% +# %% # Retrieve the job if needed # result = backend.retrieve_result("Job ID") -#%% +# %% # We can simulate the circuit with noise model based on the device we used # get the noise model of the device backend @@ -124,7 +129,7 @@ def swap(circuit, a, b): # execute noisy simulation and get counts result_noise = backend.simulate(noise_model=backend_noisemodel) -#%% +# %% # Now let us compare the results with theoretical output # calculate the theoretical output state @@ -141,12 +146,16 @@ def swap(circuit, a, b): count_theory[f"{i:03b}"] = 1024 * np.abs(state[i]) ** 2 # plot and compare the results -fig, ax = plt.subplots(figsize=(7,5)) +fig, ax = plt.subplots(figsize=(7, 5)) plot_histogram( [count_theory, result, result_noise], - legend=["theoretical probability", "execution result", "Aer simulation w/ noise model"], + legend=[ + "theoretical probability", + "execution result", + "Aer simulation w/ noise model", + ], ax=ax, - bar_labels=False + bar_labels=False, ) legend = ax.legend(fontsize=18) -legend = ax.legend(loc='upper left') +legend = ax.legend(loc="upper left") diff --git a/graphix_ibmq/__init__.py b/graphix_ibmq/__init__.py index e69de29..dc04103 100644 --- a/graphix_ibmq/__init__.py +++ b/graphix_ibmq/__init__.py @@ -0,0 +1 @@ +"""Graphix IBMQ interface.""" diff --git a/graphix_ibmq/clifford.py b/graphix_ibmq/clifford.py deleted file mode 100644 index 7b8f1d9..0000000 --- a/graphix_ibmq/clifford.py +++ /dev/null @@ -1,64 +0,0 @@ -import numpy as np - -# Conjugation of Clifford gates result in a Clifford gate. -# CLIFFORD_CONJ provides the Clifford index of conjugated matrix. -# Example (S and S dagger): CLIFFORD_CONJ[4] -# see graphix.clifford module for the definitions and details of Clifford operatos for each index. -CLIFFORD_CONJ = np.array( - [ - 0, - 1, - 2, - 3, - 5, - 4, - 6, - 15, - 12, - 9, - 10, - 11, - 8, - 13, - 14, - 7, - 20, - 22, - 23, - 21, - 16, - 19, - 17, - 18, - ], - dtype=np.int32, -) - -# qiskit representation of Clifford gates above. -# see graphix.clifford module for the definitions and details of Clifford operatos for each index. -CLIFFORD_TO_QISKIT = [ - ["id"], - ["x"], - ["y"], - ["z"], - ["s"], - ["sdg"], - ["h"], - ["sdg", "h", "sdg"], - ["h", "x"], - ["sdg", "y"], - ["sdg", "x"], - ["h", "y"], - ["h", "z"], - ["sdg", "h", "sdg", "y"], - ["sdg", "h", "s"], - ["sdg", "h", "sdg", "x"], - ["sdg", "h"], - ["sdg", "h", "y"], - ["sdg", "h", "z"], - ["sdg", "h", "x"], - ["h", "s"], - ["h", "sdg"], - ["h", "x", "sdg"], - ["h", "x", "s"], -] diff --git a/graphix_ibmq/converter.py b/graphix_ibmq/converter.py index 83c8422..b33dc21 100644 --- a/graphix_ibmq/converter.py +++ b/graphix_ibmq/converter.py @@ -1,3 +1,5 @@ +"""Qiskit to graphix circuit converter.""" + from graphix import Circuit from qiskit import QuantumCircuit, transpile @@ -14,6 +16,7 @@ def qiskit_to_graphix(qc: QuantumCircuit) -> Circuit: Returns: Circuit: Converted graphix circuit + """ if not isinstance(qc, QuantumCircuit): raise TypeError(f"qc must be QuantumCircuit, not {type(qc)}") diff --git a/graphix_ibmq/runner.py b/graphix_ibmq/runner.py index f29ae0d..fb26b2e 100644 --- a/graphix_ibmq/runner.py +++ b/graphix_ibmq/runner.py @@ -1,17 +1,82 @@ +"""Interface for MBQC pattern execution on IBM quantum devices.""" + from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from graphix.pattern import Pattern +import enum +from enum import Enum +from typing import TYPE_CHECKING, assert_never import numpy as np +from graphix.command import CommandKind +from graphix.fundamentals import Plane 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 qiskit_ibm_runtime import IBMBackend, QiskitRuntimeService, SamplerV2 -from graphix_ibmq.clifford import CLIFFORD_CONJ, CLIFFORD_TO_QISKIT +if TYPE_CHECKING: + from graphix.pattern import Pattern + + +class QiskitUGate(Enum): + """Single-qubit gate.""" + + ID = enum.auto() + X = enum.auto() + Y = enum.auto() + Z = enum.auto() + S = enum.auto() + SDG = enum.auto() + H = enum.auto() + + def perform(self, circ: QuantumCircuit, idx: int) -> None: + """Perform the gate.""" + if self == QiskitUGate.ID: + pass + elif self == QiskitUGate.X: + circ.x(idx) + elif self == QiskitUGate.Y: + circ.y(idx) + elif self == QiskitUGate.Z: + circ.z(idx) + elif self == QiskitUGate.S: + circ.s(idx) + elif self == QiskitUGate.SDG: + circ.sdg(idx) + elif self == QiskitUGate.H: + circ.h(idx) + else: + assert_never(self) + + +# qiskit representation of Clifford gates above. +# see graphix.clifford module for the definitions and details of Clifford operatos for each index. +CLIFFORD_TO_QISKIT = [ + [QiskitUGate.ID], + [QiskitUGate.X], + [QiskitUGate.Y], + [QiskitUGate.Z], + [QiskitUGate.S], + [QiskitUGate.SDG], + [QiskitUGate.H], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.SDG], + [QiskitUGate.H, QiskitUGate.X], + [QiskitUGate.SDG, QiskitUGate.Y], + [QiskitUGate.SDG, QiskitUGate.X], + [QiskitUGate.H, QiskitUGate.Y], + [QiskitUGate.H, QiskitUGate.Z], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.SDG, QiskitUGate.Y], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.S], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.SDG, QiskitUGate.X], + [QiskitUGate.SDG, QiskitUGate.H], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.Y], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.Z], + [QiskitUGate.SDG, QiskitUGate.H, QiskitUGate.X], + [QiskitUGate.H, QiskitUGate.S], + [QiskitUGate.H, QiskitUGate.SDG], + [QiskitUGate.H, QiskitUGate.X, QiskitUGate.SDG], + [QiskitUGate.H, QiskitUGate.X, QiskitUGate.S], +] class IBMQBackend: @@ -27,20 +92,22 @@ class IBMQBackend: 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): - """ + """Initialize interface. 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): + def get_system(self, service: QiskitRuntimeService, system: str | None = None): """Get the system to be used for the execution. Parameters @@ -49,9 +116,8 @@ def get_system(self, service: QiskitRuntimeService, system: str = None): 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()]: @@ -63,21 +129,22 @@ def get_system(self, service: QiskitRuntimeService, system: str = None): 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. + """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 + n_node = self.pattern.n_node qr = QuantumRegister(n) - cr = ClassicalRegister(N_node) + cr = ClassicalRegister(n_node) circ = QuantumCircuit(qr, cr) - empty_qubit = [i for i in range(n)] # list of free qubit indices + empty_qubit = list(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 @@ -85,22 +152,20 @@ def to_qiskit(self, save_statevector: bool = False): def signal_process(op, signal): 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) - else: - if self.pattern.results[s] == 1: - circ.x(circ_idx) + elif self.pattern.results[s] == 1: + 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) - else: - if self.pattern.results[s] == 1: - circ.z(circ_idx) + elif self.pattern.results[s] == 1: + circ.z(circ_idx) for i in self.pattern.input_nodes: circ_idx = empty_qubit[0] @@ -110,74 +175,56 @@ def signal_process(op, signal): qubit_dict[i] = circ_idx for cmd in self.pattern: - - if cmd[0] == "N": + 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[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] + qubit_dict[cmd.node] = circ_idx + + if cmd.kind == CommandKind.E: + u, v = cmd.nodes + circ.cz(qubit_dict[u], qubit_dict[v]) + + 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 + signal_process("X", s_list) + signal_process("Z", t_list) + + if plane == Plane.XY: + circ.rz(-alpha, circ_idx) + circ.h(circ_idx) + elif plane == Plane.YZ: + circ.rx(alpha, circ_idx) + elif plane == Plane.XZ: + circ.ry(alpha, circ_idx) + else: + assert_never(plane) + 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 - if cmd[0] == "Z": - circ_idx = qubit_dict[cmd[1]] - s_list = cmd[2] + if cmd.kind == CommandKind.X: + circ_idx = qubit_dict[cmd.node] + s_list = cmd.domain + signal_process("X", s_list) + + if cmd.kind == CommandKind.Z: + circ_idx = qubit_dict[cmd.node] + s_list = cmd.domain 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 cmd.kind == CommandKind.C: + circ_idx = qubit_dict[cmd.node] + cid = cmd.clifford + for op in CLIFFORD_TO_QISKIT[cid.value]: + op.perform(circ, circ_idx) if save_statevector: circ.save_statevector() @@ -203,7 +250,8 @@ def signal_process(op, signal): self.circ = circ def set_input(self, psi: list[list[complex]]): - """set the input state of the circuit. + """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 @@ -211,26 +259,23 @@ def set_input(self, psi: list[list[complex]]): 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) + input_nodes = list(self.pattern.input_nodes) + n = len(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: + if k >= n: break + if ope[0].name == "reset": + qubit_idx = ope[1][0]._index + i = input_nodes[k] + self.circ.initialize(psi[i], qubit_idx) def transpile(self, system: IBMBackend = None, optimization_level: int = 1): - """transpile the circuit for the designated resource. + """Transpile the circuit for the designated resource. Parameters ---------- @@ -238,6 +283,7 @@ def transpile(self, system: IBMBackend = None, optimization_level: int = 1): 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"): @@ -245,8 +291,13 @@ def transpile(self, system: IBMBackend = None, optimization_level: int = 1): 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. + def simulate( + self, + shots: int = 1024, + noise_model: NoiseModel = None, + format_result: bool = True, + ): + """Simulate the circuit with Aer. Parameters ---------- @@ -258,9 +309,10 @@ def simulate(self, shots: int = 1024, noise_model: NoiseModel = None, format_res 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: @@ -268,8 +320,8 @@ def simulate(self, shots: int = 1024, noise_model: NoiseModel = None, format_res else: try: simulator = AerSimulator.from_backend(noise_model) - except: - raise ValueError("Invalid noise model.") + except NotImplementedError as exc: + raise ValueError("Invalid noise model.") from exc else: simulator = AerSimulator() circ_sim = transpile(self.circ, simulator) @@ -281,7 +333,7 @@ def simulate(self, shots: int = 1024, noise_model: NoiseModel = None, format_res return result def run(self, shots: int = 1024, format_result: bool = True, optimization_level: int = 1): - """Run the MBQC pattern on IBMQ devices + """Run the MBQC pattern on IBMQ devices. Parameters ---------- @@ -296,6 +348,7 @@ def run(self, shots: int = 1024, format_result: bool = True, optimization_level: ------- result : dict the measurement result. + """ self.transpile(optimization_level=optimization_level) if not hasattr(self, "system"): @@ -324,16 +377,17 @@ def format_result(self, result: dict[str, int]): ------- masked_results : dict Dictionary of formatted results. + """ masked_results = {} - N_node = self.pattern.Nnode + 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] + masked_key += key[n_node - reg_idx - 1] if masked_key in masked_results: masked_results[masked_key] += value else: @@ -355,6 +409,7 @@ def retrieve_result(self, job_id: str, format_result: bool = True): ------- result : dict the measurement result. + """ if not hasattr(self, "service"): raise ValueError("No service is set.") diff --git a/graphix_ibmq/version.py b/graphix_ibmq/version.py index 81f0fde..222e506 100644 --- a/graphix_ibmq/version.py +++ b/graphix_ibmq/version.py @@ -1 +1,3 @@ +"""Library version number.""" + __version__ = "0.0.4" diff --git a/pyproject.toml b/pyproject.toml index fed528d..151bf1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,79 @@ [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" + +[tool.ruff] +line-length = 120 +extend-exclude = ["docs"] + +[tool.ruff.lint] +preview = true +select = ["ALL"] +extend-ignore = [ + "C90", # Complexity + "E74", # Ambiguous name + "ERA", # Commmented out code + "FBT", # Boolean positional arguments + "FIX", # Fixme + "PLR091", # Too many XXX + "PLR0904", # Too many public methods + "PLR2004", # Magic vavlue comparison + "S101", # assert + "T20", # print + "TD", # Todo + + # Tentative ignores + "ANN", # Missing annotations + "CPY", # Copyright + "DOC", # Docstring + "E501", # Line too long + "EM10", # Raise string + "PLC0415", # Import not at the top level + "PLR1702", # Too many nests + "PLW1641", # __hash__ missing + "PT011", # pytest raises too broad + "SLF001", # Private access + "TRY003", # Raise vanilla args + + "D203", # `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`. + "D213", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`. + + # Conflicts with ruff format + "COM812", + "COM819", + "D206", + "D300", + "E111", + "E114", + "E117", + "ISC001", + "ISC002", + "Q000", + "Q001", + "Q002", + "Q003", + "W191", +] +# Allow "α" (U+03B1 GREEK SMALL LETTER ALPHA) which could be confused for "a" +allowed-confusables = ["α"] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint.extend-per-file-ignores] +"benchmarks/*.py" = [ + "D", # Benchmarks follow Sphinx doc conventions + "INP", # Missing __init__.py +] +"examples/*.py" = [ + "ARG", # Unused arguments + "B018", # Useless expression + "D", # Examples follow Sphinx doc conventions + "E402", # Import not at top of file + "INP", # Missing __init__.py +] +"tests/*.py" = [ + "D10", # Allow undocumented items + "PLC2701", # Allow private imports + "PLR6301", # self not used +] diff --git a/requirements.txt b/requirements.txt index 485ac3f..8530199 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy>=1.22,<1.26 +numpy>=1.24,<3 qiskit>=1.0 qiskit_ibm_runtime qiskit-aer diff --git a/setup.py b/setup.py index a19f1b4..bd9391c 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,20 @@ +"""Setup file for package.""" + +from pathlib import Path + from setuptools import find_packages, setup -with open("README.md", "r", encoding="utf-8") as fh: - long_description = fh.read() +from graphix_ibmq.version import __version__ -version = {} -with open("graphix_ibmq/version.py") as fp: - exec(fp.read(), version) +with Path("README.md").open(encoding="utf-8") as fh: + long_description = fh.read() -requirements = [requirement.strip() for requirement in open("requirements.txt").readlines()] +with Path("requirements.txt").open(encoding="utf-8") as fh: + requirements = [requirement.strip() for requirement in fh] info = { "name": "graphix_ibmq", - "version": version["__version__"], + "version": __version__, "packages": find_packages(), "author": "Daichi Sasaki, Shinichi Sunami", "author_email": "shinichi.sunami@gmail.com", 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_converter.py b/tests/test_converter.py index 7077cf2..1e6e066 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1,20 +1,16 @@ -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 from graphix_ibmq.converter import qiskit_to_graphix -class TestConverter(unittest.TestCase): - def test_qiskit_to_graphix(self): - qc = random_circuit(5, 2, seed=42) - qc = transpile(qc, basis_gates=["rx", "rz", "cx"]) - gx_qc = qiskit_to_graphix(qc) - sv = Statevector.from_instruction(qc) - sv = sv.reverse_qargs() - gx_sv = gx_qc.simulate_statevector() - gx_sv = Statevector(gx_sv.statevec.flatten()) - self.assertTrue(sv.equiv(gx_sv)) +def test_qiskit_to_graphix(): + qc = random_circuit(5, 2, seed=42) + qc = transpile(qc, basis_gates=["rx", "rz", "cx"]) + gx_qc = qiskit_to_graphix(qc) + sv = Statevector.from_instruction(qc) + sv = sv.reverse_qargs() + gx_sv = gx_qc.simulate_statevector() + gx_sv = Statevector(gx_sv.statevec.flatten()) + assert sv.equiv(gx_sv) diff --git a/tests/test_ibmq_interface.py b/tests/test_ibmq_interface.py index 7a2f26b..86a18f1 100644 --- a/tests/test_ibmq_interface.py +++ b/tests/test_ibmq_interface.py @@ -1,58 +1,52 @@ -import unittest - import numpy as np +from graphix.random_objects import rand_gate -import random_circuit as rc from graphix_ibmq.runner import IBMQBackend def modify_statevector(statevector, output_qubit): - N = round(np.log2(len(statevector))) + 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") + i_str = format(i, f"0{n}b") new_idx = "" for idx in output_qubit: - new_idx += i_str[N - idx - 1] + 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() +def test_to_qiskit() -> None: + nqubits = 5 + depth = 5 + pairs = [(i, np.mod(i + 1, nqubits)) for i in range(nqubits)] + circuit = rand_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() -> None: + nqubits = 2 + depth = 2 + pairs = [(i, np.mod(i + 1, nqubits)) for i in range(nqubits)] + circuit = rand_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)