From 0683e593068cbf960802ff95ddcb69c984e21f72 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Thu, 1 Feb 2024 15:02:07 +0100 Subject: [PATCH 01/18] =?UTF-8?q?=E2=9D=87=EF=B8=8F=20Add=20Layout=20Attri?= =?UTF-8?q?bute=20and=20improved=20action=20space=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR - makes sure, that always a valid `Qiskit.QuantumCircuit.layout` attribute is set after the compilation (even when it is compiled with non-qiskit compilation passes) - removes unnecessary sabre layout and routing actions (mapping is still in there) - improves the Qiskit O3 optimization action such that it does not synthesize and map automatically any more --- src/mqt/predictor/rl/Predictor.py | 1 - src/mqt/predictor/rl/PredictorEnv.py | 27 +++++- src/mqt/predictor/rl/helper.py | 122 +++++++++++++++++-------- tests/compilation/test_helper_rl.py | 33 +------ tests/compilation/test_predictor_rl.py | 10 +- 5 files changed, 112 insertions(+), 81 deletions(-) diff --git a/src/mqt/predictor/rl/Predictor.py b/src/mqt/predictor/rl/Predictor.py index ddc7e7c1a..145ad4883 100644 --- a/src/mqt/predictor/rl/Predictor.py +++ b/src/mqt/predictor/rl/Predictor.py @@ -62,7 +62,6 @@ def compile_as_predicted( action_item = self.env.action_set[action] used_compilation_passes.append(action_item["name"]) obs, reward_val, terminated, truncated, info = self.env.step(action) - self.env.state._layout = self.env.layout # noqa: SLF001 if not self.env.error_occured: return self.env.state, used_compilation_passes diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index 26e752651..3fcfc7754 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -9,6 +9,7 @@ import numpy as np from gymnasium import Env from gymnasium.spaces import Box, Dict, Discrete +from pytket.circuit import Qubit from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit from qiskit import QuantumCircuit from qiskit.transpiler import CouplingMap, PassManager @@ -67,6 +68,7 @@ def __init__( self.action_space = Discrete(len(self.action_set.keys())) self.num_steps = 0 self.layout = None + self.num_qubits_uncompiled_circuit = 0 spaces = { "num_qubits": Discrete(128), @@ -112,6 +114,7 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any if self.state.count_ops().get("unitary"): self.state = self.state.decompose(gates_to_decompose="unitary") + self.state._layout = self.layout # noqa: SLF001 obs = rl.helper.create_feature_dict(self.state) return obs, reward_val, done, False, {} @@ -161,6 +164,8 @@ def reset( self.valid_actions = self.actions_opt_indices + self.actions_synthesis_indices self.error_occured = False + + self.num_qubits_uncompiled_circuit = self.state.num_qubits return rl.helper.create_feature_dict(self.state), {} def action_masks(self) -> list[bool]: @@ -186,8 +191,12 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: try: if action["name"] == "QiskitO3": pm = PassManager() + pm.append( - action["transpile_pass"](self.device["native_gates"], CouplingMap(self.device["cmap"])), + action["transpile_pass"]( + self.device["native_gates"], + CouplingMap(self.device["cmap"]) if self.layout is not None else None, # type: ignore[redundant-expr] + ), do_while=action["do_while"], ) else: @@ -208,14 +217,24 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: initial_layout=pm.property_set["layout"], input_qubit_mapping=pm.property_set["original_qubit_indices"], final_layout=pm.property_set["final_layout"], + _output_qubit_list=altered_qc.qubits, + _input_qubit_count=self.num_qubits_uncompiled_circuit, ) + elif action_index in self.actions_routing_indices: + self.layout.final_layout = pm.property_set["final_layout"] # type: ignore[attr-defined] elif action["origin"] == "tket": try: - tket_qc = qiskit_to_tk(self.state) + tket_qc = qiskit_to_tk(self.state, preserve_param_uuid=True) for elem in transpile_pass: elem.apply(tket_qc) + qbs = tket_qc.qubits + qubit_map = {qbs[i]: Qubit("q", i) for i in range(len(qbs))} + tket_qc.rename_units(qubit_map) # type: ignore[arg-type] altered_qc = tk_to_qiskit(tket_qc) + if action_index in self.actions_routing_indices: + self.layout.final_layout = rl.helper.final_layout_pytket_to_qiskit(tket_qc) # type: ignore[attr-defined] + except Exception: logger.exception( "Error in executing TKET transpile pass for {action} at step {i} for {filename}".format( @@ -251,8 +270,8 @@ def determine_valid_actions_for_state(self) -> list[int]: if mapped and self.layout is not None: return [self.action_terminate_index, *self.actions_opt_indices] # type: ignore[unreachable] - if self.state._layout is not None: # noqa: SLF001 - return self.actions_routing_indices + if self.layout is not None: + return self.actions_routing_indices # type: ignore[unreachable] # No layout applied yet return self.actions_mapping_indices + self.actions_layout_indices + self.actions_opt_indices diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index c2fc1885b..3ee8a70f0 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -44,14 +44,11 @@ OptimizeCliffords, RemoveDiagonalGatesBeforeMeasure, SabreLayout, - SabreSwap, Size, StochasticSwap, TrivialLayout, UnitarySynthesis, ) -from qiskit.transpiler.preset_passmanagers import common -from qiskit.transpiler.runningpassmanager import ConditionalController from sb3_contrib import MaskablePPO from tqdm import tqdm @@ -68,20 +65,14 @@ import importlib_metadata as metadata import importlib_resources as resources +from qiskit import QuantumRegister +from qiskit.transpiler.layout import Layout +from qiskit.transpiler.preset_passmanagers import common +from qiskit.transpiler.runningpassmanager import ConditionalController logger = logging.getLogger("mqt-predictor") -NUM_ACTIONS_OPT = 13 -NUM_ACTIONS_LAYOUT = 3 -NUM_ACTIONS_ROUTING = 4 -NUM_ACTIONS_SYNTHESIS = 1 -NUM_ACTIONS_TERMINATE = 1 -NUM_ACTIONS_DEVICES = 7 -NUM_ACTIONS_MAPPING = 1 -NUM_FEATURE_VECTOR_ELEMENTS = 7 - - def qcompile( qc: QuantumCircuit | str, figure_of_merit: reward.figure_of_merit | None = "expected_fidelity", @@ -179,18 +170,18 @@ def get_actions_opt() -> list[dict[str, Any]]: }, { "name": "QiskitO3", - "transpile_pass": lambda bgates, cmap: [ + "transpile_pass": lambda native_gate, coupling_map: [ Collect2qBlocks(), - ConsolidateBlocks(basis_gates=bgates), - UnitarySynthesis(basis_gates=bgates, coupling_map=cmap), - Optimize1qGatesDecomposition(basis=bgates), - CommutativeCancellation(basis_gates=bgates), - GatesInBasis(bgates), + ConsolidateBlocks(basis_gates=native_gate), + UnitarySynthesis(basis_gates=native_gate, coupling_map=coupling_map), + Optimize1qGatesDecomposition(basis=native_gate), + CommutativeCancellation(basis_gates=native_gate), + GatesInBasis(native_gate), ConditionalController( [ pass_ for x in common.generate_translation_passmanager( - target=None, basis_gates=bgates, coupling_map=cmap + target=None, basis_gates=native_gate, coupling_map=coupling_map ).passes() for pass_ in x["passes"] ], @@ -231,16 +222,6 @@ def get_actions_layout() -> list[dict[str, Any]]: ], "origin": "qiskit", }, - { - "name": "SabreLayout", - "transpile_pass": lambda c: [ - SabreLayout(coupling_map=CouplingMap(c), skip_routing=True), - FullAncillaAllocation(coupling_map=CouplingMap(c)), - EnlargeWithAncilla(), - ApplyLayout(), - ], - "origin": "qiskit", - }, ] @@ -265,11 +246,6 @@ def get_actions_routing() -> list[dict[str, Any]]: "transpile_pass": lambda c: [StochasticSwap(coupling_map=CouplingMap(c))], "origin": "qiskit", }, - { - "name": "SabreSwap", - "transpile_pass": lambda c: [SabreSwap(coupling_map=CouplingMap(c))], - "origin": "qiskit", - }, ] @@ -525,10 +501,59 @@ def handle_downloading_model(download_url: str, model_name: str) -> None: class PreProcessTKETRoutingAfterQiskitLayout: """ - Pre-processing step to route a circuit with tket after a Qiskit Layout pass has been applied. - The reason why we can apply the trivial layout here is that the circuit is already mapped by qiskit to the - device qubits and its qubits are sorted by their ascending physical qubit indices. - The trivial layout indices that this layout of the physical qubits is the identity mapping. + Pre-processing step to route a circuit with TKET after a Qiskit Layout pass has been applied. + The reason why we can apply the trivial layout here is that the circuit already got assigned a layout by qiskit. + Implicitly, Qiskit is reordering its qubits in a sequential manner, i.e., the qubit with the lowest *physical* qubit + first. + + Assuming, the layouted circuit is given by + + ┌───┐ ░ ┌─┐ + q_2 -> 0 ┤ H ├──■────────░───────┤M├ + └───┘┌─┴─┐ ░ ┌─┐└╥┘ + q_1 -> 1 ─────┤ X ├──■───░────┤M├─╫─ + └───┘┌─┴─┐ ░ ┌─┐└╥┘ ║ + q_0 -> 2 ──────────┤ X ├─░─┤M├─╫──╫─ + └───┘ ░ └╥┘ ║ ║ + ancilla_0 -> 3 ───────────────────╫──╫──╫─ + ║ ║ ║ + ancilla_1 -> 4 ───────────────────╫──╫──╫─ + ║ ║ ║ + meas: 3/═══════════════════╩══╩══╩═ + 0 1 2 + + Applying the trivial layout, we get the same qubit order as in the original circuit and can be respectively + routed. This results int: + ┌───┐ ░ ┌─┐ + q_0: ┤ H ├──■────────░───────┤M├ + └───┘┌─┴─┐ ░ ┌─┐└╥┘ + q_1: ─────┤ X ├──■───░────┤M├─╫─ + └───┘┌─┴─┐ ░ ┌─┐└╥┘ ║ + q_2: ──────────┤ X ├─░─┤M├─╫──╫─ + └───┘ ░ └╥┘ ║ ║ + q_3: ───────────────────╫──╫──╫─ + ║ ║ ║ + q_4: ───────────────────╫──╫──╫─ + ║ ║ ║ + meas: 3/═══════════════════╩══╩══╩═ + 0 1 2 + + + If we would not apply the trivial layout, no layout would be considered resulting, e.g., in the followiong circuit: + ┌───┐ ░ ┌─┐ + q_0: ─────┤ X ├─────■───░────┤M├─── + ┌───┐└─┬─┘ ┌─┴─┐ ░ ┌─┐└╥┘ + q_1: ┤ H ├──■───X─┤ X ├─░─┤M├─╫──── + └───┘ │ └───┘ ░ └╥┘ ║ ┌─┐ + q_2: ───────────X───────░──╫──╫─┤M├ + ░ ║ ║ └╥┘ + q_3: ──────────────────────╫──╫──╫─ + ║ ║ ║ + q_4: ──────────────────────╫──╫──╫─ + ║ ║ ║ + meas: 3/══════════════════════╩══╩══╩═ + 0 1 2 + """ def apply(self, circuit: Circuit) -> None: @@ -571,3 +596,22 @@ def get_device_index_of_device(device_name: str) -> int: msg = "No suitable device found." raise RuntimeError(msg) + + +def final_layout_pytket_to_qiskit(pytket_circuit: Circuit) -> Layout: + pytket_layout = pytket_circuit.qubit_readout + size_circuit = pytket_circuit.n_qubits + qiskit_layout = Layout() + qiskit_qreg = QuantumRegister(size_circuit, "q") + + pytket_layout = dict(sorted(pytket_layout.items(), key=lambda item: item[1])) + + for node, qubit_index in pytket_layout.items(): + new_index = node.index[0] + qiskit_layout[new_index] = qiskit_qreg[qubit_index] + + for i in range(size_circuit): + if i not in set(pytket_layout.values()): + qiskit_layout[i] = qiskit_qreg[i] + + return qiskit_layout diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index d23bfa69d..53513705b 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -2,35 +2,14 @@ from pathlib import Path +import numpy as np import pytest from mqt.bench import get_benchmark from mqt.predictor import rl -def test_get_actions_opt() -> None: - assert len(rl.helper.get_actions_opt()) == rl.helper.NUM_ACTIONS_OPT - - -def test_get_actions_layout() -> None: - assert len(rl.helper.get_actions_layout()) == rl.helper.NUM_ACTIONS_LAYOUT - - -def test_et_actions_routing() -> None: - assert len(rl.helper.get_actions_routing()) == rl.helper.NUM_ACTIONS_ROUTING - - -def test_get_actions_synthesis() -> None: - assert len(rl.helper.get_actions_synthesis()) == rl.helper.NUM_ACTIONS_SYNTHESIS - - -def test_get_action_terminate() -> None: - assert len(rl.helper.get_action_terminate()) == rl.helper.NUM_ACTIONS_TERMINATE - - -def test_get_actions_devices() -> None: - assert len(rl.helper.get_devices()) == rl.helper.NUM_ACTIONS_DEVICES - +def test_get_device_false_input() -> None: with pytest.raises(RuntimeError): rl.helper.get_device("false_input") @@ -40,10 +19,6 @@ def test_get_device_index_of_device_false_input() -> None: rl.helper.get_device_index_of_device("false_input") -def test_get_actions_mapping() -> None: - assert len(rl.helper.get_actions_mapping()) == rl.helper.NUM_ACTIONS_MAPPING - - @pytest.mark.parametrize( "device", ["ibm_washington", "ibm_montreal", "rigetti_aspen_m2", "oqc_lucy", "ionq_harmony", "ionq_aria1", "quantinuum_h2"], @@ -55,8 +30,8 @@ def test_get_device(device: str) -> None: def test_create_feature_dict() -> None: qc = get_benchmark("dj", 1, 5) features = rl.helper.create_feature_dict(qc) - assert features - assert len(features) == rl.helper.NUM_FEATURE_VECTOR_ELEMENTS + for feature in features.values(): + assert isinstance(feature, (np.ndarray, int)) def test_get_path_trained_model() -> None: diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 35fe94970..179cc07dc 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -1,15 +1,11 @@ from __future__ import annotations -import os - import pytest from qiskit import QuantumCircuit from mqt.bench import get_benchmark from mqt.predictor import reward, rl -IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" - @pytest.mark.parametrize( "figure_of_merit", @@ -28,10 +24,7 @@ def test_qcompile_with_pretrained_models(figure_of_merit: reward.figure_of_merit ) def test_qcompile_with_newly_trained_models(figure_of_merit: reward.figure_of_merit) -> None: predictor = rl.Predictor(figure_of_merit=figure_of_merit, device_name="ionq_harmony") - predictor.train_model( - timesteps=100, - test=True, - ) + predictor.train_model(timesteps=200, test=True) qc = get_benchmark("ghz", 1, 5) res = rl.qcompile(qc, figure_of_merit=figure_of_merit, device_name="ionq_harmony") @@ -39,6 +32,7 @@ def test_qcompile_with_newly_trained_models(figure_of_merit: reward.figure_of_me qc_compiled, compilation_information = res assert isinstance(qc_compiled, QuantumCircuit) + assert qc_compiled.layout is not None assert compilation_information is not None From 80cdd56e0329c11f8c9475db2bb0234afb0269cc Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 5 Feb 2024 13:02:14 +0100 Subject: [PATCH 02/18] =?UTF-8?q?=F0=9F=8E=A8=20=E2=9C=A8=20=20Improvement?= =?UTF-8?q?=20of=20Action=20Space:=20BQSKit=20and=20Qiskit=20O3=20Optimiza?= =?UTF-8?q?tion=20(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To further enhance MQT Predictor by additional supported quantum SDKs, [BQSKit](https://github.com/BQSKit/bqskit) has been integrated as an optimization action, synthesis action, and mapping action. Additionally, the action structure and how it is called has been improved. Furthermore, the seeding in the used `gymnasium` environment has been improved. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lukas Burgholzer --- pyproject.toml | 5 +- src/mqt/predictor/rl/Predictor.py | 8 +- src/mqt/predictor/rl/PredictorEnv.py | 67 ++++-- src/mqt/predictor/rl/helper.py | 136 ++++++++++-- .../test_integration_further_SDKs.py | 200 ++++++++++++++++++ 5 files changed, 372 insertions(+), 44 deletions(-) create mode 100644 tests/compilation/test_integration_further_SDKs.py diff --git a/pyproject.toml b/pyproject.toml index a473a211a..db6470893 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,8 @@ dependencies = [ "scikit-learn>=1.3.0, <1.3.3", "importlib_metadata>=4.4; python_version < '3.10'", "importlib_resources>=5.0; python_version < '3.10'", - "tensorboard>=2.11.0" + "tensorboard>=2.11.0", + "bqskit>=1.1.0, <1.2.0", ] classifiers = [ @@ -99,7 +100,7 @@ implicit_reexport = true # recent versions of `gym` are typed, but stable-baselines3 pins a very old version of gym. # qiskit is not yet marked as typed, but is typed mostly. # the other libraries do not have type stubs. -module = ["qiskit.*", "joblib.*", "sklearn.*", "matplotlib.*", "gymnasium.*", "mqt.bench.*", "sb3_contrib.*"] +module = ["qiskit.*", "joblib.*", "sklearn.*", "matplotlib.*", "gymnasium.*", "mqt.bench.*", "sb3_contrib.*", "bqskit.*"] ignore_missing_imports = true [tool.ruff] diff --git a/src/mqt/predictor/rl/Predictor.py b/src/mqt/predictor/rl/Predictor.py index 145ad4883..43c23f7e4 100644 --- a/src/mqt/predictor/rl/Predictor.py +++ b/src/mqt/predictor/rl/Predictor.py @@ -23,7 +23,7 @@ def __init__(self, figure_of_merit: reward.figure_of_merit, device_name: str, lo logger.setLevel(logger_level) self.model = None - self.env = rl.PredictorEnv(figure_of_merit, device_name) + self.env = rl.PredictorEnv(reward_function=figure_of_merit, device_name=device_name) self.device_name = device_name self.figure_of_merit = figure_of_merit @@ -50,7 +50,7 @@ def compile_as_predicted( raise RuntimeError(msg) from e assert self.model - obs, _ = self.env.reset(qc) # type: ignore[unreachable] + obs, _ = self.env.reset(qc, seed=0) # type: ignore[unreachable] used_compilation_passes = [] terminated = False @@ -93,11 +93,9 @@ def train_model( progress_bar = True logger.debug("Start training for: " + self.figure_of_merit + " on " + self.device_name) - env = rl.PredictorEnv(reward_function=self.figure_of_merit, device_name=self.device_name) - model = MaskablePPO( MaskableMultiInputActorCriticPolicy, - env, + self.env, verbose=verbose, tensorboard_log="./" + model_name + "_" + self.figure_of_merit + "_" + self.device_name, gamma=0.98, diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index 3fcfc7754..b105236f8 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -7,6 +7,7 @@ from pathlib import Path import numpy as np +from bqskit.ext import bqskit_to_qiskit, qiskit_to_bqskit from gymnasium import Env from gymnasium.spaces import Box, Dict, Discrete from pytket.circuit import Qubit @@ -67,9 +68,11 @@ def __init__( self.reward_function = reward_function self.action_space = Discrete(len(self.action_set.keys())) self.num_steps = 0 - self.layout = None + self.layout: TranspileLayout | None = None self.num_qubits_uncompiled_circuit = 0 + self.has_parametrized_gates = False + spaces = { "num_qubits": Discrete(128), "depth": Discrete(1000000), @@ -166,11 +169,19 @@ def reset( self.error_occured = False self.num_qubits_uncompiled_circuit = self.state.num_qubits + self.has_parametrized_gates = len(self.state.parameters) > 0 return rl.helper.create_feature_dict(self.state), {} def action_masks(self) -> list[bool]: """Returns a list of valid actions for the current state.""" - return [action in self.valid_actions for action in self.action_set] + action_mask = [action in self.valid_actions for action in self.action_set] + if self.has_parametrized_gates or self.layout is not None: + # remove all actions that are from "origin"=="bqskit" because they are not supported for parametrized gates + # or after layout since using BQSKit after a layout is set may result in an error + action_mask = [ + action_mask[i] and self.action_set[i].get("origin") != "bqskit" for i in range(len(action_mask)) + ] + return action_mask def apply_action(self, action_index: int) -> QuantumCircuit | None: """Applies the given action to the current state and returns the altered state.""" @@ -178,24 +189,19 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: action = self.action_set[action_index] if action["name"] == "terminate": return self.state - if ( - action_index - in self.actions_layout_indices + self.actions_routing_indices + self.actions_mapping_indices - ): - transpile_pass = action["transpile_pass"](self.device["cmap"]) - elif action_index in self.actions_synthesis_indices: - transpile_pass = action["transpile_pass"](self.device["native_gates"]) - else: + if action_index in self.actions_opt_indices: transpile_pass = action["transpile_pass"] + else: + transpile_pass = action["transpile_pass"](self.device) + if action["origin"] == "qiskit": try: if action["name"] == "QiskitO3": pm = PassManager() - pm.append( action["transpile_pass"]( self.device["native_gates"], - CouplingMap(self.device["cmap"]) if self.layout is not None else None, # type: ignore[redundant-expr] + CouplingMap(self.device["cmap"]) if self.layout is not None else None, ), do_while=action["do_while"], ) @@ -221,7 +227,8 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: _input_qubit_count=self.num_qubits_uncompiled_circuit, ) elif action_index in self.actions_routing_indices: - self.layout.final_layout = pm.property_set["final_layout"] # type: ignore[attr-defined] + assert self.layout is not None + self.layout.final_layout = pm.property_set["final_layout"] elif action["origin"] == "tket": try: @@ -233,7 +240,8 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: tket_qc.rename_units(qubit_map) # type: ignore[arg-type] altered_qc = tk_to_qiskit(tket_qc) if action_index in self.actions_routing_indices: - self.layout.final_layout = rl.helper.final_layout_pytket_to_qiskit(tket_qc) # type: ignore[attr-defined] + assert self.layout is not None + self.layout.final_layout = rl.helper.final_layout_pytket_to_qiskit(tket_qc, altered_qc) except Exception: logger.exception( @@ -244,6 +252,28 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: self.error_occured = True return None + elif action["origin"] == "bqskit": + try: + bqskit_qc = qiskit_to_bqskit(self.state) + if action_index in self.actions_opt_indices + self.actions_synthesis_indices: + bqskit_compiled_qc = transpile_pass(bqskit_qc) + altered_qc = bqskit_to_qiskit(bqskit_compiled_qc) + elif action_index in self.actions_mapping_indices: + bqskit_compiled_qc, initial_layout, final_layout = transpile_pass(bqskit_qc) + altered_qc = bqskit_to_qiskit(bqskit_compiled_qc) + layout = rl.helper.final_layout_bqskit_to_qiskit( + initial_layout, final_layout, altered_qc, self.state + ) + self.layout = layout + except Exception: + logger.exception( + "Error in executing BQSKit transpile pass for {action} at step {i} for {filename}".format( + action=action["name"], i=self.num_steps, filename=self.filename + ) + ) + self.error_occured = True + return None + else: error_msg = f"Origin {action['origin']} not supported." raise ValueError(error_msg) @@ -261,17 +291,20 @@ def determine_valid_actions_for_state(self) -> list[int]: only_nat_gates = check_nat_gates.property_set["all_gates_in_basis"] if not only_nat_gates: - return self.actions_synthesis_indices + self.actions_opt_indices + actions = self.actions_synthesis_indices + self.actions_opt_indices + if self.layout is not None: + actions += self.actions_routing_indices + return actions check_mapping = CheckMap(coupling_map=CouplingMap(self.device["cmap"])) check_mapping(self.state) mapped = check_mapping.property_set["is_swap_mapped"] if mapped and self.layout is not None: - return [self.action_terminate_index, *self.actions_opt_indices] # type: ignore[unreachable] + return [self.action_terminate_index, *self.actions_opt_indices] if self.layout is not None: - return self.actions_routing_indices # type: ignore[unreachable] + return self.actions_routing_indices # No layout applied yet return self.actions_mapping_indices + self.actions_layout_indices + self.actions_opt_indices diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index 3ee8a70f0..af1fb9bd7 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -8,6 +8,7 @@ import numpy as np import requests +from bqskit import MachineModel from packaging import version from pytket.architecture import Architecture from pytket.circuit import Circuit, Node, Qubit @@ -22,7 +23,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.equivalence_library import StandardEquivalenceLibrary from qiskit.circuit.library import XGate, ZGate -from qiskit.transpiler import CouplingMap +from qiskit.transpiler import CouplingMap, TranspileLayout from qiskit.transpiler.passes import ( ApplyLayout, BasicSwap, @@ -65,6 +66,8 @@ import importlib_metadata as metadata import importlib_resources as resources +from bqskit import compile as bqskit_compile +from bqskit.ir import gates from qiskit import QuantumRegister from qiskit.transpiler.layout import Layout from qiskit.transpiler.preset_passmanagers import common @@ -196,6 +199,11 @@ def get_actions_opt() -> list[dict[str, Any]]: "origin": "qiskit", "do_while": lambda property_set: (not property_set["optimization_loop_minimum_point"]), }, + { + "name": "BQSKitO2", + "transpile_pass": lambda circuit: bqskit_compile(circuit, optimization_level=2), + "origin": "bqskit", + }, ] @@ -204,9 +212,9 @@ def get_actions_layout() -> list[dict[str, Any]]: return [ { "name": "TrivialLayout", - "transpile_pass": lambda c: [ - TrivialLayout(coupling_map=CouplingMap(c)), - FullAncillaAllocation(coupling_map=CouplingMap(c)), + "transpile_pass": lambda device: [ + TrivialLayout(coupling_map=CouplingMap(device["cmap"])), + FullAncillaAllocation(coupling_map=CouplingMap(device["cmap"])), EnlargeWithAncilla(), ApplyLayout(), ], @@ -214,9 +222,9 @@ def get_actions_layout() -> list[dict[str, Any]]: }, { "name": "DenseLayout", - "transpile_pass": lambda c: [ - DenseLayout(coupling_map=CouplingMap(c)), - FullAncillaAllocation(coupling_map=CouplingMap(c)), + "transpile_pass": lambda device: [ + DenseLayout(coupling_map=CouplingMap(device["cmap"])), + FullAncillaAllocation(coupling_map=CouplingMap(device["cmap"])), EnlargeWithAncilla(), ApplyLayout(), ], @@ -230,20 +238,20 @@ def get_actions_routing() -> list[dict[str, Any]]: return [ { "name": "BasicSwap", - "transpile_pass": lambda c: [BasicSwap(coupling_map=CouplingMap(c))], + "transpile_pass": lambda device: [BasicSwap(coupling_map=CouplingMap(device["cmap"]))], "origin": "qiskit", }, { "name": "RoutingPass", - "transpile_pass": lambda c: [ + "transpile_pass": lambda device: [ PreProcessTKETRoutingAfterQiskitLayout(), - RoutingPass(Architecture(c)), + RoutingPass(Architecture(device["cmap"])), ], "origin": "tket", }, { "name": "StochasticSwap", - "transpile_pass": lambda c: [StochasticSwap(coupling_map=CouplingMap(c))], + "transpile_pass": lambda device: [StochasticSwap(coupling_map=CouplingMap(device["cmap"]))], "origin": "qiskit", }, ] @@ -254,11 +262,25 @@ def get_actions_mapping() -> list[dict[str, Any]]: return [ { "name": "SabreMapping", - "transpile_pass": lambda c: [ - SabreLayout(coupling_map=CouplingMap(c), skip_routing=False), + "transpile_pass": lambda device: [ + SabreLayout(coupling_map=CouplingMap(device["cmap"]), skip_routing=False), ], "origin": "qiskit", }, + { + "name": "BQSKitMapping", + "transpile_pass": lambda device: lambda bqskit_circuit: bqskit_compile( + bqskit_circuit, + model=MachineModel( + num_qudits=device["max_qubits"], + gate_set=get_BQSKit_native_gates(device), + coupling_graph=[(elem[0], elem[1]) for elem in device["cmap"]], + ), + with_mapping=True, + optimization_level=2, + ), + "origin": "bqskit", + }, ] @@ -267,9 +289,20 @@ def get_actions_synthesis() -> list[dict[str, Any]]: return [ { "name": "BasisTranslator", - "transpile_pass": lambda g: [BasisTranslator(StandardEquivalenceLibrary, target_basis=g)], + "transpile_pass": lambda device: [ + BasisTranslator(StandardEquivalenceLibrary, target_basis=device["native_gates"]) + ], "origin": "qiskit", }, + { + "name": "BQSKitSynthesis", + "transpile_pass": lambda device: lambda bqskit_circuit: bqskit_compile( + bqskit_circuit, + model=MachineModel(bqskit_circuit.num_qudits, gate_set=get_BQSKit_native_gates(device)), + optimization_level=2, + ), + "origin": "bqskit", + }, ] @@ -598,20 +631,83 @@ def get_device_index_of_device(device_name: str) -> int: raise RuntimeError(msg) -def final_layout_pytket_to_qiskit(pytket_circuit: Circuit) -> Layout: +def get_BQSKit_native_gates(device: dict[str, Any]) -> list[gates.Gate] | None: + """Returns the native gates of the given device. + + Args: + device: The device for which the native gates are returned. + + Returns: + list[gates.Gate]: The native gates of the given provider. + """ + provider = device["name"].split("_")[0] + + native_gatesets = { + "ibm": [gates.RZGate(), gates.SXGate(), gates.XGate(), gates.CNOTGate()], + "rigetti": [gates.RXGate(), gates.RZGate(), gates.CZGate()], + "ionq": [gates.RXXGate(), gates.RZGate(), gates.RYGate(), gates.RXGate()], + "quantinuum": [gates.RZZGate(), gates.RZGate(), gates.RYGate(), gates.RXGate()], + } + + if provider not in native_gatesets: + logger.warning("No native gateset for provider " + provider + " found. No native gateset is used.") + return None + + return native_gatesets[provider] + + +def final_layout_pytket_to_qiskit(pytket_circuit: Circuit, qiskit_ciruit: QuantumCircuit) -> Layout: pytket_layout = pytket_circuit.qubit_readout size_circuit = pytket_circuit.n_qubits - qiskit_layout = Layout() - qiskit_qreg = QuantumRegister(size_circuit, "q") + qiskit_layout = {} + qiskit_qreg = qiskit_ciruit.qregs[0] pytket_layout = dict(sorted(pytket_layout.items(), key=lambda item: item[1])) for node, qubit_index in pytket_layout.items(): - new_index = node.index[0] - qiskit_layout[new_index] = qiskit_qreg[qubit_index] + qiskit_layout[node.index[0]] = qiskit_qreg[qubit_index] for i in range(size_circuit): if i not in set(pytket_layout.values()): qiskit_layout[i] = qiskit_qreg[i] - return qiskit_layout + return Layout(input_dict=qiskit_layout) + + +def final_layout_bqskit_to_qiskit( + bqskit_initial_layout: list[int], + bqskit_final_layout: list[int], + compiled_qc: QuantumCircuit, + initial_qc: QuantumCircuit, +) -> TranspileLayout: + # BQSKit provides an initial layout as a list[int] where each virtual qubit is mapped to a physical qubit + # similarly, it provides a final layout as a list[int] representing where each virtual qubit is mapped to at the end + # of the circuit + + ancilla = QuantumRegister(compiled_qc.num_qubits - initial_qc.num_qubits, "ancilla") + qiskit_initial_layout = {} + for i in range(compiled_qc.num_qubits): + if i in bqskit_initial_layout: + qiskit_initial_layout[i] = initial_qc.qubits[bqskit_initial_layout.index(i)] + else: + qiskit_initial_layout[i] = ancilla[i - initial_qc.num_qubits] + + initial_qubit_mapping = {bit: index for index, bit in enumerate(compiled_qc.qubits)} + + if bqskit_initial_layout == bqskit_final_layout: + qiskit_final_layout = None + else: + qiskit_final_layout = {} + for i in range(compiled_qc.num_qubits): + if i in bqskit_final_layout: + qiskit_final_layout[i] = compiled_qc.qubits[bqskit_initial_layout[bqskit_final_layout.index(i)]] + else: + qiskit_final_layout[i] = compiled_qc.qubits[i] + + return TranspileLayout( + initial_layout=Layout(input_dict=qiskit_initial_layout), + input_qubit_mapping=initial_qubit_mapping, + final_layout=Layout(input_dict=qiskit_final_layout) if qiskit_final_layout else None, + _output_qubit_list=compiled_qc.qubits, + _input_qubit_count=initial_qc.num_qubits, + ) diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py new file mode 100644 index 000000000..704f6e6b1 --- /dev/null +++ b/tests/compilation/test_integration_further_SDKs.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +from typing import Any, cast + +import pytest +from bqskit.ext import bqskit_to_qiskit, qiskit_to_bqskit +from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit +from qiskit import QuantumCircuit +from qiskit.transpiler import CouplingMap, PassManager +from qiskit.transpiler.passes import CheckMap, GatesInBasis +from qiskit.transpiler.runningpassmanager import TranspileLayout + +from mqt.predictor.rl import helper + + +def test_BQSKitO2_action() -> None: + """Test the BQSKitO2 action.""" + action_BQSKitO2 = None + for action in helper.get_actions_opt(): + if action["name"] == "BQSKitO2": + action_BQSKitO2 = action + + assert action_BQSKitO2 is not None + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + + bqskit_qc = qiskit_to_bqskit(qc) + optimized_qc = bqskit_to_qiskit(action_BQSKitO2["transpile_pass"](bqskit_qc)) + + assert optimized_qc != qc + + +@pytest.mark.parametrize("device", helper.get_devices(), ids=lambda device: cast(str, device["name"])) +def test_BQSKitSynthesis_action(device: dict[str, Any]) -> None: + """Test the BQSKitSynthesis action for all devices.""" + action_BQSKitSynthesis = None + for action in helper.get_actions_synthesis(): + if action["name"] == "BQSKitSynthesis": + action_BQSKitSynthesis = action + + assert action_BQSKitSynthesis is not None + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + + check_nat_gates = GatesInBasis(basis_gates=device["native_gates"]) + check_nat_gates(qc) + assert not check_nat_gates.property_set["all_gates_in_basis"] + + transpile_pass = action_BQSKitSynthesis["transpile_pass"](device) + bqskit_qc = qiskit_to_bqskit(qc) + native_gates_qc = bqskit_to_qiskit(transpile_pass(bqskit_qc)) + + check_nat_gates = GatesInBasis(basis_gates=device["native_gates"]) + check_nat_gates(native_gates_qc) + only_nat_gates = check_nat_gates.property_set["all_gates_in_basis"] + if "oqc" not in device["name"]: + assert only_nat_gates + + +def test_BQSKitMapping_action_swaps_necessary() -> None: + """Test the BQSKitMapping action for quantum circuit that requires SWAP gates.""" + action_BQSKitMapping = None + for action in helper.get_actions_mapping(): + if action["name"] == "BQSKitMapping": + action_BQSKitMapping = action + + assert action_BQSKitMapping is not None + + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + + device = helper.get_devices()[1] + bqskit_qc = qiskit_to_bqskit(qc) + bqskit_qc_mapped, input_mapping, output_mapping = action_BQSKitMapping["transpile_pass"](device)(bqskit_qc) + mapped_qc = bqskit_to_qiskit(bqskit_qc_mapped) + layout = helper.final_layout_bqskit_to_qiskit(input_mapping, output_mapping, mapped_qc, qc) + + assert input_mapping != output_mapping + assert layout.final_layout is not None + check_mapped_circuit(initial_qc=qc, mapped_qc=mapped_qc, device=device, layout=layout) + + +def check_mapped_circuit( + initial_qc: QuantumCircuit, mapped_qc: QuantumCircuit, device: dict[str, Any], layout: TranspileLayout +) -> None: + # check if the altered circuit is correctly mapped to the device + check_mapping = CheckMap(coupling_map=CouplingMap(device["cmap"])) + check_mapping(mapped_qc) + mapped = check_mapping.property_set["is_swap_mapped"] + assert mapped + assert mapped_qc != initial_qc + assert layout is not None + assert len(layout.initial_layout) == device["max_qubits"] + if layout.final_layout is not None: + assert len(layout.final_layout) == device["max_qubits"] + + # each qubit of the initial layout is part of the initial quantum circuit and the register name is correctly set + for assigned_physical_qubit in layout.initial_layout._p2v.values(): # noqa: SLF001 + qreg = assigned_physical_qubit.register + assert qreg.name in {"q", "ancilla"} + + # assigned_physical_qubit is part of the original quantum circuit + if qreg.name == "q": + assert qreg.size == initial_qc.num_qubits + # each qubit is also part of the initial uncompiled quantum circuit + assert initial_qc.find_bit(assigned_physical_qubit).registers[0][0].name == "q" + # assigned_physical_qubit is an ancilla qubit + else: + assert qreg.size == device["max_qubits"] - initial_qc.num_qubits + # each qubit of the final layout is part of the mapped quantum circuit and the register name is correctly set + if layout.final_layout is not None: + for assigned_physical_qubit in layout.final_layout._p2v.values(): # noqa: SLF001 + assert mapped_qc.find_bit(assigned_physical_qubit).registers[0][0].name == "q" + # each virtual qubit of the original quantum circuit is part of the initial layout + for virtual_qubit in initial_qc.qubits: + assert virtual_qubit in layout.initial_layout._p2v.values() # noqa: SLF001 + + +def test_BQSKitMapping_action_no_swaps_necessary() -> None: + """Test the BQSKitMapping action for a simple quantum circuit that does not require SWAP gates.""" + + action_BQSKitMapping = None + for action in helper.get_actions_mapping(): + if action["name"] == "BQSKitMapping": + action_BQSKitMapping = action + + assert action_BQSKitMapping is not None + + qc_no_swap_needed = QuantumCircuit(2) + qc_no_swap_needed.h(0) + qc_no_swap_needed.cx(0, 1) + + device = helper.get_devices()[1] + + bqskit_qc = qiskit_to_bqskit(qc_no_swap_needed) + bqskit_qc_mapped, input_mapping, output_mapping = action_BQSKitMapping["transpile_pass"](device)(bqskit_qc) + mapped_qc = bqskit_to_qiskit(bqskit_qc_mapped) + layout = helper.final_layout_bqskit_to_qiskit(input_mapping, output_mapping, mapped_qc, qc_no_swap_needed) + assert layout is not None + assert input_mapping == output_mapping + assert layout.final_layout is None + + check_mapped_circuit(qc_no_swap_needed, mapped_qc, device, layout) + + +def test_TKET_routing() -> None: + """Test the TKETRouting action.""" + + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + + device = helper.get_devices()[1] + + layout_action = helper.get_actions_layout()[0] + transpile_pass = layout_action["transpile_pass"](device) + pm = PassManager(transpile_pass) + layouted_qc = pm.run(qc) + initial_layout = pm.property_set["layout"] + input_qubit_mapping = pm.property_set["original_qubit_indices"] + + routing_action = None + for action in helper.get_actions_routing(): + if action["origin"] == "tket": + routing_action = action + assert routing_action is not None + + tket_qc = qiskit_to_tk(layouted_qc, preserve_param_uuid=True) + for elem in routing_action["transpile_pass"](device): + elem.apply(tket_qc) + from pytket.circuit import Qubit + + qbs = tket_qc.qubits + qubit_map = {qbs[i]: Qubit("q", i) for i in range(len(qbs))} + tket_qc.rename_units(qubit_map) # type: ignore[arg-type] + + mapped_qc = tk_to_qiskit(tket_qc) + + final_layout = helper.final_layout_pytket_to_qiskit(tket_qc, mapped_qc) + + layout = TranspileLayout( + initial_layout=initial_layout, + input_qubit_mapping=input_qubit_mapping, + final_layout=final_layout, + _output_qubit_list=mapped_qc.qubits, + _input_qubit_count=qc.num_qubits, + ) + + check_mapped_circuit(qc, mapped_qc, device, layout) From 03e3de3283d283adfb0642d3c08b46fe746d5542 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Fri, 16 Feb 2024 10:09:11 +0100 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=93=8C=20adjusted=20scikit=20learn?= =?UTF-8?q?=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index db6470893..8e4c7fe72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dynamic = ["version"] dependencies = [ "mqt.bench>=1.0.0,<1.1.0", "sb3_contrib>=2.0.0", - "scikit-learn>=1.3.0, <1.3.3", + "scikit-learn>=1.4.0,<1.4.2", "importlib_metadata>=4.4; python_version < '3.10'", "importlib_resources>=5.0; python_version < '3.10'", "tensorboard>=2.11.0", From a378936aaf946ef2bf5580f408906c00eeaee4eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:36:07 +0000 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/helper.py | 1 + tests/compilation/test_helper_rl.py | 2 +- tests/compilation/test_predictor_rl.py | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index b0e597a38..ea582e8b0 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -545,6 +545,7 @@ def apply(self, circuit: Circuit) -> None: mapping = {Qubit(i): Node(i) for i in range(circuit.n_qubits)} place_with_map(circuit=circuit, qmap=mapping) + def get_BQSKit_native_gates(device: dict[str, Any]) -> list[gates.Gate] | None: """Returns the native gates of the given device. diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 8a008d2d9..40d0b2669 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -31,7 +31,7 @@ def test_create_feature_dict() -> None: qc = get_benchmark("dj", 1, 5) features = rl.helper.create_feature_dict(qc) for feature in features.values(): - assert isinstance(feature, (np.ndarray, int)) + assert isinstance(feature, np.ndarray | int) def test_get_path_trained_model() -> None: diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index e24d1222c..c5c5cff69 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -1,7 +1,7 @@ from __future__ import annotations - from pathlib import Path + import pytest from qiskit import QuantumCircuit @@ -17,7 +17,6 @@ def test_predictor_env_reset_from_string() -> None: assert predictor.env.reset(qc=qasm_path)[0] == rl.helper.create_feature_dict(qc) - @pytest.mark.parametrize( "figure_of_merit", ["expected_fidelity", "critical_depth"], From ea55f00f1f216acbf05aa865786043311c7e5447 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 11:34:11 +0100 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=A8=20added=20VF2Layout+VF2PostLayo?= =?UTF-8?q?ut=20and=20adjusted=20errors=20due=20to=20merge=20from=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/ml/Predictor.py | 2 +- src/mqt/predictor/rl/PredictorEnv.py | 69 +++++++++++--- src/mqt/predictor/rl/helper.py | 90 ++++++++++++++++--- tests/compilation/test_helper_rl.py | 19 ---- .../test_integration_further_SDKs.py | 31 +++---- tests/compilation/test_pretrained_models.py | 33 +------ 6 files changed, 151 insertions(+), 93 deletions(-) diff --git a/src/mqt/predictor/ml/Predictor.py b/src/mqt/predictor/ml/Predictor.py index 02172391c..7c894c222 100644 --- a/src/mqt/predictor/ml/Predictor.py +++ b/src/mqt/predictor/ml/Predictor.py @@ -514,7 +514,7 @@ def generate_eval_all_datapoints( tmp_res = scores_filtered_sorted_accordingly[i] max_score = max(tmp_res) for j in range(len(tmp_res)): - plt.plot(i, tmp_res[j] / max_score, "b.", alpha=1.0, markersize=1.7, color=color_all) + plt.plot(i, tmp_res[j] / max_score, alpha=1.0, markersize=1.7, color=color_all) plt.plot( i, diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index b4560c7cc..a42a7e0ca 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -14,7 +14,7 @@ from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit from qiskit import QuantumCircuit from qiskit.transpiler import CouplingMap, PassManager -from qiskit.transpiler.passes import CheckMap, GatesInBasis +from qiskit.transpiler.passes import ApplyLayout, CheckMap, GatesInBasis from qiskit.transpiler.runningpassmanager import TranspileLayout from mqt.bench.devices import get_device_by_name @@ -37,6 +37,7 @@ def __init__( self.actions_routing_indices = [] self.actions_mapping_indices = [] self.actions_opt_indices = [] + self.actions_final_optimization_indices = [] self.used_actions: list[str] = [] self.device = get_device_by_name(device_name) @@ -62,6 +63,10 @@ def __init__( self.action_set[index] = elem self.actions_mapping_indices.append(index) index += 1 + for elem in rl.helper.get_actions_final_optimization(): + self.action_set[index] = elem + self.actions_final_optimization_indices.append(index) + index += 1 self.action_set[index] = rl.helper.get_action_terminate() self.action_terminate_index = index @@ -174,6 +179,13 @@ def reset( def action_masks(self) -> list[bool]: """Returns a list of valid actions for the current state.""" action_mask = [action in self.valid_actions for action in self.action_set] + + # it is not clear how tket will handle the layout, so we remove all actions that are from "origin"=="tket" if a layout is set + if self.layout is not None: + action_mask = [ + action_mask[i] and self.action_set[i].get("origin") != "tket" for i in range(len(action_mask)) + ] + if self.has_parametrized_gates or self.layout is not None: # remove all actions that are from "origin"=="bqskit" because they are not supported for parametrized gates # or after layout since using BQSKit after a layout is set may result in an error @@ -199,8 +211,8 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: pm = PassManager() pm.append( action["transpile_pass"]( - self.device["native_gates"], - CouplingMap(self.device["cmap"]) if self.layout is not None else None, + self.device.basis_gates, + CouplingMap(self.device.coupling_map) if self.layout is not None else None, ), do_while=action["do_while"], ) @@ -216,15 +228,46 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: self.error_occured = True return None - if action_index in self.actions_layout_indices + self.actions_mapping_indices: - assert pm.property_set["layout"] - self.layout = TranspileLayout( - initial_layout=pm.property_set["layout"], - input_qubit_mapping=pm.property_set["original_qubit_indices"], - final_layout=pm.property_set["final_layout"], - _output_qubit_list=altered_qc.qubits, - _input_qubit_count=self.num_qubits_uncompiled_circuit, - ) + if ( + action_index + in self.actions_layout_indices + + self.actions_mapping_indices + + self.actions_final_optimization_indices + ): + if action["name"] == "VF2Layout": + if pm.property_set["layout"]: + initial_layout = pm.property_set["layout"] + original_qubit_indices = pm.property_set["original_qubit_indices"] + final_layout = pm.property_set["final_layout"] + postprocessing_action = rl.helper.get_layout_postprocessing_qiskit_pass()(self.device) + pm = PassManager(postprocessing_action) + pm.property_set["layout"] = initial_layout + pm.property_set["original_qubit_indices"] = original_qubit_indices + pm.property_set["final_layout"] = final_layout + altered_qc = pm.run(altered_qc) + elif action["name"] == "VF2PostLayout": + assert pm.property_set["VF2PostLayout_stop_reason"] is not None + post_layout = pm.property_set["post_layout"] + if post_layout: + pm = PassManager(ApplyLayout()) + assert self.layout is not None + pm.property_set["layout"] = self.layout.initial_layout + pm.property_set["original_qubit_indices"] = self.layout.input_qubit_mapping + pm.property_set["final_layout"] = self.layout.final_layout + pm.property_set["post_layout"] = post_layout + altered_qc = pm.run(altered_qc) + else: + assert pm.property_set["layout"] + + if pm.property_set["layout"]: + self.layout = TranspileLayout( + initial_layout=pm.property_set["layout"], + input_qubit_mapping=pm.property_set["original_qubit_indices"], + final_layout=pm.property_set["final_layout"], + _output_qubit_list=altered_qc.qubits, + _input_qubit_count=self.num_qubits_uncompiled_circuit, + ) + elif action_index in self.actions_routing_indices: assert self.layout is not None self.layout.final_layout = pm.property_set["final_layout"] @@ -266,7 +309,7 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: self.layout = layout except Exception: logger.exception( - "Error in executing BQSKit transpile pass for {action} at step {i} for {filename}".format( + "Error in executing BQSKit transpile pass for {action} at step {i} for {filename}".format( action=action["name"], i=self.num_steps, filename=self.filename ) ) diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index ea582e8b0..f8422e088 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -49,6 +49,8 @@ StochasticSwap, TrivialLayout, UnitarySynthesis, + VF2Layout, + VF2PostLayout, ) from sb3_contrib import MaskablePPO from tqdm import tqdm @@ -57,8 +59,13 @@ from mqt.predictor import reward, rl if TYPE_CHECKING: + from collections.abc import Callable + from numpy.typing import NDArray + from mqt.bench.devices import Device + + if TYPE_CHECKING or sys.version_info >= (3, 10, 0): from importlib import metadata, resources else: @@ -68,6 +75,7 @@ from bqskit import compile as bqskit_compile from bqskit.ir import gates from qiskit import QuantumRegister +from qiskit.providers.fake_provider import FakeGuadalupe, FakeMontreal, FakeQuito, FakeWashington from qiskit.transpiler.layout import Layout from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.runningpassmanager import ConditionalController @@ -206,14 +214,28 @@ def get_actions_opt() -> list[dict[str, Any]]: ] +def get_actions_final_optimization() -> list[dict[str, Any]]: + """Returns a list of dictionaries containing information about the optimization passes that are available.""" + return [ + { + "name": "VF2PostLayout", + "transpile_pass": lambda device: VF2PostLayout( + coupling_map=CouplingMap(device.coupling_map), + properties=get_ibm_backend_properties_by_device_name(device.name), + ), + "origin": "qiskit", + } + ] + + def get_actions_layout() -> list[dict[str, Any]]: """Returns a list of dictionaries containing information about the layout passes that are available.""" return [ { "name": "TrivialLayout", "transpile_pass": lambda device: [ - TrivialLayout(coupling_map=CouplingMap(device["cmap"])), - FullAncillaAllocation(coupling_map=CouplingMap(device["cmap"])), + TrivialLayout(coupling_map=CouplingMap(device.coupling_map)), + FullAncillaAllocation(coupling_map=CouplingMap(device.coupling_map)), EnlargeWithAncilla(), ApplyLayout(), ], @@ -222,13 +244,23 @@ def get_actions_layout() -> list[dict[str, Any]]: { "name": "DenseLayout", "transpile_pass": lambda device: [ - DenseLayout(coupling_map=CouplingMap(device["cmap"])), - FullAncillaAllocation(coupling_map=CouplingMap(device["cmap"])), + DenseLayout(coupling_map=CouplingMap(device.coupling_map)), + FullAncillaAllocation(coupling_map=CouplingMap(device.coupling_map)), EnlargeWithAncilla(), ApplyLayout(), ], "origin": "qiskit", }, + { + "name": "VF2Layout", + "transpile_pass": lambda device: [ + VF2Layout( + coupling_map=CouplingMap(device.coupling_map), + properties=get_ibm_backend_properties_by_device_name(device.name), + ), + ], + "origin": "qiskit", + }, ] @@ -237,20 +269,20 @@ def get_actions_routing() -> list[dict[str, Any]]: return [ { "name": "BasicSwap", - "transpile_pass": lambda device: [BasicSwap(coupling_map=CouplingMap(device["cmap"]))], + "transpile_pass": lambda device: [BasicSwap(coupling_map=CouplingMap(device.coupling_map))], "origin": "qiskit", }, { "name": "RoutingPass", "transpile_pass": lambda device: [ PreProcessTKETRoutingAfterQiskitLayout(), - RoutingPass(Architecture(device["cmap"])), + RoutingPass(Architecture(device.coupling_map)), ], "origin": "tket", }, { "name": "StochasticSwap", - "transpile_pass": lambda device: [StochasticSwap(coupling_map=CouplingMap(device["cmap"]))], + "transpile_pass": lambda device: [StochasticSwap(coupling_map=CouplingMap(device.coupling_map))], "origin": "qiskit", }, ] @@ -262,7 +294,7 @@ def get_actions_mapping() -> list[dict[str, Any]]: { "name": "SabreMapping", "transpile_pass": lambda device: [ - SabreLayout(coupling_map=CouplingMap(device["cmap"]), skip_routing=False), + SabreLayout(coupling_map=CouplingMap(device.coupling_map), skip_routing=False), ], "origin": "qiskit", }, @@ -271,9 +303,9 @@ def get_actions_mapping() -> list[dict[str, Any]]: "transpile_pass": lambda device: lambda bqskit_circuit: bqskit_compile( bqskit_circuit, model=MachineModel( - num_qudits=device["max_qubits"], + num_qudits=device.num_qubits, gate_set=get_BQSKit_native_gates(device), - coupling_graph=[(elem[0], elem[1]) for elem in device["cmap"]], + coupling_graph=[(elem[0], elem[1]) for elem in device.coupling_map], ), with_mapping=True, optimization_level=2, @@ -289,7 +321,7 @@ def get_actions_synthesis() -> list[dict[str, Any]]: { "name": "BasisTranslator", "transpile_pass": lambda device: [ - BasisTranslator(StandardEquivalenceLibrary, target_basis=device["native_gates"]) + BasisTranslator(StandardEquivalenceLibrary, target_basis=device.basis_gates) ], "origin": "qiskit", }, @@ -546,7 +578,7 @@ def apply(self, circuit: Circuit) -> None: place_with_map(circuit=circuit, qmap=mapping) -def get_BQSKit_native_gates(device: dict[str, Any]) -> list[gates.Gate] | None: +def get_BQSKit_native_gates(device: Device) -> list[gates.Gate] | None: """Returns the native gates of the given device. Args: @@ -555,7 +587,7 @@ def get_BQSKit_native_gates(device: dict[str, Any]) -> list[gates.Gate] | None: Returns: list[gates.Gate]: The native gates of the given provider. """ - provider = device["name"].split("_")[0] + provider = device.name.split("_")[0] native_gatesets = { "ibm": [gates.RZGate(), gates.SXGate(), gates.XGate(), gates.CNOTGate()], @@ -626,3 +658,35 @@ def final_layout_bqskit_to_qiskit( _output_qubit_list=compiled_qc.qubits, _input_qubit_count=initial_qc.num_qubits, ) + + +def get_ibm_backend_properties_by_device_name(device_name: str) -> Any: + """Returns the IBM backend name for the given device name. + + Args: + device_name (str): The name of the device for which the IBM backend name is returned. + + Returns: + str: The IBM backend name for the given device name. + """ + if "ibm" not in device_name: + return None + if device_name == "ibm_washington": + return FakeWashington().properties() + if device_name == "ibm_montreal": + return FakeMontreal().properties() + if device_name == "ibm_guadalupe": + return FakeGuadalupe().properties() + if device_name == "ibm_quito": + return FakeQuito().properties() + return None + + +def get_layout_postprocessing_qiskit_pass() -> ( + Callable[[Device], list[FullAncillaAllocation | EnlargeWithAncilla | ApplyLayout]] +): + return lambda device: [ + FullAncillaAllocation(coupling_map=CouplingMap(device.coupling_map)), + EnlargeWithAncilla(), + ApplyLayout(), + ] diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 40d0b2669..8a3a1fc13 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -3,30 +3,11 @@ from pathlib import Path import numpy as np -import pytest from mqt.bench import get_benchmark from mqt.predictor import rl -def test_get_device_false_input() -> None: - with pytest.raises(RuntimeError): - rl.helper.get_device("false_input") - - -def test_get_device_index_of_device_false_input() -> None: - with pytest.raises(RuntimeError): - rl.helper.get_device_index_of_device("false_input") - - -@pytest.mark.parametrize( - "device", - ["ibm_washington", "ibm_montreal", "rigetti_aspen_m2", "oqc_lucy", "ionq_harmony", "ionq_aria1", "quantinuum_h2"], -) -def test_get_device(device: str) -> None: - assert rl.helper.get_device(device) is not None - - def test_create_feature_dict() -> None: qc = get_benchmark("dj", 1, 5) features = rl.helper.create_feature_dict(qc) diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 704f6e6b1..1b8cf7b37 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -1,15 +1,17 @@ from __future__ import annotations -from typing import Any, cast +from typing import cast import pytest from bqskit.ext import bqskit_to_qiskit, qiskit_to_bqskit +from pytket.circuit import Qubit from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit from qiskit import QuantumCircuit from qiskit.transpiler import CouplingMap, PassManager from qiskit.transpiler.passes import CheckMap, GatesInBasis from qiskit.transpiler.runningpassmanager import TranspileLayout +from mqt.bench.devices import Device, get_available_devices, get_device_by_name from mqt.predictor.rl import helper @@ -32,8 +34,8 @@ def test_BQSKitO2_action() -> None: assert optimized_qc != qc -@pytest.mark.parametrize("device", helper.get_devices(), ids=lambda device: cast(str, device["name"])) -def test_BQSKitSynthesis_action(device: dict[str, Any]) -> None: +@pytest.mark.parametrize("device", get_available_devices(), ids=lambda device: cast(str, device.name)) +def test_BQSKitSynthesis_action(device: Device) -> None: """Test the BQSKitSynthesis action for all devices.""" action_BQSKitSynthesis = None for action in helper.get_actions_synthesis(): @@ -46,7 +48,7 @@ def test_BQSKitSynthesis_action(device: dict[str, Any]) -> None: qc.h(0) qc.cx(0, 1) - check_nat_gates = GatesInBasis(basis_gates=device["native_gates"]) + check_nat_gates = GatesInBasis(basis_gates=device.basis_gates) check_nat_gates(qc) assert not check_nat_gates.property_set["all_gates_in_basis"] @@ -54,10 +56,10 @@ def test_BQSKitSynthesis_action(device: dict[str, Any]) -> None: bqskit_qc = qiskit_to_bqskit(qc) native_gates_qc = bqskit_to_qiskit(transpile_pass(bqskit_qc)) - check_nat_gates = GatesInBasis(basis_gates=device["native_gates"]) + check_nat_gates = GatesInBasis(basis_gates=device.basis_gates) check_nat_gates(native_gates_qc) only_nat_gates = check_nat_gates.property_set["all_gates_in_basis"] - if "oqc" not in device["name"]: + if "oqc" not in device.name: assert only_nat_gates @@ -77,7 +79,7 @@ def test_BQSKitMapping_action_swaps_necessary() -> None: qc.cx(0, 3) qc.cx(0, 4) - device = helper.get_devices()[1] + device = get_device_by_name("ibm_montreal") bqskit_qc = qiskit_to_bqskit(qc) bqskit_qc_mapped, input_mapping, output_mapping = action_BQSKitMapping["transpile_pass"](device)(bqskit_qc) mapped_qc = bqskit_to_qiskit(bqskit_qc_mapped) @@ -89,18 +91,18 @@ def test_BQSKitMapping_action_swaps_necessary() -> None: def check_mapped_circuit( - initial_qc: QuantumCircuit, mapped_qc: QuantumCircuit, device: dict[str, Any], layout: TranspileLayout + initial_qc: QuantumCircuit, mapped_qc: QuantumCircuit, device: Device, layout: TranspileLayout ) -> None: # check if the altered circuit is correctly mapped to the device - check_mapping = CheckMap(coupling_map=CouplingMap(device["cmap"])) + check_mapping = CheckMap(coupling_map=CouplingMap(device.coupling_map)) check_mapping(mapped_qc) mapped = check_mapping.property_set["is_swap_mapped"] assert mapped assert mapped_qc != initial_qc assert layout is not None - assert len(layout.initial_layout) == device["max_qubits"] + assert len(layout.initial_layout) == device.num_qubits if layout.final_layout is not None: - assert len(layout.final_layout) == device["max_qubits"] + assert len(layout.final_layout) == device.num_qubits # each qubit of the initial layout is part of the initial quantum circuit and the register name is correctly set for assigned_physical_qubit in layout.initial_layout._p2v.values(): # noqa: SLF001 @@ -114,7 +116,7 @@ def check_mapped_circuit( assert initial_qc.find_bit(assigned_physical_qubit).registers[0][0].name == "q" # assigned_physical_qubit is an ancilla qubit else: - assert qreg.size == device["max_qubits"] - initial_qc.num_qubits + assert qreg.size == device.num_qubits - initial_qc.num_qubits # each qubit of the final layout is part of the mapped quantum circuit and the register name is correctly set if layout.final_layout is not None: for assigned_physical_qubit in layout.final_layout._p2v.values(): # noqa: SLF001 @@ -138,7 +140,7 @@ def test_BQSKitMapping_action_no_swaps_necessary() -> None: qc_no_swap_needed.h(0) qc_no_swap_needed.cx(0, 1) - device = helper.get_devices()[1] + device = get_device_by_name("ibm_montreal") bqskit_qc = qiskit_to_bqskit(qc_no_swap_needed) bqskit_qc_mapped, input_mapping, output_mapping = action_BQSKitMapping["transpile_pass"](device)(bqskit_qc) @@ -161,7 +163,7 @@ def test_TKET_routing() -> None: qc.cx(0, 3) qc.cx(0, 4) - device = helper.get_devices()[1] + device = get_device_by_name("ibm_montreal") layout_action = helper.get_actions_layout()[0] transpile_pass = layout_action["transpile_pass"](device) @@ -179,7 +181,6 @@ def test_TKET_routing() -> None: tket_qc = qiskit_to_tk(layouted_qc, preserve_param_uuid=True) for elem in routing_action["transpile_pass"](device): elem.apply(tket_qc) - from pytket.circuit import Qubit qbs = tket_qc.qubits qubit_map = {qbs[i]: Qubit("q", i) for i in range(len(qbs))} diff --git a/tests/compilation/test_pretrained_models.py b/tests/compilation/test_pretrained_models.py index d84eaff19..fb0dbc7f2 100644 --- a/tests/compilation/test_pretrained_models.py +++ b/tests/compilation/test_pretrained_models.py @@ -1,15 +1,12 @@ from __future__ import annotations import os -from pathlib import Path import pytest from qiskit import QuantumCircuit from mqt.bench import get_benchmark -from mqt.bench.devices import get_available_device_names -from mqt.predictor import qcompile, reward, rl -from mqt.predictor.evaluation import evaluate_sample_circuit +from mqt.predictor import reward, rl # only run test when executed on GitHub runner IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" @@ -25,31 +22,3 @@ def test_qcompile_with_pretrained_models(figure_of_merit: reward.figure_of_merit qc_compiled, compilation_information = rl.qcompile(qc, figure_of_merit=figure_of_merit, device_name="quantinuum_h2") assert isinstance(qc_compiled, QuantumCircuit) assert compilation_information is not None - - -def test_qcompile() -> None: - qc = get_benchmark("ghz", 1, 5) - qc_compiled, compilation_information, quantum_device = qcompile(qc) - assert quantum_device in get_available_device_names() - assert qc_compiled.layout is not None - assert len(qc_compiled) > 0 - - -def test_evaluate_sample_circuit() -> None: - qc = get_benchmark("ghz", 1, 3) - filename = "test_3.qasm" - qc.qasm(filename=filename) - res = evaluate_sample_circuit(filename) - expected_keys = [] - for compilation_setup in ["qiskit", "tket", "mqt-predictor_expected_fidelity", "mqt-predictor_critical_depth"]: - for key in ["time", "expected_fidelity", "critical_depth"]: - if "mqt-predictor" in compilation_setup: - expected_keys.append(compilation_setup + "_" + key) - else: - expected_keys.extend( - [compilation_setup + "_" + name + "_" + key for name in get_available_device_names()] - ) - - assert all(key in res for key in expected_keys) - if Path(filename).exists(): - Path(filename).unlink() From 2d71be1e3fab997aadad6b61fb16f81f352a3860 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 12:34:00 +0100 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=85=20reordered=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/device_selection/test_predictor_ml.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/device_selection/test_predictor_ml.py b/tests/device_selection/test_predictor_ml.py index e455a0218..2a652a7d1 100644 --- a/tests/device_selection/test_predictor_ml.py +++ b/tests/device_selection/test_predictor_ml.py @@ -10,6 +10,16 @@ from mqt.predictor import ml, reward +def test_train_random_forest_classifier() -> None: + """Test the training of a random forest classifier. This test must be executed prior to any prediction to make sure + the model is trained using the latest scikit-learn version.""" + predictor = ml.Predictor() + assert predictor.clf is None + predictor.train_random_forest_classifier(visualize_results=False) + + assert predictor.clf is not None + + def test_predict() -> None: path = ml.helper.get_path_trained_model(figure_of_merit="expected_fidelity") assert path.is_file() @@ -67,14 +77,6 @@ def test_performance_measures() -> None: result_path.unlink() -def test_train_random_forest_classifier() -> None: - predictor = ml.Predictor() - assert predictor.clf is None - predictor.train_random_forest_classifier(visualize_results=False) - - assert predictor.clf is not None - - def test_compile_all_circuits_for_dev_and_fom() -> None: predictor = ml.Predictor() source_path = Path() From d416d67cc94fa2186acd18a5f1f750ca0c3f5e90 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 12:48:40 +0100 Subject: [PATCH 07/18] =?UTF-8?q?=E2=9C=85=20reordered=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/device_selection/test_helper_ml.py | 9 --------- tests/device_selection/test_predictor_ml.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/device_selection/test_helper_ml.py b/tests/device_selection/test_helper_ml.py index fab46869e..87fb2e7bf 100644 --- a/tests/device_selection/test_helper_ml.py +++ b/tests/device_selection/test_helper_ml.py @@ -3,7 +3,6 @@ import pytest from mqt.bench import benchmark_generator -from mqt.bench.devices import get_available_device_names from mqt.predictor import ml @@ -53,14 +52,6 @@ def test_get_path_trained_model() -> None: assert path.exists() -def test_predict_device_for_figure_of_merit() -> None: - qc = benchmark_generator.get_benchmark("ghz", 1, 5) - assert ml.helper.predict_device_for_figure_of_merit(qc, "expected_fidelity").name in get_available_device_names() - - with pytest.raises(FileNotFoundError, match="Classifier is neither trained nor saved."): - ml.helper.predict_device_for_figure_of_merit(qc, "false_input") # type: ignore[arg-type] - - def test_get_path_results() -> None: for get_ghz_path_results in (True, False): path = ml.helper.get_path_results(ghz_results=get_ghz_path_results) diff --git a/tests/device_selection/test_predictor_ml.py b/tests/device_selection/test_predictor_ml.py index 2a652a7d1..043fa8914 100644 --- a/tests/device_selection/test_predictor_ml.py +++ b/tests/device_selection/test_predictor_ml.py @@ -4,9 +4,10 @@ from typing import Literal import numpy as np +import pytest from mqt.bench import benchmark_generator -from mqt.bench.devices import get_available_devices +from mqt.bench.devices import get_available_device_names, get_available_devices from mqt.predictor import ml, reward @@ -20,6 +21,14 @@ def test_train_random_forest_classifier() -> None: assert predictor.clf is not None +def test_predict_device_for_figure_of_merit() -> None: + qc = benchmark_generator.get_benchmark("ghz", 1, 5) + assert ml.helper.predict_device_for_figure_of_merit(qc, "expected_fidelity").name in get_available_device_names() + + with pytest.raises(FileNotFoundError, match="Classifier is neither trained nor saved."): + ml.helper.predict_device_for_figure_of_merit(qc, "false_input") # type: ignore[arg-type] + + def test_predict() -> None: path = ml.helper.get_path_trained_model(figure_of_merit="expected_fidelity") assert path.is_file() From ef772d984f2bf1201310702b7dbcae7325f79e50 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 13:11:11 +0100 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=85=20adjusted=20test=20with=20pre-?= =?UTF-8?q?trained=20models=20triggered=20only=20via=20GitHub=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_pretrained_models.py | 24 --------- tests/test_pretrained_models.py | 57 +++++++++++++++++++++ 2 files changed, 57 insertions(+), 24 deletions(-) delete mode 100644 tests/compilation/test_pretrained_models.py create mode 100644 tests/test_pretrained_models.py diff --git a/tests/compilation/test_pretrained_models.py b/tests/compilation/test_pretrained_models.py deleted file mode 100644 index fb0dbc7f2..000000000 --- a/tests/compilation/test_pretrained_models.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import os - -import pytest -from qiskit import QuantumCircuit - -from mqt.bench import get_benchmark -from mqt.predictor import reward, rl - -# only run test when executed on GitHub runner -IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" - - -@pytest.mark.skipif(not IN_GITHUB_ACTIONS, reason="Only run this test on GitHub runner") -@pytest.mark.parametrize( - "figure_of_merit", - ["expected_fidelity", "critical_depth"], -) -def test_qcompile_with_pretrained_models(figure_of_merit: reward.figure_of_merit) -> None: - qc = get_benchmark("ghz", 1, 3) - qc_compiled, compilation_information = rl.qcompile(qc, figure_of_merit=figure_of_merit, device_name="quantinuum_h2") - assert isinstance(qc_compiled, QuantumCircuit) - assert compilation_information is not None diff --git a/tests/test_pretrained_models.py b/tests/test_pretrained_models.py new file mode 100644 index 000000000..f8981418d --- /dev/null +++ b/tests/test_pretrained_models.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import os +from pathlib import Path + +import pytest +from qiskit import QuantumCircuit + +from mqt.bench import get_benchmark +from mqt.bench.devices import get_available_device_names +from mqt.predictor import qcompile, reward, rl +from mqt.predictor.evaluation import evaluate_sample_circuit + +# only run test when executed on GitHub runner +IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" + + +@pytest.mark.skipif(not IN_GITHUB_ACTIONS, reason="Only run this test on GitHub runner") +@pytest.mark.parametrize( + "figure_of_merit", + ["expected_fidelity", "critical_depth"], +) +def test_qcompile_with_pretrained_models(figure_of_merit: reward.figure_of_merit) -> None: + qc = get_benchmark("ghz", 1, 3) + qc_compiled, compilation_information = rl.qcompile(qc, figure_of_merit=figure_of_merit, device_name="quantinuum_h2") + assert isinstance(qc_compiled, QuantumCircuit) + assert compilation_information is not None + + +@pytest.mark.skipif(not IN_GITHUB_ACTIONS, reason="Only run this test on GitHub runner") +def test_qcompile() -> None: + qc = get_benchmark("ghz", 1, 5) + qc_compiled, compilation_information, quantum_device = qcompile(qc) + assert quantum_device in get_available_device_names() + assert qc_compiled.layout is not None + assert len(qc_compiled) > 0 + + +@pytest.mark.skipif(not IN_GITHUB_ACTIONS, reason="Only run this test on GitHub runner") +def test_evaluate_sample_circuit() -> None: + qc = get_benchmark("ghz", 1, 3) + filename = "test_3.qasm" + qc.qasm(filename=filename) + res = evaluate_sample_circuit(filename) + expected_keys = [] + for compilation_setup in ["qiskit", "tket", "mqt-predictor_expected_fidelity", "mqt-predictor_critical_depth"]: + for key in ["time", "expected_fidelity", "critical_depth"]: + if "mqt-predictor" in compilation_setup: + expected_keys.append(compilation_setup + "_" + key) + else: + expected_keys.extend( + [compilation_setup + "_" + name + "_" + key for name in get_available_device_names()] + ) + + assert all(key in res for key in expected_keys) + if Path(filename).exists(): + Path(filename).unlink() From cb138e6b2b1aad22d559471fee1b670c153aecd1 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 13:25:26 +0100 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=8E=A8=20refactored=20postprocessin?= =?UTF-8?q?g=20of=20VF2(Post)Layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/PredictorEnv.py | 52 ++++++++++++++++++---------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index a42a7e0ca..88f7ee607 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -12,10 +12,10 @@ from gymnasium.spaces import Box, Dict, Discrete from pytket.circuit import Qubit from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, QuantumRegister from qiskit.transpiler import CouplingMap, PassManager from qiskit.transpiler.passes import ApplyLayout, CheckMap, GatesInBasis -from qiskit.transpiler.runningpassmanager import TranspileLayout +from qiskit.transpiler.runningpassmanager import Layout, TranspileLayout from mqt.bench.devices import get_device_by_name from mqt.predictor import reward, rl @@ -236,26 +236,17 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: ): if action["name"] == "VF2Layout": if pm.property_set["layout"]: - initial_layout = pm.property_set["layout"] - original_qubit_indices = pm.property_set["original_qubit_indices"] - final_layout = pm.property_set["final_layout"] - postprocessing_action = rl.helper.get_layout_postprocessing_qiskit_pass()(self.device) - pm = PassManager(postprocessing_action) - pm.property_set["layout"] = initial_layout - pm.property_set["original_qubit_indices"] = original_qubit_indices - pm.property_set["final_layout"] = final_layout - altered_qc = pm.run(altered_qc) + altered_qc, pm = self.postprocess_VF2Layout( + altered_qc, + pm.property_set["layout"], + pm.property_set["original_qubit_indices"], + pm.property_set["final_layout"], + ) elif action["name"] == "VF2PostLayout": assert pm.property_set["VF2PostLayout_stop_reason"] is not None post_layout = pm.property_set["post_layout"] if post_layout: - pm = PassManager(ApplyLayout()) - assert self.layout is not None - pm.property_set["layout"] = self.layout.initial_layout - pm.property_set["original_qubit_indices"] = self.layout.input_qubit_mapping - pm.property_set["final_layout"] = self.layout.final_layout - pm.property_set["post_layout"] = post_layout - altered_qc = pm.run(altered_qc) + altered_qc, pm = self.postprocess_VF2PostLayout(altered_qc, post_layout) else: assert pm.property_set["layout"] @@ -326,6 +317,31 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: return altered_qc + def postprocess_VF2PostLayout(self, qc: QuantumCircuit, post_layout: Layout) -> tuple[QuantumCircuit, PassManager]: + pm = PassManager(ApplyLayout()) + assert self.layout is not None + pm.property_set["layout"] = self.layout.initial_layout + pm.property_set["original_qubit_indices"] = self.layout.input_qubit_mapping + pm.property_set["final_layout"] = self.layout.final_layout + pm.property_set["post_layout"] = post_layout + altered_qc = pm.run(qc) + return altered_qc, pm + + def postprocess_VF2Layout( + self, + qc: QuantumCircuit, + initial_layout: TranspileLayout, + original_qubit_indices: dict[QuantumRegister, int], + final_layout: TranspileLayout, + ) -> tuple[QuantumCircuit, PassManager]: + postprocessing_action = rl.helper.get_layout_postprocessing_qiskit_pass()(self.device) + pm = PassManager(postprocessing_action) + pm.property_set["layout"] = initial_layout + pm.property_set["original_qubit_indices"] = original_qubit_indices + pm.property_set["final_layout"] = final_layout + altered_qc = pm.run(qc) + return altered_qc, pm + def determine_valid_actions_for_state(self) -> list[int]: """Determines and returns the valid actions for the current state.""" check_nat_gates = GatesInBasis(basis_gates=self.device.basis_gates) From 3245f5dcbba80ae99b3a6a38fe0ad550cf2bfd43 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 13:26:26 +0100 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=93=9Dadded=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/PredictorEnv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index 88f7ee607..388b9ef5c 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -318,6 +318,7 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: return altered_qc def postprocess_VF2PostLayout(self, qc: QuantumCircuit, post_layout: Layout) -> tuple[QuantumCircuit, PassManager]: + """Postprocesses the given quantum circuit with the post_layout and returns the altered quantum circuit and the respective PassManager.""" pm = PassManager(ApplyLayout()) assert self.layout is not None pm.property_set["layout"] = self.layout.initial_layout @@ -334,6 +335,7 @@ def postprocess_VF2Layout( original_qubit_indices: dict[QuantumRegister, int], final_layout: TranspileLayout, ) -> tuple[QuantumCircuit, PassManager]: + """Postprocesses the given quantum circuit with the given layout and returns the altered quantum circuit and the respective PassManager.""" postprocessing_action = rl.helper.get_layout_postprocessing_qiskit_pass()(self.device) pm = PassManager(postprocessing_action) pm.property_set["layout"] = initial_layout From 8c8941d0a4f4867c30972ef2c23cb42c9a2dce65 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 13:35:46 +0100 Subject: [PATCH 11/18] =?UTF-8?q?=F0=9F=8E=A8=20adjusted=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/PredictorEnv.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index 388b9ef5c..eca5ea8f6 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -13,9 +13,8 @@ from pytket.circuit import Qubit from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit from qiskit import QuantumCircuit, QuantumRegister -from qiskit.transpiler import CouplingMap, PassManager +from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspileLayout from qiskit.transpiler.passes import ApplyLayout, CheckMap, GatesInBasis -from qiskit.transpiler.runningpassmanager import Layout, TranspileLayout from mqt.bench.devices import get_device_by_name from mqt.predictor import reward, rl From b408187803c403b260a89b4e80061cf5be859fa5 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 13:48:00 +0100 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=9A=9A=20moved=20test=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 239622f9b..7ae9c5f20 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,7 +23,7 @@ jobs: - name: Install MQT Predictor run: pip install .[coverage] - name: Generate Report - run: pytest -v --cov --cov-config=pyproject.toml --cov-report=xml --ignore=tests/compilation/test_pretrained_models.py + run: pytest -v --cov --cov-config=pyproject.toml --cov-report=xml --ignore=tests/test_pretrained_models.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload coverage to Codecov From 92b71daa1594f97f8f71f9cca926ef227f679036 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 16:05:26 +0100 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=93=8C=20adjusted=20upper=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4bcee25db..4e95e7ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,12 +20,12 @@ dynamic = ["version"] dependencies = [ "mqt.bench @ git+https://github.com/cda-tum/mqt-bench.git", - "sb3_contrib>=2.0.0", + "sb3_contrib>=2.0.0, <2.2.2", "scikit-learn>=1.4.0,<1.4.2", "importlib_metadata>=4.4; python_version < '3.10'", "importlib_resources>=5.0; python_version < '3.10'", - "tensorboard>=2.11.0", - "bqskit>=1.1.0, <1.2.0", + "tensorboard>=2.11.0, <2.16.3", + "bqskit>=1.1.0, <1.1.2", ] classifiers = [ From beb1c138fa0b5f1b71e580f9ec8a4b7d812bf82f Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 16:32:28 +0100 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=8E=A8=20assert=20that=20VF2PostLay?= =?UTF-8?q?out=20is=20only=20available=20for=20ibm=20devices=20because=20o?= =?UTF-8?q?f=20the=20required=20backend=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/PredictorEnv.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index eca5ea8f6..6212cfc3d 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -191,6 +191,12 @@ def action_masks(self) -> list[bool]: action_mask = [ action_mask[i] and self.action_set[i].get("origin") != "bqskit" for i in range(len(action_mask)) ] + + # only allow VF2PostLayout if "ibm" is in the device name + if "ibm" not in self.device.name: + action_mask = [ + action_mask[i] and self.action_set[i].get("name") != "VF2PostLayout" for i in range(len(action_mask)) + ] return action_mask def apply_action(self, action_index: int) -> QuantumCircuit | None: From e1c38b955cd5737cc7b0d54edabda6bf795de253 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 16:32:41 +0100 Subject: [PATCH 15/18] =?UTF-8?q?=E2=9C=85=20test=20the=20VF2Layout=20and?= =?UTF-8?q?=20PostLayout=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_helper_rl.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 8a3a1fc13..a9c8fdbfb 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -3,8 +3,11 @@ from pathlib import Path import numpy as np +from qiskit import transpile +from qiskit.transpiler import PassManager from mqt.bench import get_benchmark +from mqt.bench.devices import get_device_by_name from mqt.predictor import rl @@ -25,3 +28,32 @@ def test_get_path_training_circuits() -> None: path = rl.helper.get_path_training_circuits() assert path.exists() assert isinstance(path, Path) + + +def test_VF2_layout_and_postlayout() -> None: + qc = get_benchmark("ghz", 1, 3) + # qc_transpiled = transpile(qc, basis_gates=dev.basis_gates, coupling_map=dev.coupling_map) + + for dev in [get_device_by_name("ibm_montreal"), get_device_by_name("ionq_harmony")]: + layout_pass = None + for layout_action in rl.helper.get_actions_layout(): + if layout_action["name"] == "VF2Layout": + layout_pass = layout_action["transpile_pass"](dev) + break + pm = PassManager(layout_pass) + pm.run(qc) + assert pm.property_set["VF2Layout_stop_reason"] is not None + + qc_transpiled = transpile(qc, basis_gates=dev.basis_gates, coupling_map=dev.coupling_map) + assert qc_transpiled.layout is not None + + dev_success = get_device_by_name("ibm_montreal") + post_layout_pass = None + for layout_action in rl.helper.get_actions_final_optimization(): + if layout_action["name"] == "VF2PostLayout": + post_layout_pass = layout_action["transpile_pass"](dev_success) + break + + pm = PassManager(post_layout_pass) + pm.run(qc_transpiled) + assert pm.property_set["VF2PostLayout_stop_reason"] is not None From 154f9a027f2a9f9c5deeb4b229018bf859db2c07 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 17:37:25 +0100 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=8E=A8=20moved=20helper=20functions?= =?UTF-8?q?=20to=20helper=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/PredictorEnv.py | 36 ++++------------------------ src/mqt/predictor/rl/helper.py | 34 ++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index 6212cfc3d..b5397fc78 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -12,9 +12,9 @@ from gymnasium.spaces import Box, Dict, Discrete from pytket.circuit import Qubit from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit -from qiskit import QuantumCircuit, QuantumRegister -from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspileLayout -from qiskit.transpiler.passes import ApplyLayout, CheckMap, GatesInBasis +from qiskit import QuantumCircuit +from qiskit.transpiler import CouplingMap, PassManager, TranspileLayout +from qiskit.transpiler.passes import CheckMap, GatesInBasis from mqt.bench.devices import get_device_by_name from mqt.predictor import reward, rl @@ -246,12 +246,13 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: pm.property_set["layout"], pm.property_set["original_qubit_indices"], pm.property_set["final_layout"], + self.device, ) elif action["name"] == "VF2PostLayout": assert pm.property_set["VF2PostLayout_stop_reason"] is not None post_layout = pm.property_set["post_layout"] if post_layout: - altered_qc, pm = self.postprocess_VF2PostLayout(altered_qc, post_layout) + altered_qc, pm = self.postprocess_VF2PostLayout(altered_qc, post_layout, self.layout) else: assert pm.property_set["layout"] @@ -322,33 +323,6 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: return altered_qc - def postprocess_VF2PostLayout(self, qc: QuantumCircuit, post_layout: Layout) -> tuple[QuantumCircuit, PassManager]: - """Postprocesses the given quantum circuit with the post_layout and returns the altered quantum circuit and the respective PassManager.""" - pm = PassManager(ApplyLayout()) - assert self.layout is not None - pm.property_set["layout"] = self.layout.initial_layout - pm.property_set["original_qubit_indices"] = self.layout.input_qubit_mapping - pm.property_set["final_layout"] = self.layout.final_layout - pm.property_set["post_layout"] = post_layout - altered_qc = pm.run(qc) - return altered_qc, pm - - def postprocess_VF2Layout( - self, - qc: QuantumCircuit, - initial_layout: TranspileLayout, - original_qubit_indices: dict[QuantumRegister, int], - final_layout: TranspileLayout, - ) -> tuple[QuantumCircuit, PassManager]: - """Postprocesses the given quantum circuit with the given layout and returns the altered quantum circuit and the respective PassManager.""" - postprocessing_action = rl.helper.get_layout_postprocessing_qiskit_pass()(self.device) - pm = PassManager(postprocessing_action) - pm.property_set["layout"] = initial_layout - pm.property_set["original_qubit_indices"] = original_qubit_indices - pm.property_set["final_layout"] = final_layout - altered_qc = pm.run(qc) - return altered_qc, pm - def determine_valid_actions_for_state(self) -> list[int]: """Determines and returns the valid actions for the current state.""" check_nat_gates = GatesInBasis(basis_gates=self.device.basis_gates) diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index f8422e088..9c25ecbdd 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -23,7 +23,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.equivalence_library import StandardEquivalenceLibrary from qiskit.circuit.library import XGate, ZGate -from qiskit.transpiler import CouplingMap, TranspileLayout +from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspileLayout from qiskit.transpiler.passes import ( ApplyLayout, BasicSwap, @@ -76,7 +76,6 @@ from bqskit.ir import gates from qiskit import QuantumRegister from qiskit.providers.fake_provider import FakeGuadalupe, FakeMontreal, FakeQuito, FakeWashington -from qiskit.transpiler.layout import Layout from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.runningpassmanager import ConditionalController @@ -690,3 +689,34 @@ def get_layout_postprocessing_qiskit_pass() -> ( EnlargeWithAncilla(), ApplyLayout(), ] + + +def postprocess_VF2Layout( + qc: QuantumCircuit, + initial_layout: Layout, + original_qubit_indices: dict[QuantumRegister, int], + final_layout: Layout, + device: Device, +) -> tuple[QuantumCircuit, PassManager]: + """Postprocesses the given quantum circuit with the given layout and returns the altered quantum circuit and the respective PassManager.""" + postprocessing_action = rl.helper.get_layout_postprocessing_qiskit_pass()(device) + pm = PassManager(postprocessing_action) + pm.property_set["layout"] = initial_layout + pm.property_set["original_qubit_indices"] = original_qubit_indices + pm.property_set["final_layout"] = final_layout + altered_qc = pm.run(qc) + return altered_qc, pm + + +def postprocess_VF2PostLayout( + qc: QuantumCircuit, post_layout: Layout, layout_before: TranspileLayout +) -> tuple[QuantumCircuit, PassManager]: + """Postprocesses the given quantum circuit with the post_layout and returns the altered quantum circuit and the respective PassManager.""" + pm = PassManager(ApplyLayout()) + assert layout_before is not None + pm.property_set["layout"] = layout_before.initial_layout + pm.property_set["original_qubit_indices"] = layout_before.input_qubit_mapping + pm.property_set["final_layout"] = layout_before.final_layout + pm.property_set["post_layout"] = post_layout + altered_qc = pm.run(qc) + return altered_qc, pm From fed617117a9f71c22c35e90b320079681fe3d004 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 17:37:31 +0100 Subject: [PATCH 17/18] =?UTF-8?q?=E2=9C=85=20improved=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_helper_rl.py | 33 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index a9c8fdbfb..03bbabcd3 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -5,6 +5,7 @@ import numpy as np from qiskit import transpile from qiskit.transpiler import PassManager +from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from mqt.bench import get_benchmark from mqt.bench.devices import get_device_by_name @@ -32,7 +33,6 @@ def test_get_path_training_circuits() -> None: def test_VF2_layout_and_postlayout() -> None: qc = get_benchmark("ghz", 1, 3) - # qc_transpiled = transpile(qc, basis_gates=dev.basis_gates, coupling_map=dev.coupling_map) for dev in [get_device_by_name("ibm_montreal"), get_device_by_name("ionq_harmony")]: layout_pass = None @@ -41,13 +41,26 @@ def test_VF2_layout_and_postlayout() -> None: layout_pass = layout_action["transpile_pass"](dev) break pm = PassManager(layout_pass) - pm.run(qc) + altered_qc = pm.run(qc) + assert len(altered_qc.layout.initial_layout) == 3 assert pm.property_set["VF2Layout_stop_reason"] is not None + layouted_qc, pm = rl.helper.postprocess_VF2Layout( + altered_qc, + pm.property_set["layout"], + pm.property_set["original_qubit_indices"], + pm.property_set["final_layout"], + dev, + ) + assert len(layouted_qc.layout.initial_layout) == dev.num_qubits - qc_transpiled = transpile(qc, basis_gates=dev.basis_gates, coupling_map=dev.coupling_map) + dev_success = get_device_by_name("ibm_montreal") + qc_transpiled = transpile( + qc, basis_gates=dev_success.basis_gates, coupling_map=dev_success.coupling_map, optimization_level=0 + ) assert qc_transpiled.layout is not None - dev_success = get_device_by_name("ibm_montreal") + initial_layout_before = qc_transpiled.layout.initial_layout + post_layout_pass = None for layout_action in rl.helper.get_actions_final_optimization(): if layout_action["name"] == "VF2PostLayout": @@ -55,5 +68,15 @@ def test_VF2_layout_and_postlayout() -> None: break pm = PassManager(post_layout_pass) - pm.run(qc_transpiled) + altered_qc = pm.run(qc_transpiled) + assert pm.property_set["VF2PostLayout_stop_reason"] is not None + assert pm.property_set["VF2PostLayout_stop_reason"] == VF2PostLayoutStopReason.SOLUTION_FOUND + + assert len(layouted_qc.layout.initial_layout) == dev.num_qubits + layouted_qc, pm = rl.helper.postprocess_VF2PostLayout( + altered_qc, pm.property_set["post_layout"], qc_transpiled.layout + ) + initial_layout_after = layouted_qc.layout.initial_layout + + assert initial_layout_before != initial_layout_after From 23d31fd5918c34f08ce7e584474af40455f5d3a1 Mon Sep 17 00:00:00 2001 From: Nils Quetschlich Date: Mon, 26 Feb 2024 17:50:26 +0100 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=90=9B=20fixed=20function=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/PredictorEnv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mqt/predictor/rl/PredictorEnv.py b/src/mqt/predictor/rl/PredictorEnv.py index b5397fc78..74359a779 100644 --- a/src/mqt/predictor/rl/PredictorEnv.py +++ b/src/mqt/predictor/rl/PredictorEnv.py @@ -241,7 +241,7 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: ): if action["name"] == "VF2Layout": if pm.property_set["layout"]: - altered_qc, pm = self.postprocess_VF2Layout( + altered_qc, pm = rl.helper.postprocess_VF2Layout( altered_qc, pm.property_set["layout"], pm.property_set["original_qubit_indices"], @@ -252,7 +252,7 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: assert pm.property_set["VF2PostLayout_stop_reason"] is not None post_layout = pm.property_set["post_layout"] if post_layout: - altered_qc, pm = self.postprocess_VF2PostLayout(altered_qc, post_layout, self.layout) + altered_qc, pm = rl.helper.postprocess_VF2PostLayout(altered_qc, post_layout, self.layout) else: assert pm.property_set["layout"]