From 90ec2cff02947517a215608894aed6127f4b060d Mon Sep 17 00:00:00 2001 From: Patrick Hopf <81010725+flowerthrower@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:24:41 +0100 Subject: [PATCH 01/42] =?UTF-8?q?=F0=9F=90=9B=20Fix=20RL=20Training=20and?= =?UTF-8?q?=20Improve=20Structure=20(#573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR addresses critical bugs in the RL training process with the following key changes: **Structure Improvements:** - **Redesigned action validation logic** (`predictorenv.py`): Rewrote `determine_valid_actions_for_state()` with a more structured (but equivalent) state machine that explicitly tracks three circuit states (synthesized, laid_out, routed) and handles 6 different state combinations. - Added helper methods `is_circuit_laid_out()` and `is_circuit_routed()` to replace the buggy `CheckMap` pass with more reliable state checking. The new logic supports both the original restricted MDP and a flexible general MDP mode. - **Fixed type annotation** (`actions.py`): Corrected `do_while` parameter type from `dict[str, Circuit]` to `PropertySet` and added missing import for Qiskit's `PropertySet`. - **Added reproducibility** (`predictor.py`): Set random seed for non-test training runs to ensure reproducible results. - **Improved VF2Layout error handling** (`predictorenv.py`): Replaced assertion failures with warning logs when VF2Layout doesn't find a solution, preventing crashes during training. **Test Updates:** - Suppressed deprecation warnings in tket routing test --------- Signed-off-by: Patrick Hopf <81010725+flowerthrower@users.noreply.github.com> Co-authored-by: flowerthrower Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/mqt/predictor/reward.py | 45 +---- src/mqt/predictor/rl/actions.py | 3 +- src/mqt/predictor/rl/predictor.py | 7 +- src/mqt/predictor/rl/predictorenv.py | 191 +++++++++++++++--- .../test_integration_further_SDKs.py | 2 +- tests/compilation/test_predictor_rl.py | 2 +- 6 files changed, 177 insertions(+), 73 deletions(-) diff --git a/src/mqt/predictor/reward.py b/src/mqt/predictor/reward.py index 7c1ce1bba..a8cd76952 100644 --- a/src/mqt/predictor/reward.py +++ b/src/mqt/predictor/reward.py @@ -23,7 +23,6 @@ if TYPE_CHECKING: from qiskit import QuantumCircuit - from qiskit.circuit import QuantumRegister, Qubit from qiskit.transpiler import Target from sklearn.ensemble import RandomForestRegressor @@ -62,44 +61,22 @@ def expected_fidelity(qc: QuantumCircuit, device: Target, precision: int = 10) - if gate_type != "barrier": assert len(qargs) in [1, 2] - first_qubit_idx = calc_qubit_index(qargs, qc.qregs, 0) + first_qubit_idx = qc.find_bit(qargs[0]).index if len(qargs) == 1: specific_fidelity = 1 - device[gate_type][first_qubit_idx,].error else: - second_qubit_idx = calc_qubit_index(qargs, qc.qregs, 1) - specific_fidelity = 1 - device[gate_type][first_qubit_idx, second_qubit_idx].error - + second_qubit_idx = qc.find_bit(qargs[1]).index + try: + specific_fidelity = 1 - device[gate_type][first_qubit_idx, second_qubit_idx].error + except KeyError: + msg = f"Error rate for gate {gate_type} on qubits {first_qubit_idx} and {second_qubit_idx} not found in device properties." + raise KeyError(msg) from None res *= specific_fidelity return float(np.round(res, precision).item()) -def calc_qubit_index(qargs: list[Qubit], qregs: list[QuantumRegister], index: int) -> int: - """Calculates the global qubit index for a given quantum circuit and qubit index. - - Arguments: - qargs: The qubits of the quantum circuit. - qregs: The quantum registers of the quantum circuit. - index: The index of the qubit in the qargs list. - - Returns: - The global qubit index of the given qubit in the quantum circuit. - - Raises: - ValueError: If the qubit index is not found in the quantum registers. - """ - offset = 0 - for reg in qregs: - if qargs[index] not in reg: - offset += reg.size - else: - qubit_index: int = offset + reg.index(qargs[index]) - return qubit_index - error_msg = f"Global qubit index for local qubit {index} index not found." - raise ValueError(error_msg) - - def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: int = 10) -> float: """Calculates the estimated success probability of a given quantum circuit on a given device. @@ -125,7 +102,7 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: if gate_type == "barrier" or gate_type == "id": continue assert len(qargs) in (1, 2) - first_qubit_idx = calc_qubit_index(qargs, qc.qregs, 0) + first_qubit_idx = qc.find_bit(qargs[0]).index active_qubits.add(first_qubit_idx) if len(qargs) == 1: # single-qubit gate @@ -140,7 +117,7 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: )) exec_time_per_qubit[first_qubit_idx] += duration else: # multi-qubit gate - second_qubit_idx = calc_qubit_index(qargs, qc.qregs, 1) + second_qubit_idx = qc.find_bit(qargs[1]).index active_qubits.add(second_qubit_idx) duration = device[gate_type][first_qubit_idx, second_qubit_idx].duration op_times.append((gate_type, [first_qubit_idx, second_qubit_idx], duration, "s")) @@ -191,7 +168,7 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: continue assert len(qargs) in (1, 2) - first_qubit_idx = calc_qubit_index(qargs, qc.qregs, 0) + first_qubit_idx = scheduled_circ.find_bit(qargs[0]).index if len(qargs) == 1: if gate_type == "measure": @@ -213,7 +190,7 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: continue res *= 1 - device[gate_type][first_qubit_idx,].error else: - second_qubit_idx = calc_qubit_index(qargs, qc.qregs, 1) + second_qubit_idx = scheduled_circ.find_bit(qargs[1]).index res *= 1 - device[gate_type][first_qubit_idx, second_qubit_idx].error if qiskit_version >= "2.0.0": diff --git a/src/mqt/predictor/rl/actions.py b/src/mqt/predictor/rl/actions.py index 7b4432f5a..9efcc76d7 100644 --- a/src/mqt/predictor/rl/actions.py +++ b/src/mqt/predictor/rl/actions.py @@ -86,6 +86,7 @@ from bqskit import Circuit from pytket._tket.passes import BasePass as tket_BasePass + from qiskit.passmanager import PropertySet from qiskit.transpiler.basepasses import BasePass as qiskit_BasePass @@ -143,7 +144,7 @@ class DeviceDependentAction(Action): Callable[..., tuple[Any, ...] | Circuit], ] ) - do_while: Callable[[dict[str, Circuit]], bool] | None = None + do_while: Callable[[PropertySet], bool] | None = None # Registry of actions diff --git a/src/mqt/predictor/rl/predictor.py b/src/mqt/predictor/rl/predictor.py index 2654f34fe..1f75b1901 100644 --- a/src/mqt/predictor/rl/predictor.py +++ b/src/mqt/predictor/rl/predictor.py @@ -99,11 +99,12 @@ def train_model( """ if test: set_random_seed(0) # for reproducibility - n_steps = 10 - n_epochs = 1 - batch_size = 10 + n_steps = 32 + n_epochs = 2 + batch_size = 8 progress_bar = False else: + set_random_seed(0) # default PPO values n_steps = 2048 n_epochs = 10 diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 79249c729..c560b9e99 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -21,7 +21,7 @@ from bqskit import Circuit from qiskit.passmanager.base_tasks import Task - from qiskit.transpiler import Target + from qiskit.transpiler import Layout, Target from mqt.predictor.reward import figure_of_merit from mqt.predictor.rl.actions import Action @@ -40,7 +40,6 @@ from qiskit import QuantumCircuit from qiskit.passmanager.flow_controllers import DoWhileController from qiskit.transpiler import CouplingMap, PassManager, TranspileLayout -from qiskit.transpiler.passes import CheckMap, GatesInBasis from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from mqt.predictor.hellinger import get_hellinger_model_path @@ -69,6 +68,7 @@ class PredictorEnv(Env): def __init__( self, device: Target, + mdp: str = "paper", reward_function: figure_of_merit = "expected_fidelity", path_training_circuits: Path | None = None, ) -> None: @@ -76,6 +76,7 @@ def __init__( Arguments: device: The target device to be used for compilation. + mdp: The MDP transition policy. "paper" (default) enforces a strict, linear pipeline (synthesis -> (layout->routing) / mapping), while "flexible" allows for a cyclical approach where actions can be interleaved or reversed. reward_function: The figure of merit to be used for the reward function. Defaults to "expected_fidelity". path_training_circuits: The path to the training circuits folder. Defaults to None, which uses the default path. @@ -96,6 +97,9 @@ def __init__( self.used_actions: list[str] = [] self.device = device + logger.info("MDP: " + mdp) + self.mdp = mdp + # check for uni-directional coupling map coupling_set = {tuple(pair) for pair in self.device.build_coupling_map()} if any((b, a) not in coupling_set for (a, b) in coupling_set): @@ -189,23 +193,21 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any self.state: QuantumCircuit = altered_qc self.num_steps += 1 + self.state._layout = self.layout # noqa: SLF001 + self.valid_actions = self.determine_valid_actions_for_state() if len(self.valid_actions) == 0: msg = "No valid actions left." raise RuntimeError(msg) if action == self.action_terminate_index: + assert action in self.valid_actions, "Terminate action is not valid but was chosen." reward_val = self.calculate_reward() done = True else: reward_val = 0 done = False - # in case the Qiskit.QuantumCircuit has unitary or u gates in it, decompose them (because otherwise qiskit will throw an error when applying the BasisTranslator - if self.state.count_ops().get("unitary"): # ty: ignore[invalid-argument-type] - self.state = self.state.decompose(gates_to_decompose="unitary") - - self.state._layout = self.layout # noqa: SLF001 obs = create_feature_dict(self.state) return obs, reward_val, done, False, {} @@ -256,7 +258,15 @@ def reset( self.layout = None - self.valid_actions = self.actions_opt_indices + self.actions_synthesis_indices + if self.mdp == "flexible": + self.valid_actions = ( + self.actions_synthesis_indices + + self.actions_mapping_indices + + self.actions_layout_indices + + self.actions_opt_indices + ) + else: + self.valid_actions = self.actions_synthesis_indices + self.actions_opt_indices self.error_occurred = False @@ -268,10 +278,14 @@ 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 + # TKET layout/optimization actions must not run after a Qiskit layout has been set + # (it is not clear how tket will handle the layout). TKET routing actions are + # designed to work after a Qiskit layout via PreProcessTKETRoutingAfterQiskitLayout. if self.layout is not None: action_mask = [ - action_mask[i] and self.action_set[i].origin != CompilationOrigin.TKET for i in range(len(action_mask)) + action_mask[i] + and (self.action_set[i].origin != CompilationOrigin.TKET or i in self.actions_routing_indices) + for i in range(len(action_mask)) ] if self.has_parameterized_gates or self.layout is not None: @@ -342,9 +356,16 @@ def _apply_qiskit_action(self, action: Action, action_index: int) -> QuantumCirc ): altered_qc = self._handle_qiskit_layout_postprocessing(action, pm, altered_qc) - elif action_index in self.actions_routing_indices and self.layout: + elif ( + action_index in self.actions_routing_indices and self.layout and pm.property_set["final_layout"] is not None + ): self.layout.final_layout = pm.property_set["final_layout"] + # BasisTranslator errors on unitary gates; decompose them immediately so + # the circuit is always in a consistent state after a Qiskit action. + if altered_qc.count_ops().get("unitary"): # ty: ignore[invalid-argument-type] + altered_qc = altered_qc.decompose(gates_to_decompose="unitary") + return altered_qc def _handle_qiskit_layout_postprocessing( @@ -357,8 +378,13 @@ def _handle_qiskit_layout_postprocessing( assert self.layout is not None altered_qc, _ = postprocess_vf2postlayout(altered_qc, post_layout, self.layout) elif action.name == "VF2Layout": - assert pm.property_set["VF2Layout_stop_reason"] == VF2LayoutStopReason.SOLUTION_FOUND - assert pm.property_set["layout"] + if pm.property_set["VF2Layout_stop_reason"] != VF2LayoutStopReason.SOLUTION_FOUND: + logger.warning( + "VF2Layout pass did not find a solution. Reason: %s", + pm.property_set["VF2Layout_stop_reason"], + ) + else: + assert pm.property_set["layout"] else: assert pm.property_set["layout"] @@ -385,7 +411,7 @@ def _apply_tket_action(self, action: Action, action_index: int) -> QuantumCircui qbs = tket_qc.qubits tket_qc.rename_units({qbs[i]: Qubit("q", i) for i in range(len(qbs))}) - altered_qc = tk_to_qiskit(tket_qc) + altered_qc = tk_to_qiskit(tket_qc, replace_implicit_swaps=True) if action_index in self.actions_routing_indices: assert self.layout is not None @@ -428,27 +454,126 @@ def _apply_bqskit_action(self, action: Action, action_index: int) -> QuantumCirc return bqskit_to_qiskit(bqskit_compiled_qc) - 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.operation_names) - check_nat_gates(self.state) - only_nat_gates = check_nat_gates.property_set["all_gates_in_basis"] + def is_circuit_laid_out(self, circuit: QuantumCircuit, layout: TranspileLayout | Layout) -> bool: + """True if every logical qubit in the circuit has a physical assignment.""" + if isinstance(layout, TranspileLayout): + # Use final_layout if available; otherwise fallback to initial_layout + layout = layout.final_layout or layout.initial_layout + + v2p = layout.get_virtual_bits() + return all(q in v2p for q in circuit.qubits) + + def is_circuit_synthesized(self, circuit: QuantumCircuit) -> bool: + """Check if the circuit uses only native gates of the device. + + Verifies that every gate name in the circuit is present in + ``device.operation_names``, equivalent to the ``GatesInBasis`` pass. + + Args: + circuit: QuantumCircuit to check. + + Returns: + True if all gates are native to the device. + """ + native_names = set(self.device.operation_names) + return all( + instr.operation.name in native_names or instr.operation.name in ("barrier", "measure") + for instr in circuit.data + ) + + def is_circuit_routed(self, circuit: QuantumCircuit, coupling_map: CouplingMap) -> bool: + """Check if a circuit is fully routed to the device, including directionality. - if not only_nat_gates: - actions = self.actions_synthesis_indices + self.actions_opt_indices - if self.layout is not None: - actions += self.actions_routing_indices - return actions + A circuit is considered routed if all two-qubit gates are on qubit pairs + that exist as directed edges in the device coupling map. - check_mapping = CheckMap(coupling_map=self.device.build_coupling_map()) - check_mapping(self.state) - mapped = check_mapping.property_set["is_swap_mapped"] + After a layout pass the circuit's qubits are already physical qubits, so + ``circuit.find_bit(q).index`` gives the physical index directly — + consistent with how ``reward.py`` looks up gate calibrations. - if mapped and self.layout is not None: # The circuit is correctly mapped. - return [self.action_terminate_index, *self.actions_opt_indices] + Args: + circuit: QuantumCircuit to check. + coupling_map: CouplingMap of the target device. - if self.layout is not None: # The circuit is not yet mapped but a layout is set. - return self.actions_routing_indices + Returns: + True if fully routed, False otherwise. + """ + directed_edges = set(coupling_map.get_edges()) + for instr in circuit.data: + if len(instr.qubits) == 2: + q0 = circuit.find_bit(instr.qubits[0]).index + q1 = circuit.find_bit(instr.qubits[1]).index + if (q0, q1) not in directed_edges: + return False + return True - # No layout applied yet - return self.actions_mapping_indices + self.actions_layout_indices + self.actions_opt_indices + def determine_valid_actions_for_state(self) -> list[int]: + """Determine valid actions based on circuit state: synthesized, mapped, routed.""" + synthesized = self.is_circuit_synthesized(self.state) + laid_out = self.is_circuit_laid_out(self.state, self.layout) if self.layout else False + # Routing is only allowed after layout + routed = ( + self.is_circuit_routed(self.state, CouplingMap(self.device.build_coupling_map())) if laid_out else False + ) + + actions = [] + + # Initial state + if not synthesized and not laid_out and not routed: + if self.mdp == "flexible": + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_mapping_indices) + actions.extend(self.actions_layout_indices) + actions.extend(self.actions_opt_indices) + if self.mdp == "paper": + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_opt_indices) + + if synthesized and not laid_out and not routed: + if self.mdp == "flexible": + actions.extend(self.actions_mapping_indices) + actions.extend(self.actions_layout_indices) + actions.extend(self.actions_opt_indices) + if self.mdp == "paper": + actions.extend(self.actions_mapping_indices) + actions.extend(self.actions_layout_indices) + actions.extend(self.actions_opt_indices) + + # Not *depicted* in paper; necessary because optimization can destroy the native gate set + if not synthesized and laid_out and not routed: + if self.mdp == "flexible": + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_routing_indices) + actions.extend(self.actions_opt_indices) + if self.mdp == "paper": + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_routing_indices) + actions.extend(self.actions_opt_indices) + + # Not *depicted* in paper; necessary because of layout-only passes + if synthesized and laid_out and not routed: + if self.mdp == "flexible": + actions.extend(self.actions_routing_indices) + actions.extend(self.actions_opt_indices) + if self.mdp == "paper": + actions.extend(self.actions_routing_indices) + + # Not *depicted* in paper; necessary because routing can insert non-native SWAPs + if not synthesized and laid_out and routed: + if self.mdp == "flexible": + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_opt_indices) + if self.mdp == "paper": + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_opt_indices) + + # Final state + if synthesized and laid_out and routed: + if self.mdp == "flexible": + actions.extend([self.action_terminate_index]) + actions.extend(self.actions_opt_indices) + if self.mdp == "paper": + actions.extend([self.action_terminate_index]) + actions.extend(self.actions_opt_indices) + + return actions diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 9aa33bb6f..150363ea3 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -241,7 +241,7 @@ def test_tket_routing(available_actions_dict: dict[PassType, list[Action]]) -> N qubit_map = {qbs[i]: Qubit("q", i) for i in range(len(qbs))} tket_qc.rename_units(qubit_map) - mapped_qc = tk_to_qiskit(tket_qc) + mapped_qc = tk_to_qiskit(tket_qc, replace_implicit_swaps=True) final_layout = final_layout_pytket_to_qiskit(tket_qc, mapped_qc) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 5aa086d3c..798da1da2 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -85,7 +85,7 @@ def test_qcompile_with_newly_trained_models() -> None: rl_compile(qc, device=device, figure_of_merit=figure_of_merit) predictor.train_model( - timesteps=100, + timesteps=1000, test=True, ) From 68eb33836582af60e79bfa4f943ac615646aa1c1 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 11 May 2026 16:56:31 +0200 Subject: [PATCH 02/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20seed=20and=20tra?= =?UTF-8?q?ining=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/predictor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mqt/predictor/rl/predictor.py b/src/mqt/predictor/rl/predictor.py index 1f75b1901..0385c563f 100644 --- a/src/mqt/predictor/rl/predictor.py +++ b/src/mqt/predictor/rl/predictor.py @@ -97,14 +97,13 @@ def train_model( verbose: The verbosity level. Defaults to 2. test: Whether to train the model for testing purposes. Defaults to False. """ + set_random_seed(0) # for reproducibility if test: - set_random_seed(0) # for reproducibility - n_steps = 32 - n_epochs = 2 - batch_size = 8 + n_steps = 1000 + n_epochs = 1 + batch_size = 32 progress_bar = False else: - set_random_seed(0) # default PPO values n_steps = 2048 n_epochs = 10 From f9de637e49d23442d36d7974dbf3271d173da006 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 11 May 2026 17:12:47 +0200 Subject: [PATCH 03/42] =?UTF-8?q?=F0=9F=8E=A8=20adjust=20test=20step=20lim?= =?UTF-8?q?its?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/predictor.py | 4 ++-- src/mqt/predictor/rl/predictorenv.py | 2 +- tests/compilation/test_predictor_rl.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mqt/predictor/rl/predictor.py b/src/mqt/predictor/rl/predictor.py index 0385c563f..0f3c8c540 100644 --- a/src/mqt/predictor/rl/predictor.py +++ b/src/mqt/predictor/rl/predictor.py @@ -99,9 +99,9 @@ def train_model( """ set_random_seed(0) # for reproducibility if test: - n_steps = 1000 + n_steps = 512 n_epochs = 1 - batch_size = 32 + batch_size = 16 progress_bar = False else: # default PPO values diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 4d03fb1ae..1693887d6 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -369,7 +369,7 @@ def _apply_qiskit_action(self, action: Action, action_index: int) -> QuantumCirc # BasisTranslator errors on unitary gates; decompose them immediately so # the circuit is always in a consistent state after a Qiskit action. - if altered_qc.count_ops().get("unitary"): # ty: ignore[invalid-argument-type] + if altered_qc.count_ops().get("unitary"): altered_qc = altered_qc.decompose(gates_to_decompose="unitary") return altered_qc diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 798da1da2..6a323c528 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -85,7 +85,6 @@ def test_qcompile_with_newly_trained_models() -> None: rl_compile(qc, device=device, figure_of_merit=figure_of_merit) predictor.train_model( - timesteps=1000, test=True, ) From 55e5e0861969a01a9d3a4e9c6163de3fb44c9e7c Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 11 May 2026 17:22:38 +0200 Subject: [PATCH 04/42] =?UTF-8?q?=E2=8F=AA=20revert=20unrelated=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/predictorenv.py | 43 +--------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 1693887d6..f126f8a5c 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -69,7 +69,6 @@ class PredictorEnv(Env): def __init__( self, device: Target, - mdp: str = "paper", reward_function: figure_of_merit = "expected_fidelity", path_training_circuits: Path | None = None, ) -> None: @@ -77,7 +76,6 @@ def __init__( Arguments: device: The target device to be used for compilation. - mdp: The MDP transition policy. "paper" (default) enforces a strict, linear pipeline (synthesis -> (layout->routing) / mapping), while "flexible" allows for a cyclical approach where actions can be interleaved or reversed. reward_function: The figure of merit to be used for the reward function. Defaults to "expected_fidelity". path_training_circuits: The path to the training circuits folder. Defaults to None, which uses the default path. @@ -98,9 +96,6 @@ def __init__( self.used_actions: list[str] = [] self.device = device - logger.info("MDP: " + mdp) - self.mdp = mdp - # check for uni-directional coupling map coupling_set = {tuple(pair) for pair in self.device.build_coupling_map()} if any((b, a) not in coupling_set for (a, b) in coupling_set): @@ -264,15 +259,7 @@ def reset( self.layout = None - if self.mdp == "flexible": - self.valid_actions = ( - self.actions_synthesis_indices - + self.actions_mapping_indices - + self.actions_layout_indices - + self.actions_opt_indices - ) - else: - self.valid_actions = self.actions_synthesis_indices + self.actions_opt_indices + self.valid_actions = self.actions_synthesis_indices + self.actions_opt_indices self.error_occurred = False @@ -526,59 +513,31 @@ def determine_valid_actions_for_state(self) -> list[int]: # Initial state if not synthesized and not laid_out and not routed: - if self.mdp == "flexible": - actions.extend(self.actions_synthesis_indices) - actions.extend(self.actions_mapping_indices) - actions.extend(self.actions_layout_indices) - actions.extend(self.actions_opt_indices) - if self.mdp == "paper": actions.extend(self.actions_synthesis_indices) actions.extend(self.actions_opt_indices) if synthesized and not laid_out and not routed: - if self.mdp == "flexible": - actions.extend(self.actions_mapping_indices) - actions.extend(self.actions_layout_indices) - actions.extend(self.actions_opt_indices) - if self.mdp == "paper": actions.extend(self.actions_mapping_indices) actions.extend(self.actions_layout_indices) actions.extend(self.actions_opt_indices) # Not *depicted* in paper; necessary because optimization can destroy the native gate set if not synthesized and laid_out and not routed: - if self.mdp == "flexible": - actions.extend(self.actions_synthesis_indices) - actions.extend(self.actions_routing_indices) - actions.extend(self.actions_opt_indices) - if self.mdp == "paper": actions.extend(self.actions_synthesis_indices) actions.extend(self.actions_routing_indices) actions.extend(self.actions_opt_indices) # Not *depicted* in paper; necessary because of layout-only passes if synthesized and laid_out and not routed: - if self.mdp == "flexible": - actions.extend(self.actions_routing_indices) - actions.extend(self.actions_opt_indices) - if self.mdp == "paper": actions.extend(self.actions_routing_indices) # Not *depicted* in paper; necessary because routing can insert non-native SWAPs if not synthesized and laid_out and routed: - if self.mdp == "flexible": - actions.extend(self.actions_synthesis_indices) - actions.extend(self.actions_opt_indices) - if self.mdp == "paper": actions.extend(self.actions_synthesis_indices) actions.extend(self.actions_opt_indices) # Final state if synthesized and laid_out and routed: - if self.mdp == "flexible": - actions.extend([self.action_terminate_index]) - actions.extend(self.actions_opt_indices) - if self.mdp == "paper": actions.extend([self.action_terminate_index]) actions.extend(self.actions_opt_indices) From ba9042d7555dcdc05a50e60df7435f753036bb7e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 15:24:21 +0000 Subject: [PATCH 05/42] =?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/predictorenv.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index f126f8a5c..a7e001737 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -513,32 +513,32 @@ def determine_valid_actions_for_state(self) -> list[int]: # Initial state if not synthesized and not laid_out and not routed: - actions.extend(self.actions_synthesis_indices) - actions.extend(self.actions_opt_indices) + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_opt_indices) if synthesized and not laid_out and not routed: - actions.extend(self.actions_mapping_indices) - actions.extend(self.actions_layout_indices) - actions.extend(self.actions_opt_indices) + actions.extend(self.actions_mapping_indices) + actions.extend(self.actions_layout_indices) + actions.extend(self.actions_opt_indices) # Not *depicted* in paper; necessary because optimization can destroy the native gate set if not synthesized and laid_out and not routed: - actions.extend(self.actions_synthesis_indices) - actions.extend(self.actions_routing_indices) - actions.extend(self.actions_opt_indices) + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_routing_indices) + actions.extend(self.actions_opt_indices) # Not *depicted* in paper; necessary because of layout-only passes if synthesized and laid_out and not routed: - actions.extend(self.actions_routing_indices) + actions.extend(self.actions_routing_indices) # Not *depicted* in paper; necessary because routing can insert non-native SWAPs if not synthesized and laid_out and routed: - actions.extend(self.actions_synthesis_indices) - actions.extend(self.actions_opt_indices) + actions.extend(self.actions_synthesis_indices) + actions.extend(self.actions_opt_indices) # Final state if synthesized and laid_out and routed: - actions.extend([self.action_terminate_index]) - actions.extend(self.actions_opt_indices) + actions.extend([self.action_terminate_index]) + actions.extend(self.actions_opt_indices) return actions From dca4827a21eff19adc1feb3f2e377e907e3de51c Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 11 May 2026 17:57:54 +0200 Subject: [PATCH 06/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/predictorenv.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index a7e001737..684cb28ff 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -145,9 +145,13 @@ def __init__( self.reward_function = reward_function self.action_space = Discrete(len(self.action_set.keys())) self.num_steps = 0 - self.layout: TranspileLayout | None = None self.num_qubits_uncompiled_circuit = 0 + # Canonical layout state for the current circuit. It is mirrored to + # QuantumCircuit.layout for callers, but kept here because TKET and + # BQSKit conversions do not preserve Qiskit's layout metadata. + self.layout: TranspileLayout | None = None + self.has_parameterized_gates = False self.rng = np.random.default_rng(10) @@ -204,11 +208,10 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any reward_val = 0 done = False - # in case the Qiskit.QuantumCircuit has unitary or u gates in it, decompose them (because otherwise qiskit will throw an error when applying the BasisTranslator + # in case the Qiskit.QuantumCircuit has unitary or u gates in it, decompose them (because otherwise qiskit will throw an error when applying BasisTranslator if self.state.count_ops().get("unitary"): self.state = self.state.decompose(gates_to_decompose="unitary") - self.state._layout = self.layout # noqa: SLF001 obs = create_feature_dict(self.state) return obs, reward_val, done, False, {} @@ -271,19 +274,21 @@ 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] - # TKET layout/optimization actions must not run after a Qiskit layout has been set - # (it is not clear how tket will handle the layout). TKET routing actions are - # designed to work after a Qiskit layout via PreProcessTKETRoutingAfterQiskitLayout. - if self.layout is not None: + has_layout = self.layout is not None + + if has_layout: + # TKET layout/optimization actions must not run after a Qiskit layout has been set + # (it is not clear how tket will handle the layout). TKET routing actions are + # designed to work after a Qiskit layout via PreProcessTKETRoutingAfterQiskitLayout. action_mask = [ action_mask[i] and (self.action_set[i].origin != CompilationOrigin.TKET or i in self.actions_routing_indices) for i in range(len(action_mask)) ] - if self.has_parameterized_gates or self.layout is not None: + if self.has_parameterized_gates or has_layout: # remove all actions that are from "origin"=="bqskit" because they are not supported for parameterized gates - # or after layout since using BQSKit after a layout is set may result in an error + # or after layout since using BQSKit after a layout is set can result in an error action_mask = [ action_mask[i] and self.action_set[i].origin != CompilationOrigin.BQSKIT for i in range(len(action_mask)) @@ -382,6 +387,8 @@ def _handle_qiskit_layout_postprocessing( assert pm.property_set["layout"] if pm.property_set["layout"]: + # Layout/mapping passes create the base logical-to-physical mapping; + # later routing actions only update final_layout. self.layout = TranspileLayout( initial_layout=pm.property_set["layout"], input_qubit_mapping=pm.property_set["original_qubit_indices"], From 1e523e150977c5e1a89e0aabdfe64cc15ddf296e Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 11 May 2026 18:13:02 +0200 Subject: [PATCH 07/42] =?UTF-8?q?=E2=9C=85=20fix=20synthesis=20size=20limi?= =?UTF-8?q?t=20for=20bqskit=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mqt/predictor/rl/actions.py b/src/mqt/predictor/rl/actions.py index 118f48ef9..aabd06c6b 100644 --- a/src/mqt/predictor/rl/actions.py +++ b/src/mqt/predictor/rl/actions.py @@ -333,7 +333,7 @@ def remove_action(name: str) -> None: circuit, optimization_level=1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, synthesis_epsilon=1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, - max_synthesis_size=2 if os.getenv("GITHUB_ACTIONS") == "true" else 3, + max_synthesis_size=3, seed=10, num_workers=1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, ), @@ -432,7 +432,7 @@ def remove_action(name: str) -> None: with_mapping=True, optimization_level=1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, synthesis_epsilon=1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, - max_synthesis_size=2 if os.getenv("GITHUB_ACTIONS") == "true" and sys.platform != "linux" else 3, + max_synthesis_size=3, seed=10, num_workers=1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, ) @@ -462,7 +462,7 @@ def remove_action(name: str) -> None: model=MachineModel(bqskit_circuit.num_qudits, gate_set=get_bqskit_native_gates(device)), optimization_level=1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, synthesis_epsilon=1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, - max_synthesis_size=2 if os.getenv("GITHUB_ACTIONS") == "true" and sys.platform != "linux" else 3, + max_synthesis_size=3, seed=10, num_workers=1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, ) From 6d6487a915c917d70217bc2dfceb49e922a46087 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 16:13:21 +0000 Subject: [PATCH 08/42] =?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/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mqt/predictor/rl/actions.py b/src/mqt/predictor/rl/actions.py index aabd06c6b..598dfe0e6 100644 --- a/src/mqt/predictor/rl/actions.py +++ b/src/mqt/predictor/rl/actions.py @@ -11,7 +11,6 @@ from __future__ import annotations import os -import sys from collections import defaultdict from dataclasses import dataclass from enum import Enum From 7a300a206f23ef5af369ec13591c25fa4ee06206 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 13:27:06 +0200 Subject: [PATCH 09/42] =?UTF-8?q?=F0=9F=8E=A8=20reduce=20test=20training?= =?UTF-8?q?=20overhead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/predictor.py | 5 +++-- tests/compilation/test_predictor_rl.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mqt/predictor/rl/predictor.py b/src/mqt/predictor/rl/predictor.py index 0f3c8c540..2baa3da49 100644 --- a/src/mqt/predictor/rl/predictor.py +++ b/src/mqt/predictor/rl/predictor.py @@ -99,9 +99,10 @@ def train_model( """ set_random_seed(0) # for reproducibility if test: - n_steps = 512 + # minimum training overhead + n_steps = max(timesteps, 2) n_epochs = 1 - batch_size = 16 + batch_size = n_steps progress_bar = False else: # default PPO values diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 6a323c528..9ead59f3b 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -84,9 +84,7 @@ def test_qcompile_with_newly_trained_models() -> None: ): rl_compile(qc, device=device, figure_of_merit=figure_of_merit) - predictor.train_model( - test=True, - ) + predictor.train_model(test=True) qc_compiled, compilation_information = rl_compile(qc, device=device, figure_of_merit=figure_of_merit) From d64a97f97272ac8f79c39d00582d78e1bb32abc9 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 13:41:18 +0200 Subject: [PATCH 10/42] =?UTF-8?q?=F0=9F=8E=A8=20add=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/predictorenv.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 684cb28ff..aa57e1af0 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -92,7 +92,7 @@ def __init__( self.actions_routing_indices = [] self.actions_mapping_indices = [] self.actions_opt_indices = [] - self.actions_final_optimization_indices = [] + self.actions_final_optimization_indices = [] # TODO: currently not used; will be improved by addressing issue https://github.com/munich-quantum-toolkit/predictor/issues/666 self.used_actions: list[str] = [] self.device = device @@ -193,6 +193,11 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any self.state: QuantumCircuit = altered_qc self.num_steps += 1 + # in case a Qiskit.QuantumCircuit has `unitary` or `u`` gates in it, decompose them (otherwise qiskit will throw an error when applying BasisTranslator) + # TODO: will be improved by addressing issue https://github.com/munich-quantum-toolkit/predictor/issues/668 + if self.state.count_ops().get("unitary"): + self.state = self.state.decompose(gates_to_decompose="unitary") + self.state._layout = self.layout # noqa: SLF001 self.valid_actions = self.determine_valid_actions_for_state() @@ -208,10 +213,6 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any reward_val = 0 done = False - # in case the Qiskit.QuantumCircuit has unitary or u gates in it, decompose them (because otherwise qiskit will throw an error when applying BasisTranslator - if self.state.count_ops().get("unitary"): - self.state = self.state.decompose(gates_to_decompose="unitary") - obs = create_feature_dict(self.state) return obs, reward_val, done, False, {} From 9f2697e6b520ce5edf6e4ce59cd47d803ac9df90 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 13:43:41 +0200 Subject: [PATCH 11/42] =?UTF-8?q?=F0=9F=8E=A8=20reduce=20number=20of=20tra?= =?UTF-8?q?ining=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_predictor_rl.py | 2 +- .../hellinger_distance/test_estimated_hellinger_distance.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 9ead59f3b..9fb68914b 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -84,7 +84,7 @@ def test_qcompile_with_newly_trained_models() -> None: ): rl_compile(qc, device=device, figure_of_merit=figure_of_merit) - predictor.train_model(test=True) + predictor.train_model(timesteps=512, test=True) qc_compiled, compilation_information = rl_compile(qc, device=device, figure_of_merit=figure_of_merit) diff --git a/tests/hellinger_distance/test_estimated_hellinger_distance.py b/tests/hellinger_distance/test_estimated_hellinger_distance.py index 1d08c6189..30635cc4b 100644 --- a/tests/hellinger_distance/test_estimated_hellinger_distance.py +++ b/tests/hellinger_distance/test_estimated_hellinger_distance.py @@ -196,10 +196,7 @@ def test_train_and_qcompile_with_hellinger_model(source_path: Path, target_path: # 1. Train the reinforcement learning model for circuit compilation rl_predictor = rl_Predictor(device=device, figure_of_merit=figure_of_merit) - rl_predictor.train_model( - timesteps=5, - test=True, - ) + rl_predictor.train_model(timesteps=5, test=True) # 2. Setup and train the machine learning model for device selection ml_predictor = ml_Predictor(devices=[device], figure_of_merit=figure_of_merit) From f685fdd627197a61ebf9e7f32caf6b1cfdb86622 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 15:36:01 +0200 Subject: [PATCH 12/42] =?UTF-8?q?=F0=9F=9A=9A=20move=20actions=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions.py | 487 ------------------ .../predictor/rl/actions/bqskit_actions.py | 268 ++++++++++ .../predictor/rl/actions/qiskit_actions.py | 351 +++++++++++++ src/mqt/predictor/rl/actions/tket_actions.py | 139 +++++ 4 files changed, 758 insertions(+), 487 deletions(-) delete mode 100644 src/mqt/predictor/rl/actions.py create mode 100644 src/mqt/predictor/rl/actions/bqskit_actions.py create mode 100644 src/mqt/predictor/rl/actions/qiskit_actions.py create mode 100644 src/mqt/predictor/rl/actions/tket_actions.py diff --git a/src/mqt/predictor/rl/actions.py b/src/mqt/predictor/rl/actions.py deleted file mode 100644 index 598dfe0e6..000000000 --- a/src/mqt/predictor/rl/actions.py +++ /dev/null @@ -1,487 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -"""This module provides the actions that can be used in the reinforcement learning environment.""" - -from __future__ import annotations - -import os -from collections import defaultdict -from dataclasses import dataclass -from enum import Enum -from typing import TYPE_CHECKING, cast - -from bqskit import MachineModel -from bqskit import compile as bqskit_compile -from pytket.architecture import Architecture -from pytket.passes import ( - CliffordSimp, - FullPeepholeOptimise, - PeepholeOptimise2Q, - RemoveRedundancies, - RoutingPass, -) -from qiskit.circuit import StandardEquivalenceLibrary -from qiskit.circuit.library import ( - CXGate, - CYGate, - CZGate, - ECRGate, - HGate, - SdgGate, - SGate, - SwapGate, - SXdgGate, - SXGate, - TdgGate, - TGate, - XGate, - YGate, - ZGate, -) -from qiskit.passmanager import ConditionalController -from qiskit.transpiler import ( - CouplingMap, -) -from qiskit.transpiler.passes import ( - ApplyLayout, - BasisTranslator, - Collect2qBlocks, - CommutativeCancellation, - CommutativeInverseCancellation, - ConsolidateBlocks, - DenseLayout, - Depth, - EnlargeWithAncilla, - FixedPoint, - FullAncillaAllocation, - GatesInBasis, - InverseCancellation, - MinimumPoint, - Optimize1qGatesDecomposition, - OptimizeCliffords, - RemoveDiagonalGatesBeforeMeasure, - SabreLayout, - Size, - UnitarySynthesis, - VF2Layout, - VF2PostLayout, -) -from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason -from qiskit.transpiler.preset_passmanagers import common - -from mqt.predictor.rl.parsing import ( - PreProcessTKETRoutingAfterQiskitLayout, - get_bqskit_native_gates, -) - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any - - from bqskit import Circuit - from pytket._tket.passes import BasePass as TketBasePass - from qiskit.passmanager.base_tasks import Task - - TaskList = list[Task | TketBasePass | PreProcessTKETRoutingAfterQiskitLayout] - from qiskit.passmanager import PropertySet - - -class CompilationOrigin(str, Enum): - """Enumeration of the origin of the compilation action.""" - - QISKIT = "qiskit" - TKET = "tket" - BQSKIT = "bqskit" - GENERAL = "general" - - -class PassType(str, Enum): - """Enumeration of the type of compilation pass.""" - - OPT = "optimization" - SYNTHESIS = "synthesis" - MAPPING = "mapping" - LAYOUT = "layout" - ROUTING = "routing" - FINAL_OPT = "final_optimization" - TERMINATE = "terminate" - - -@dataclass -class Action: - """Base class for all actions in the reinforcement learning environment.""" - - name: str - origin: CompilationOrigin - pass_type: PassType - transpile_pass: ( - TaskList - | Callable[..., TaskList] - | Callable[ - ..., - Callable[..., tuple[Any, ...] | Circuit], - ] - ) - - -@dataclass -class DeviceIndependentAction(Action): - """Action that represents a static compilation pass that can be applied directly.""" - - -@dataclass -class DeviceDependentAction(Action): - """Action that represents a device-specific compilation pass that can be applied to a specific device.""" - - transpile_pass: ( - Callable[..., TaskList] - | Callable[ - ..., - Callable[..., tuple[Any, ...] | Circuit], - ] - ) - do_while: Callable[[PropertySet], bool] | None = None - - -# Registry of actions -_ACTIONS: dict[str, Action] = {} - - -def register_action(action: Action) -> Action: - """Registers a new action in the global actions registry. - - Raises: - ValueError: If an action with the same name is already registered. - """ - if action.name in _ACTIONS: - msg = f"Action with name {action.name} already registered." - raise ValueError(msg) - _ACTIONS[action.name] = action - return action - - -def remove_action(name: str) -> None: - """Removes an action from the global actions registry by name. - - Raises: - ValueError: If no action with the given name is registered. - """ - if name not in _ACTIONS: - msg = f"No action with name {name} is registered." - raise KeyError(msg) - del _ACTIONS[name] - - -register_action( - DeviceIndependentAction( - "Optimize1qGatesDecomposition", - CompilationOrigin.QISKIT, - PassType.OPT, - [Optimize1qGatesDecomposition()], - ) -) - -register_action( - DeviceIndependentAction( - "CommutativeCancellation", - CompilationOrigin.QISKIT, - PassType.OPT, - [CommutativeCancellation()], - ) -) - -register_action( - DeviceIndependentAction( - "CommutativeInverseCancellation", - CompilationOrigin.QISKIT, - PassType.OPT, - [CommutativeInverseCancellation()], - ) -) - -register_action( - DeviceIndependentAction( - "RemoveDiagonalGatesBeforeMeasure", - CompilationOrigin.QISKIT, - PassType.OPT, - [RemoveDiagonalGatesBeforeMeasure()], - ) -) - -register_action( - DeviceIndependentAction( - "InverseCancellation", - CompilationOrigin.QISKIT, - PassType.OPT, - [ - InverseCancellation([ - CXGate(), - ECRGate(), - CZGate(), - CYGate(), - XGate(), - YGate(), - ZGate(), - HGate(), - SwapGate(), - (TGate(), TdgGate()), - (SGate(), SdgGate()), - (SXGate(), SXdgGate()), - ]) - ], - ) -) - -register_action( - DeviceIndependentAction( - "OptimizeCliffords", - CompilationOrigin.QISKIT, - PassType.OPT, - [OptimizeCliffords()], - ) -) - -register_action( - DeviceIndependentAction( - "Opt2qBlocks", - CompilationOrigin.QISKIT, - PassType.OPT, - [Collect2qBlocks(), ConsolidateBlocks(), UnitarySynthesis()], - ) -) - -register_action( - DeviceIndependentAction( - "PeepholeOptimise2Q", - CompilationOrigin.TKET, - PassType.OPT, - [PeepholeOptimise2Q()], - ) -) - -register_action( - DeviceIndependentAction( - "CliffordSimp", - CompilationOrigin.TKET, - PassType.OPT, - [CliffordSimp()], - ) -) - -register_action( - DeviceIndependentAction( - "FullPeepholeOptimiseCX", - CompilationOrigin.TKET, - PassType.OPT, - [FullPeepholeOptimise()], - ) -) - -register_action( - DeviceIndependentAction( - "RemoveRedundancies", - CompilationOrigin.TKET, - PassType.OPT, - [RemoveRedundancies()], - ) -) - -register_action( - DeviceDependentAction( - "QiskitO3", - CompilationOrigin.QISKIT, - PassType.OPT, - transpile_pass=lambda native_gate, coupling_map: cast( - "TaskList", - [ - Collect2qBlocks(), - 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( - common.generate_translation_passmanager( - target=None, basis_gates=native_gate, coupling_map=coupling_map - ).to_flow_controller(), - condition=lambda property_set: not property_set["all_gates_in_basis"], - ), - Depth(recurse=True), - FixedPoint("depth"), - Size(recurse=True), - FixedPoint("size"), - MinimumPoint(["depth", "size"], "optimization_loop"), - ], - ), - do_while=lambda property_set: not property_set["optimization_loop_minimum_point"], - ) -) - -register_action( - DeviceDependentAction( - "BQSKitO2", - CompilationOrigin.BQSKIT, - PassType.OPT, - transpile_pass=lambda circuit: bqskit_compile( - circuit, - optimization_level=1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, - synthesis_epsilon=1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, - max_synthesis_size=3, - seed=10, - num_workers=1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, - ), - ) -) - -register_action( - DeviceDependentAction( - "VF2PostLayout", - CompilationOrigin.QISKIT, - PassType.FINAL_OPT, - transpile_pass=lambda device: VF2PostLayout(target=device), - ) -) - -register_action( - DeviceDependentAction( - "DenseLayout", - CompilationOrigin.QISKIT, - PassType.LAYOUT, - transpile_pass=lambda device: cast( - "TaskList", - [ - DenseLayout(coupling_map=CouplingMap(device.build_coupling_map())), - FullAncillaAllocation(coupling_map=CouplingMap(device.build_coupling_map())), - EnlargeWithAncilla(), - ApplyLayout(), - ], - ), - ) -) - -register_action( - DeviceDependentAction( - "VF2Layout", - CompilationOrigin.QISKIT, - PassType.LAYOUT, - transpile_pass=lambda device: cast( - "TaskList", - [ - VF2Layout(target=device), - ConditionalController( - [ - FullAncillaAllocation(coupling_map=CouplingMap(device.build_coupling_map())), - EnlargeWithAncilla(), - ApplyLayout(), - ], - condition=lambda property_set: ( - property_set["VF2Layout_stop_reason"] == VF2LayoutStopReason.SOLUTION_FOUND - ), - ), - ], - ), - ) -) - -register_action( - DeviceDependentAction( - "RoutingPass", - CompilationOrigin.TKET, - PassType.ROUTING, - transpile_pass=lambda device: cast( - "TaskList", - [ - PreProcessTKETRoutingAfterQiskitLayout(), - RoutingPass(Architecture(list(device.build_coupling_map()))), - ], - ), - ) -) - -register_action( - DeviceDependentAction( - "SabreMapping", - CompilationOrigin.QISKIT, - PassType.MAPPING, - transpile_pass=lambda device: cast( - "TaskList", [SabreLayout(coupling_map=CouplingMap(device.build_coupling_map()), skip_routing=False)] - ), - ) -) - -register_action( - DeviceDependentAction( - "BQSKitMapping", - CompilationOrigin.BQSKIT, - PassType.MAPPING, - transpile_pass=lambda device: ( - lambda bqskit_circuit: bqskit_compile( - bqskit_circuit, - model=MachineModel( - num_qudits=device.num_qubits, - gate_set=get_bqskit_native_gates(device), - coupling_graph=[(elem[0], elem[1]) for elem in device.build_coupling_map()], - ), - with_mapping=True, - optimization_level=1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, - synthesis_epsilon=1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, - max_synthesis_size=3, - seed=10, - num_workers=1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, - ) - ), - ) -) - -register_action( - DeviceDependentAction( - "BasisTranslator", - CompilationOrigin.QISKIT, - PassType.SYNTHESIS, - transpile_pass=lambda device: cast( - "TaskList", [BasisTranslator(StandardEquivalenceLibrary, target_basis=device.operation_names)] - ), - ) -) - -register_action( - DeviceDependentAction( - "BQSKitSynthesis", - CompilationOrigin.BQSKIT, - PassType.SYNTHESIS, - 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=1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, - synthesis_epsilon=1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, - max_synthesis_size=3, - seed=10, - num_workers=1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, - ) - ), - ) -) - -register_action( - DeviceIndependentAction( - "terminate", - CompilationOrigin.GENERAL, - PassType.TERMINATE, - transpile_pass=[], - ) -) - - -def get_actions_by_pass_type() -> dict[PassType, list[Action]]: - """Returns a dictionary mapping each PassType to a list of Actions of that type.""" - result: dict[PassType, list[Action]] = defaultdict(list) - for action in _ACTIONS.values(): - result[action.pass_type].append(action) - return result diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py new file mode 100644 index 000000000..c0e795138 --- /dev/null +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -0,0 +1,268 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""BQSKit actions and execution helpers.""" + +from __future__ import annotations + +import os +from collections.abc import Callable +from functools import cache +from typing import TYPE_CHECKING, cast + +from bqskit import MachineModel +from bqskit import compile as bqskit_compile +from bqskit.ext import qiskit_to_bqskit +from bqskit.ext.qiskit.translate import OPENQASM2Language +from bqskit.ir import gates +from qiskit import qasm2 +from qiskit.circuit import Instruction, QuantumRegister +from qiskit.circuit.library import RGate +from qiskit.transpiler import Layout, TranspileLayout + +from mqt.predictor.rl.actions import Action, CompilationOrigin, DeviceDependentAction, PassType + +if TYPE_CHECKING: + from bqskit import Circuit + from bqskit.ir import Gate + from qiskit import QuantumCircuit + from qiskit.transpiler import Target + + +def _r_gate(theta: float, phi: float) -> Instruction: + return RGate(theta, phi) + + +def _bqskit_compilation_options() -> dict[str, float | int]: + """Returns BQSKit options tuned for local runs and CI.""" + return { + "optimization_level": 1 if os.getenv("GITHUB_ACTIONS") == "true" else 2, + "synthesis_epsilon": 1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8, + "max_synthesis_size": 3, + "seed": 10, + "num_workers": 1 if os.getenv("GITHUB_ACTIONS") == "true" else -1, + } + + +def bqskit_to_qiskit(circuit: Circuit) -> QuantumCircuit: + """Convert a BQSKit circuit to Qiskit, including BQSKit's IQM-compatible U1q gate.""" + qasm = OPENQASM2Language().encode(circuit).replace("U1q(", "r(") + return qasm2.loads( + qasm, + custom_instructions=( + *qasm2.LEGACY_CUSTOM_INSTRUCTIONS, + qasm2.CustomInstruction( + "r", 2, 1, cast("Callable[[tuple[int | float, ...]], Instruction]", _r_gate), builtin=True + ), + ), + ) + + +def bqskit_optimization_action() -> Action: + """Returns the BQSKit optimization action.""" + return DeviceDependentAction( + "BQSKitO2", + CompilationOrigin.BQSKIT, + PassType.OPT, + transpile_pass=lambda circuit: bqskit_compile(circuit, **_bqskit_compilation_options()), + ) + + +def bqskit_mapping_action() -> Action: + """Returns the BQSKit mapping action.""" + return DeviceDependentAction( + "BQSKitMapping", + CompilationOrigin.BQSKIT, + PassType.MAPPING, + transpile_pass=lambda device: ( + lambda bqskit_circuit: bqskit_compile( + bqskit_circuit, + model=MachineModel( + num_qudits=device.num_qubits, + gate_set=get_bqskit_native_gates(device), + coupling_graph=[(elem[0], elem[1]) for elem in device.build_coupling_map()], + ), + with_mapping=True, + **_bqskit_compilation_options(), + ) + ), + ) + + +def bqskit_synthesis_action() -> Action: + """Returns the BQSKit synthesis action.""" + return DeviceDependentAction( + "BQSKitSynthesis", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: ( + lambda bqskit_circuit: bqskit_compile( + bqskit_circuit, + model=MachineModel(bqskit_circuit.num_qudits, gate_set=get_bqskit_native_gates(device)), + **_bqskit_compilation_options(), + ) + ), + ) + + +@cache +def get_bqskit_native_gates(device: Target) -> list[Gate]: + """Returns the native gates of the given device. + + Arguments: + device: The device for which the native gates are returned. + + Returns: + The native gates of the given device as BQSKit gates. + + Raises: + ValueError: If a gate in the device is not supported in BQSKit. + """ + gate_map = { + # --- 1-qubit gates --- + "id": gates.IdentityGate(), + "x": gates.XGate(), + "y": gates.YGate(), + "z": gates.ZGate(), + "h": gates.HGate(), + "s": gates.SGate(), + "sdg": gates.SdgGate(), + "t": gates.TGate(), + "tdg": gates.TdgGate(), + "sx": gates.SXGate(), + "rx": gates.RXGate(), + "ry": gates.RYGate(), + "rz": gates.RZGate(), + "r": gates.U1qGate(), + "u1": gates.U1Gate(), + "u2": gates.U2Gate(), + "u3": gates.U3Gate(), + # --- Controlled 1-qubit gates --- + "cx": gates.CXGate(), + "cy": gates.CYGate(), + "cz": gates.CZGate(), + "ch": gates.CHGate(), + "crx": gates.CRXGate(), + "cry": gates.CRYGate(), + "crz": gates.CRZGate(), + "cp": gates.CPGate(), + "cu": gates.CUGate(), + # --- 2-qubit gates --- + "swap": gates.SwapGate(), + "iswap": gates.ISwapGate(), + "ecr": gates.ECRGate(), + "rzz": gates.RZZGate(), + "rxx": gates.RXXGate(), + "ryy": gates.RYYGate(), + "zz": gates.ZZGate(), + # --- 3-qubit gates --- + "ccx": gates.CCXGate(), + # --- Others / approximations --- + "reset": gates.Reset(), + } + + native_gates = [] + + for instr in device.operation_names: + name = instr + + if name in [ + "barrier", + "measure", + "delay", + "for_loop", + "control", + "while_loop", + "if_test", + "if_else", + "switch_case", + "break", + "continue", + "box", + ]: + continue + + if name not in gate_map: + msg = f"The '{name}' gate of device '{device.description}' is not supported in BQSKIT." + raise ValueError(msg) + + native_gates.append(gate_map[name]) + + return native_gates + + +def final_layout_bqskit_to_qiskit( + bqskit_initial_layout: tuple[int, ...], + bqskit_final_layout: tuple[int, ...], + compiled_qc: QuantumCircuit, + initial_qc: QuantumCircuit, +) -> TranspileLayout: + """Converts a final layout from bqskit to qiskit. + + 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 = {} + counter_ancilla_qubit = 0 + 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[counter_ancilla_qubit] + counter_ancilla_qubit += 1 + + 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, + ) + + +def run_bqskit_action( + action: Action, + circuit: QuantumCircuit, + device: Target, + layout: TranspileLayout | None, +) -> tuple[QuantumCircuit, TranspileLayout | None]: + """Apply a BQSKit action and return the updated circuit and layout metadata.""" + bqskit_qc = qiskit_to_bqskit(circuit) + if action.pass_type == PassType.OPT: + transpile = cast("Callable[[Circuit], Circuit]", action.transpile_pass) + bqskit_compiled_qc = transpile(bqskit_qc) + elif action.pass_type == PassType.SYNTHESIS: + factory = cast("Callable[[Target], Callable[[Circuit], Circuit]]", action.transpile_pass) + bqskit_compiled_qc = factory(device)(bqskit_qc) + elif action.pass_type == PassType.MAPPING: + factory = cast( + "Callable[[Target], Callable[[Circuit], tuple[Circuit, tuple[int, ...], tuple[int, ...]]]]", + action.transpile_pass, + ) + bqskit_compiled_qc, initial, final = factory(device)(bqskit_qc) + compiled_qiskit_qc = bqskit_to_qiskit(bqskit_compiled_qc) + return compiled_qiskit_qc, final_layout_bqskit_to_qiskit(initial, final, compiled_qiskit_qc, circuit) + else: + msg = f"Unhandled BQSKit pass type: {action.pass_type}" + raise ValueError(msg) + + return bqskit_to_qiskit(bqskit_compiled_qc), layout diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py new file mode 100644 index 000000000..1b86eaa0f --- /dev/null +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -0,0 +1,351 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Qiskit actions and execution helpers.""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from typing import TYPE_CHECKING, cast + +from qiskit.circuit import StandardEquivalenceLibrary +from qiskit.circuit.library import ( + CXGate, + CYGate, + CZGate, + ECRGate, + HGate, + SdgGate, + SGate, + SwapGate, + SXdgGate, + SXGate, + TdgGate, + TGate, + XGate, + YGate, + ZGate, +) +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.passmanager import ConditionalController +from qiskit.passmanager.flow_controllers import DoWhileController +from qiskit.transpiler import CouplingMap, PassManager, TranspileLayout +from qiskit.transpiler.passes import ( + ApplyLayout, + BasisTranslator, + Collect2qBlocks, + CommutativeCancellation, + CommutativeInverseCancellation, + ConsolidateBlocks, + DenseLayout, + Depth, + EnlargeWithAncilla, + FixedPoint, + FullAncillaAllocation, + GatesInBasis, + InverseCancellation, + MinimumPoint, + Optimize1qGatesDecomposition, + OptimizeCliffords, + RemoveDiagonalGatesBeforeMeasure, + SabreLayout, + Size, + UnitarySynthesis, + VF2Layout, + VF2PostLayout, +) +from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason +from qiskit.transpiler.preset_passmanagers import common + +from mqt.predictor.rl.actions import ( + Action, + CompilationOrigin, + DeviceDependentAction, + DeviceIndependentAction, + PassType, +) + +if TYPE_CHECKING: + from qiskit import QuantumCircuit + from qiskit.passmanager import PropertySet + from qiskit.passmanager.base_tasks import Task + from qiskit.transpiler import Layout, Target + +logger = logging.getLogger("mqt-predictor") + + +def qiskit_optimization_actions() -> list[Action]: + """Returns the Qiskit optimization actions.""" + return [ + DeviceIndependentAction( + "Optimize1qGatesDecomposition", + CompilationOrigin.QISKIT, + PassType.OPT, + [Optimize1qGatesDecomposition()], + ), + DeviceIndependentAction( + "CommutativeCancellation", + CompilationOrigin.QISKIT, + PassType.OPT, + [CommutativeCancellation()], + ), + DeviceIndependentAction( + "CommutativeInverseCancellation", + CompilationOrigin.QISKIT, + PassType.OPT, + [CommutativeInverseCancellation()], + ), + DeviceIndependentAction( + "RemoveDiagonalGatesBeforeMeasure", + CompilationOrigin.QISKIT, + PassType.OPT, + [RemoveDiagonalGatesBeforeMeasure()], + ), + DeviceIndependentAction( + "InverseCancellation", + CompilationOrigin.QISKIT, + PassType.OPT, + [ + InverseCancellation([ + CXGate(), + ECRGate(), + CZGate(), + CYGate(), + XGate(), + YGate(), + ZGate(), + HGate(), + SwapGate(), + (TGate(), TdgGate()), + (SGate(), SdgGate()), + (SXGate(), SXdgGate()), + ]) + ], + ), + DeviceIndependentAction( + "OptimizeCliffords", + CompilationOrigin.QISKIT, + PassType.OPT, + [OptimizeCliffords()], + ), + DeviceIndependentAction( + "Opt2qBlocks", + CompilationOrigin.QISKIT, + PassType.OPT, + [Collect2qBlocks(), ConsolidateBlocks(), UnitarySynthesis()], + ), + ] + + +def qiskit_o3_action() -> Action: + """Returns the Qiskit level-3 optimization action.""" + return DeviceDependentAction( + "QiskitO3", + CompilationOrigin.QISKIT, + PassType.OPT, + transpile_pass=lambda native_gate, coupling_map: cast( + "list[Task]", + [ + Collect2qBlocks(), + 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( + common.generate_translation_passmanager( + target=None, basis_gates=native_gate, coupling_map=coupling_map + ).to_flow_controller(), + condition=lambda property_set: not property_set["all_gates_in_basis"], + ), + Depth(recurse=True), + FixedPoint("depth"), + Size(recurse=True), + FixedPoint("size"), + MinimumPoint(["depth", "size"], "optimization_loop"), + ], + ), + do_while=lambda property_set: not property_set["optimization_loop_minimum_point"], + ) + + +def qiskit_final_optimization_action() -> Action: + """Returns the Qiskit final layout optimization action.""" + return DeviceDependentAction( + "VF2PostLayout", + CompilationOrigin.QISKIT, + PassType.FINAL_OPT, + transpile_pass=lambda device: [VF2PostLayout(target=device)], + ) + + +def qiskit_layout_actions() -> list[Action]: + """Returns the Qiskit layout actions.""" + return [ + DeviceDependentAction( + "DenseLayout", + CompilationOrigin.QISKIT, + PassType.LAYOUT, + transpile_pass=lambda device: cast( + "list[Task]", + [ + DenseLayout(coupling_map=CouplingMap(device.build_coupling_map())), + FullAncillaAllocation(coupling_map=CouplingMap(device.build_coupling_map())), + EnlargeWithAncilla(), + ApplyLayout(), + ], + ), + ), + DeviceDependentAction( + "VF2Layout", + CompilationOrigin.QISKIT, + PassType.LAYOUT, + transpile_pass=lambda device: cast( + "list[Task]", + [ + VF2Layout(target=device), + ConditionalController( + [ + FullAncillaAllocation(coupling_map=CouplingMap(device.build_coupling_map())), + EnlargeWithAncilla(), + ApplyLayout(), + ], + condition=lambda property_set: ( + property_set["VF2Layout_stop_reason"] == VF2LayoutStopReason.SOLUTION_FOUND + ), + ), + ], + ), + ), + ] + + +def qiskit_mapping_action() -> Action: + """Returns the Qiskit mapping action.""" + return DeviceDependentAction( + "SabreMapping", + CompilationOrigin.QISKIT, + PassType.MAPPING, + transpile_pass=lambda device: cast( + "list[Task]", [SabreLayout(coupling_map=CouplingMap(device.build_coupling_map()), skip_routing=False)] + ), + ) + + +def qiskit_synthesis_action() -> Action: + """Returns the Qiskit synthesis action.""" + return DeviceDependentAction( + "BasisTranslator", + CompilationOrigin.QISKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: cast( + "list[Task]", [BasisTranslator(StandardEquivalenceLibrary, target_basis=device.operation_names)] + ), + ) + + +def postprocess_vf2postlayout( + qc: QuantumCircuit, post_layout: Layout, layout_before: TranspileLayout +) -> tuple[QuantumCircuit, ApplyLayout]: + """Postprocess a quantum circuit after VF2 layout assignment. + + Args: + qc: The quantum circuit to transform. + post_layout: The layout computed after routing. + layout_before: The layout before post-routing adjustment. + + Returns: + A tuple of the transformed circuit and the ApplyLayout used. + """ + apply_layout = ApplyLayout() + apply_layout.property_set["layout"] = layout_before.initial_layout + apply_layout.property_set["original_qubit_indices"] = layout_before.input_qubit_mapping + apply_layout.property_set["final_layout"] = layout_before.final_layout + apply_layout.property_set["post_layout"] = post_layout + + altered_qc = apply_layout.run(circuit_to_dag(qc)) + return dag_to_circuit(altered_qc), apply_layout + + +def _qiskit_passes(action: Action, device: Target, layout: TranspileLayout | None) -> list[Task]: + """Build the concrete Qiskit pass list for an action.""" + if action.name == "QiskitO3" and isinstance(action, DeviceDependentAction): + factory = cast("Callable[[list[str], CouplingMap | None], list[Task]]", action.transpile_pass) + return factory( + device.operation_names, + CouplingMap(device.build_coupling_map()) if layout else None, + ) + if callable(action.transpile_pass): + factory = cast("Callable[[Target], list[Task]]", action.transpile_pass) + return factory(device) + return cast("list[Task]", action.transpile_pass) + + +def _postprocess_layout_action( + action: Action, + property_set: PropertySet, + altered_qc: QuantumCircuit, + layout: TranspileLayout | None, + input_qubit_count: int, +) -> tuple[QuantumCircuit, TranspileLayout | None]: + """Update Qiskit's layout metadata after passes that can create or alter layouts.""" + if action.name == "VF2PostLayout": + assert property_set["VF2PostLayout_stop_reason"] is not None + post_layout = property_set["post_layout"] + if post_layout: + assert layout is not None + altered_qc, _ = postprocess_vf2postlayout(altered_qc, post_layout, layout) + elif action.name == "VF2Layout": + if property_set["VF2Layout_stop_reason"] != VF2LayoutStopReason.SOLUTION_FOUND: + logger.warning( + "VF2Layout pass did not find a solution. Reason: %s", + property_set["VF2Layout_stop_reason"], + ) + else: + assert property_set["layout"] + else: + assert property_set["layout"] + + if property_set["layout"]: + return altered_qc, TranspileLayout( + initial_layout=property_set["layout"], + input_qubit_mapping=property_set["original_qubit_indices"], + final_layout=property_set["final_layout"], + _output_qubit_list=altered_qc.qubits, + _input_qubit_count=input_qubit_count, + ) + return altered_qc, layout + + +def run_qiskit_action( + action: Action, + circuit: QuantumCircuit, + device: Target, + layout: TranspileLayout | None, + input_qubit_count: int, +) -> tuple[QuantumCircuit, TranspileLayout | None]: + """Apply a Qiskit action and return the updated circuit and layout metadata.""" + passes = _qiskit_passes(action, device, layout) + if action.name == "QiskitO3" and isinstance(action, DeviceDependentAction): + assert action.do_while is not None + pm = PassManager([DoWhileController(passes, do_while=action.do_while)]) + else: + pm = PassManager(passes) + + altered_qc = pm.run(circuit) + + if action.pass_type in {PassType.LAYOUT, PassType.MAPPING, PassType.FINAL_OPT}: + altered_qc, layout = _postprocess_layout_action(action, pm.property_set, altered_qc, layout, input_qubit_count) + elif action.pass_type == PassType.ROUTING and layout and pm.property_set["final_layout"] is not None: + layout.final_layout = pm.property_set["final_layout"] + + if altered_qc.count_ops().get("unitary"): + altered_qc = altered_qc.decompose(gates_to_decompose="unitary") + + return altered_qc, layout diff --git a/src/mqt/predictor/rl/actions/tket_actions.py b/src/mqt/predictor/rl/actions/tket_actions.py new file mode 100644 index 000000000..5ef1fb989 --- /dev/null +++ b/src/mqt/predictor/rl/actions/tket_actions.py @@ -0,0 +1,139 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""TKET actions and execution helpers.""" + +from __future__ import annotations + +import operator +from collections.abc import Callable +from typing import TYPE_CHECKING, cast + +from pytket import Qubit +from pytket.architecture import Architecture +from pytket.circuit import Node +from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit +from pytket.placement import place_with_map +from pytket.passes import CliffordSimp, FullPeepholeOptimise, PeepholeOptimise2Q, RemoveRedundancies, RoutingPass +from qiskit.transpiler import Layout + +from mqt.predictor.rl.actions import Action, CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, PassType + +if TYPE_CHECKING: + from pytket import Circuit + from pytket._tket.passes import BasePass as TketBasePass + from qiskit import QuantumCircuit + from qiskit.passmanager.base_tasks import Task + from qiskit.transpiler import Target, TranspileLayout + + +class PreProcessTKETRoutingAfterQiskitLayout: + """Pre-process TKET routing for circuits that already carry a Qiskit layout. + + Qiskit layout passes rewrite the circuit into physical-qubit order. Before + TKET routing can operate on that circuit, it needs an equivalent trivial + placement so the current wire order is treated as the starting placement. + """ + + def apply(self, circuit: Circuit) -> None: + """Applies the pre-processing step to route a circuit with tket after a Qiskit Layout pass has been applied.""" + mapping = {Qubit(i): Node(i) for i in range(circuit.n_qubits)} + place_with_map(circuit=circuit, qmap=mapping) + + +def tket_optimization_actions() -> list[Action]: + """Returns the TKET optimization actions.""" + return [ + DeviceIndependentAction( + "PeepholeOptimise2Q", + CompilationOrigin.TKET, + PassType.OPT, + [PeepholeOptimise2Q()], + ), + DeviceIndependentAction( + "CliffordSimp", + CompilationOrigin.TKET, + PassType.OPT, + [CliffordSimp()], + ), + DeviceIndependentAction( + "FullPeepholeOptimiseCX", + CompilationOrigin.TKET, + PassType.OPT, + [FullPeepholeOptimise()], + ), + DeviceIndependentAction( + "RemoveRedundancies", + CompilationOrigin.TKET, + PassType.OPT, + [RemoveRedundancies()], + ), + ] + + +def tket_routing_action() -> Action: + """Returns the TKET routing action.""" + return DeviceDependentAction( + "RoutingPass", + CompilationOrigin.TKET, + PassType.ROUTING, + transpile_pass=lambda device: cast( + "list[Task]", + [ + PreProcessTKETRoutingAfterQiskitLayout(), + RoutingPass(Architecture(list(device.build_coupling_map()))), + ], + ), + ) + + +def final_layout_pytket_to_qiskit(pytket_circuit: Circuit, qiskit_circuit: QuantumCircuit) -> Layout: + """Converts a final layout from pytket to qiskit.""" + pytket_layout = pytket_circuit.qubit_readout + size_circuit = pytket_circuit.n_qubits + qiskit_layout = {} + qiskit_qreg = qiskit_circuit.qregs[0] + + pytket_layout = dict(sorted(pytket_layout.items(), key=operator.itemgetter(1))) + + for node, qubit_index in pytket_layout.items(): + 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 Layout(input_dict=qiskit_layout) + + +def run_tket_action( + action: Action, + circuit: QuantumCircuit, + device: Target, + layout: TranspileLayout | None, +) -> tuple[QuantumCircuit, TranspileLayout | None]: + """Apply a TKET action and return the updated circuit and layout metadata.""" + tket_qc = qiskit_to_tk(circuit, preserve_param_uuid=True) + if callable(action.transpile_pass): + factory = cast("Callable[[Target], list[Task]]", action.transpile_pass) + passes = factory(device) + else: + passes = cast("list[Task]", action.transpile_pass) + for pass_ in passes: + assert isinstance(pass_, TketBasePass | PreProcessTKETRoutingAfterQiskitLayout) + pass_.apply(tket_qc) + + qbs = tket_qc.qubits + tket_qc.rename_units({qbs[i]: Qubit("q", i) for i in range(len(qbs))}) + altered_qc = tk_to_qiskit(tket_qc, replace_implicit_swaps=True) + + if action.pass_type == PassType.ROUTING: + assert layout is not None + layout.final_layout = final_layout_pytket_to_qiskit(tket_qc, altered_qc) + + return altered_qc, layout From 74a0f9a8288559f4ef4dd4c696bc975209adc8a7 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 15:36:40 +0200 Subject: [PATCH 13/42] =?UTF-8?q?=F0=9F=9A=9A=20move=20parsing=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 159 ++++++++++++++ src/mqt/predictor/rl/parsing.py | 268 ++--------------------- src/mqt/predictor/rl/predictorenv.py | 159 +++----------- 3 files changed, 204 insertions(+), 382 deletions(-) create mode 100644 src/mqt/predictor/rl/actions/__init__.py diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py new file mode 100644 index 000000000..2858d9ddd --- /dev/null +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -0,0 +1,159 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Actions available in the reinforcement learning environment.""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Callable + + from qiskit.passmanager import PropertySet + + +class CompilationOrigin(str, Enum): + """Enumeration of the origin of the compilation action.""" + + QISKIT = "qiskit" + TKET = "tket" + BQSKIT = "bqskit" + GENERAL = "general" + + +class PassType(str, Enum): + """Enumeration of the type of compilation pass.""" + + OPT = "optimization" + SYNTHESIS = "synthesis" + MAPPING = "mapping" + LAYOUT = "layout" + ROUTING = "routing" + FINAL_OPT = "final_optimization" + TERMINATE = "terminate" + + +@dataclass +class Action: + """Base class for all actions in the reinforcement learning environment.""" + + name: str + origin: CompilationOrigin + pass_type: PassType + transpile_pass: Any + + +@dataclass +class DeviceIndependentAction(Action): + """Action that represents a static compilation pass that can be applied directly.""" + + +@dataclass +class DeviceDependentAction(Action): + """Action that represents a device-specific compilation pass that can be applied to a specific device.""" + + transpile_pass: Any + do_while: Callable[[PropertySet], bool] | None = None + + +_ACTIONS: dict[str, Action] = {} + + +def register_action(action: Action) -> Action: + """Registers a new action in the global actions registry. + + Raises: + ValueError: If an action with the same name is already registered. + """ + if action.name in _ACTIONS: + msg = f"Action with name {action.name} already registered." + raise ValueError(msg) + _ACTIONS[action.name] = action + return action + + +def remove_action(name: str) -> None: + """Removes an action from the global actions registry by name. + + Raises: + ValueError: If no action with the given name is registered. + """ + if name not in _ACTIONS: + msg = f"No action with name {name} is registered." + raise KeyError(msg) + del _ACTIONS[name] + + +def get_actions_by_pass_type() -> dict[PassType, list[Action]]: + """Returns a dictionary mapping each PassType to a list of Actions of that type.""" + result: dict[PassType, list[Action]] = defaultdict(list) + for action in _ACTIONS.values(): + result[action.pass_type].append(action) + return result + + +from mqt.predictor.rl.actions import bqskit_actions as _bqskit_actions # noqa: E402 +from mqt.predictor.rl.actions import qiskit_actions as _qiskit_actions # noqa: E402 +from mqt.predictor.rl.actions import tket_actions as _tket_actions # noqa: E402 + +for _action in ( + *_qiskit_actions.qiskit_optimization_actions(), + *_tket_actions.tket_optimization_actions(), + _qiskit_actions.qiskit_o3_action(), + _bqskit_actions.bqskit_optimization_action(), + _qiskit_actions.qiskit_final_optimization_action(), + *_qiskit_actions.qiskit_layout_actions(), + _tket_actions.tket_routing_action(), + _qiskit_actions.qiskit_mapping_action(), + _bqskit_actions.bqskit_mapping_action(), + _qiskit_actions.qiskit_synthesis_action(), + _bqskit_actions.bqskit_synthesis_action(), + DeviceIndependentAction( + "terminate", + CompilationOrigin.GENERAL, + PassType.TERMINATE, + transpile_pass=[], + ), +): + register_action(_action) + +PreProcessTKETRoutingAfterQiskitLayout = _tket_actions.PreProcessTKETRoutingAfterQiskitLayout +final_layout_pytket_to_qiskit = _tket_actions.final_layout_pytket_to_qiskit +run_tket_action = _tket_actions.run_tket_action + +final_layout_bqskit_to_qiskit = _bqskit_actions.final_layout_bqskit_to_qiskit +bqskit_to_qiskit = _bqskit_actions.bqskit_to_qiskit +get_bqskit_native_gates = _bqskit_actions.get_bqskit_native_gates +run_bqskit_action = _bqskit_actions.run_bqskit_action + +postprocess_vf2postlayout = _qiskit_actions.postprocess_vf2postlayout +run_qiskit_action = _qiskit_actions.run_qiskit_action + +__all__ = [ + "Action", + "CompilationOrigin", + "DeviceDependentAction", + "DeviceIndependentAction", + "PassType", + "PreProcessTKETRoutingAfterQiskitLayout", + "bqskit_to_qiskit", + "final_layout_bqskit_to_qiskit", + "final_layout_pytket_to_qiskit", + "get_actions_by_pass_type", + "get_bqskit_native_gates", + "postprocess_vf2postlayout", + "register_action", + "remove_action", + "run_bqskit_action", + "run_qiskit_action", + "run_tket_action", +] diff --git a/src/mqt/predictor/rl/parsing.py b/src/mqt/predictor/rl/parsing.py index 528358c7b..8588d1be9 100644 --- a/src/mqt/predictor/rl/parsing.py +++ b/src/mqt/predictor/rl/parsing.py @@ -6,258 +6,22 @@ # # Licensed under the MIT License -"""Helper methods necessary for parsing between circuit formats.""" +"""Compatibility exports for circuit parsing helpers.""" from __future__ import annotations -import operator -from functools import cache -from typing import TYPE_CHECKING - -from bqskit.ir import gates -from pytket import Qubit -from pytket.circuit import Node -from pytket.placement import place_with_map -from qiskit.circuit import QuantumRegister -from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.transpiler import Layout, TranspileLayout -from qiskit.transpiler.passes import ApplyLayout - -if TYPE_CHECKING: - from bqskit.ir import Gate - from pytket import Circuit - from qiskit import QuantumCircuit - from qiskit.transpiler import Target - - -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 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 in: - ┌───┐ ░ ┌─┐ - 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 following 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: - """Applies the pre-processing step to route a circuit with tket after a Qiskit Layout pass has been applied.""" - mapping = {Qubit(i): Node(i) for i in range(circuit.n_qubits)} - place_with_map(circuit=circuit, qmap=mapping) - - -@cache -def get_bqskit_native_gates(device: Target) -> list[Gate]: - """Returns the native gates of the given device. - - Arguments: - device: The device for which the native gates are returned. - - Returns: - The native gates of the given device as BQSKit gates. - - Raises: - ValueError: If a gate in the device is not supported in BQSKit. - """ - gate_map = { - # --- 1-qubit gates --- - "id": gates.IdentityGate(), - "x": gates.XGate(), - "y": gates.YGate(), - "z": gates.ZGate(), - "h": gates.HGate(), - "s": gates.SGate(), - "sdg": gates.SdgGate(), - "t": gates.TGate(), - "tdg": gates.TdgGate(), - "sx": gates.SXGate(), - "rx": gates.RXGate(), - "ry": gates.RYGate(), - "rz": gates.RZGate(), - "u1": gates.U1Gate(), - "u2": gates.U2Gate(), - "u3": gates.U3Gate(), - # --- Controlled 1-qubit gates --- - "cx": gates.CXGate(), - "cy": gates.CYGate(), - "cz": gates.CZGate(), - "ch": gates.CHGate(), - "crx": gates.CRXGate(), - "cry": gates.CRYGate(), - "crz": gates.CRZGate(), - "cp": gates.CPGate(), - "cu": gates.CUGate(), - # --- 2-qubit gates --- - "swap": gates.SwapGate(), - "iswap": gates.ISwapGate(), - "ecr": gates.ECRGate(), - "rzz": gates.RZZGate(), - "rxx": gates.RXXGate(), - "ryy": gates.RYYGate(), - "zz": gates.ZZGate(), - # --- 3-qubit gates --- - "ccx": gates.CCXGate(), - # --- Others / approximations --- - "reset": gates.Reset(), - } - - native_gates = [] - - for instr in device.operation_names: - name = instr - - if name in [ - "barrier", - "measure", - "delay", - "for_loop", - "control", - "while_loop", - "if_test", - "if_else", - "switch_case", - "break", - "continue", - "box", - ]: - continue - - if name not in gate_map: - msg = f"The '{name}' gate of device '{device.description}' is not supported in BQSKIT." - raise ValueError(msg) - - native_gates.append(gate_map[name]) - - return native_gates - - -def final_layout_pytket_to_qiskit(pytket_circuit: Circuit, qiskit_circuit: QuantumCircuit) -> Layout: - """Converts a final layout from pytket to qiskit.""" - pytket_layout = pytket_circuit.qubit_readout - size_circuit = pytket_circuit.n_qubits - qiskit_layout = {} - qiskit_qreg = qiskit_circuit.qregs[0] - - pytket_layout = dict(sorted(pytket_layout.items(), key=operator.itemgetter(1))) - - for node, qubit_index in pytket_layout.items(): - 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 Layout(input_dict=qiskit_layout) - - -def final_layout_bqskit_to_qiskit( - bqskit_initial_layout: tuple[int, ...], - bqskit_final_layout: tuple[int, ...], - compiled_qc: QuantumCircuit, - initial_qc: QuantumCircuit, -) -> TranspileLayout: - """Converts a final layout from bqskit to qiskit. - - 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 = {} - counter_ancilla_qubit = 0 - 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[counter_ancilla_qubit] - counter_ancilla_qubit += 1 - - 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, - ) - - -def postprocess_vf2postlayout( - qc: QuantumCircuit, post_layout: Layout, layout_before: TranspileLayout -) -> tuple[QuantumCircuit, ApplyLayout]: - """Postprocess a quantum circuit after VF2 layout assignment. - - Args: - qc: The quantum circuit to transform. - post_layout: The layout computed after routing. - layout_before: The layout before post-routing adjustment. - - Returns: - A tuple of the transformed circuit and the ApplyLayout used. - """ - apply_layout = ApplyLayout() - apply_layout.property_set["layout"] = layout_before.initial_layout - apply_layout.property_set["original_qubit_indices"] = layout_before.input_qubit_mapping - apply_layout.property_set["final_layout"] = layout_before.final_layout - apply_layout.property_set["post_layout"] = post_layout - - altered_qc = apply_layout.run(circuit_to_dag(qc)) - return dag_to_circuit(altered_qc), apply_layout +from mqt.predictor.rl.actions import ( + PreProcessTKETRoutingAfterQiskitLayout, + final_layout_bqskit_to_qiskit, + final_layout_pytket_to_qiskit, + get_bqskit_native_gates, + postprocess_vf2postlayout, +) + +__all__ = [ + "PreProcessTKETRoutingAfterQiskitLayout", + "final_layout_bqskit_to_qiskit", + "final_layout_pytket_to_qiskit", + "get_bqskit_native_gates", + "postprocess_vf2postlayout", +] diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index aa57e1af0..81c6166a7 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -13,15 +13,10 @@ import logging from typing import TYPE_CHECKING, Any -from pytket._tket.passes import BasePass as TketBasePass # noqa: PLC2701 - if TYPE_CHECKING: - from collections.abc import Callable from pathlib import Path - from bqskit import Circuit from gymnasium.spaces import Space - from qiskit.passmanager.base_tasks import Task from qiskit.transpiler import Layout, Target from mqt.predictor.reward import figure_of_merit @@ -29,19 +24,13 @@ import warnings -from typing import cast 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 joblib import load -from pytket.circuit import Qubit -from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit from qiskit import QuantumCircuit -from qiskit.passmanager.flow_controllers import DoWhileController -from qiskit.transpiler import CouplingMap, PassManager, TranspileLayout -from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason +from qiskit.transpiler import CouplingMap, TranspileLayout from mqt.predictor.hellinger import get_hellinger_model_path from mqt.predictor.reward import ( @@ -51,14 +40,15 @@ estimated_success_probability, expected_fidelity, ) -from mqt.predictor.rl.actions import CompilationOrigin, DeviceDependentAction, PassType, get_actions_by_pass_type -from mqt.predictor.rl.helper import create_feature_dict, get_path_training_circuits, get_state_sample -from mqt.predictor.rl.parsing import ( - PreProcessTKETRoutingAfterQiskitLayout, - final_layout_bqskit_to_qiskit, - final_layout_pytket_to_qiskit, - postprocess_vf2postlayout, +from mqt.predictor.rl.actions import ( + CompilationOrigin, + PassType, + get_actions_by_pass_type, + run_bqskit_action, + run_qiskit_action, + run_tket_action, ) +from mqt.predictor.rl.helper import create_feature_dict, get_path_training_circuits, get_state_sample logger = logging.getLogger("mqt-predictor") @@ -323,109 +313,35 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: if action.name == "terminate": return self.state if action.origin == CompilationOrigin.QISKIT: - return self._apply_qiskit_action(action, action_index) + return self._apply_qiskit_action(action) if action.origin == CompilationOrigin.TKET: - return self._apply_tket_action(action, action_index) + return self._apply_tket_action(action) if action.origin == CompilationOrigin.BQSKIT: - return self._apply_bqskit_action(action, action_index) + return self._apply_bqskit_action(action) msg = f"Origin {action.origin} not supported." raise ValueError(msg) - def _apply_qiskit_action(self, action: Action, action_index: int) -> QuantumCircuit: - if action.name == "QiskitO3" and isinstance(action, DeviceDependentAction): - factory = cast("Callable[[list[str], CouplingMap | None], list[Task]]", action.transpile_pass) - passes = factory( - self.device.operation_names, - CouplingMap(self.device.build_coupling_map()) if self.layout else None, - ) - assert action.do_while is not None - pm = PassManager([DoWhileController(passes, do_while=action.do_while)]) - else: - if callable(action.transpile_pass): - factory = cast("Callable[[Target], list[Task]]", action.transpile_pass) - passes = factory(self.device) - else: - passes = cast("list[Task]", action.transpile_pass) - pm = PassManager(passes) - - altered_qc = pm.run(self.state) - - if action_index in ( - self.actions_layout_indices + self.actions_mapping_indices + self.actions_final_optimization_indices - ): - altered_qc = self._handle_qiskit_layout_postprocessing(action, pm, altered_qc) - - elif ( - action_index in self.actions_routing_indices and self.layout and pm.property_set["final_layout"] is not None - ): - self.layout.final_layout = pm.property_set["final_layout"] - - # BasisTranslator errors on unitary gates; decompose them immediately so - # the circuit is always in a consistent state after a Qiskit action. - if altered_qc.count_ops().get("unitary"): - altered_qc = altered_qc.decompose(gates_to_decompose="unitary") - - return altered_qc - - def _handle_qiskit_layout_postprocessing( - self, action: Action, pm: PassManager, altered_qc: QuantumCircuit - ) -> QuantumCircuit: - if action.name == "VF2PostLayout": - assert pm.property_set["VF2PostLayout_stop_reason"] is not None - post_layout = pm.property_set["post_layout"] - if post_layout: - assert self.layout is not None - altered_qc, _ = postprocess_vf2postlayout(altered_qc, post_layout, self.layout) - elif action.name == "VF2Layout": - if pm.property_set["VF2Layout_stop_reason"] != VF2LayoutStopReason.SOLUTION_FOUND: - logger.warning( - "VF2Layout pass did not find a solution. Reason: %s", - pm.property_set["VF2Layout_stop_reason"], - ) - else: - assert pm.property_set["layout"] - else: - assert pm.property_set["layout"] - - if pm.property_set["layout"]: - # Layout/mapping passes create the base logical-to-physical mapping; - # later routing actions only update final_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, - ) + def _apply_qiskit_action(self, action: Action) -> QuantumCircuit: + altered_qc, self.layout = run_qiskit_action( + action=action, + circuit=self.state, + device=self.device, + layout=self.layout, + input_qubit_count=self.num_qubits_uncompiled_circuit, + ) return altered_qc - def _apply_tket_action(self, action: Action, action_index: int) -> QuantumCircuit: - tket_qc = qiskit_to_tk(self.state, preserve_param_uuid=True) - if callable(action.transpile_pass): - factory = cast("Callable[[Target], list[Task]]", action.transpile_pass) - passes = factory(self.device) - else: - passes = cast("list[Task]", action.transpile_pass) - for pass_ in passes: - assert isinstance(pass_, TketBasePass | PreProcessTKETRoutingAfterQiskitLayout) - pass_.apply(tket_qc) - - qbs = tket_qc.qubits - tket_qc.rename_units({qbs[i]: Qubit("q", i) for i in range(len(qbs))}) - altered_qc = tk_to_qiskit(tket_qc, replace_implicit_swaps=True) - - if action_index in self.actions_routing_indices: - assert self.layout is not None - self.layout.final_layout = final_layout_pytket_to_qiskit(tket_qc, altered_qc) - + def _apply_tket_action(self, action: Action) -> QuantumCircuit: + altered_qc, self.layout = run_tket_action( + action=action, circuit=self.state, device=self.device, layout=self.layout + ) return altered_qc - def _apply_bqskit_action(self, action: Action, action_index: int) -> QuantumCircuit: + def _apply_bqskit_action(self, action: Action) -> QuantumCircuit: """Applies the given BQSKit action to the current state and returns the altered state. Arguments: action: The BQSKit action to be applied. - action_index: The index of the action in the action set. Returns: The altered quantum circuit after applying the action. @@ -433,27 +349,10 @@ def _apply_bqskit_action(self, action: Action, action_index: int) -> QuantumCirc Raises: ValueError: If the action index is not in the action set or if the action origin is not supported. """ - bqskit_qc = qiskit_to_bqskit(self.state) - if action_index in self.actions_opt_indices: - transpile = cast("Callable[[Circuit], Circuit]", action.transpile_pass) - bqskit_compiled_qc = transpile(bqskit_qc) - elif action_index in self.actions_synthesis_indices: - factory = cast("Callable[[Target], Callable[[Circuit], Circuit]]", action.transpile_pass) - bqskit_compiled_qc = factory(self.device)(bqskit_qc) - elif action_index in self.actions_mapping_indices: - factory = cast( - "Callable[[Target], Callable[[Circuit], tuple[Circuit, tuple[int, ...], tuple[int, ...]]]]", - action.transpile_pass, - ) - bqskit_compiled_qc, initial, final = factory(self.device)(bqskit_qc) - compiled_qiskit_qc = bqskit_to_qiskit(bqskit_compiled_qc) - self.layout = final_layout_bqskit_to_qiskit(initial, final, compiled_qiskit_qc, self.state) - return compiled_qiskit_qc - else: - msg = f"Unhandled BQSKit action index: {action_index}" - raise ValueError(msg) - - return bqskit_to_qiskit(bqskit_compiled_qc) + altered_qc, self.layout = run_bqskit_action( + action=action, circuit=self.state, device=self.device, layout=self.layout + ) + return altered_qc def is_circuit_laid_out(self, circuit: QuantumCircuit, layout: TranspileLayout | Layout) -> bool: """True if every logical qubit in the circuit has a physical assignment.""" From 512d2b05342fbe1d6909eb61c9a3863eaf7f2bc5 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 15:36:51 +0200 Subject: [PATCH 14/42] =?UTF-8?q?=F0=9F=8E=A8=20add=20changelog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b636e604..a48cf7220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Changed +- ♻️ Restructure existing RL passes into SDK-level action modules, including their wrapper logic ([#644]) ([**@flowerthrower**]) +- ✨ Support BQSKit conversion of IQM's native `r` gate. - 🔧 Replace `mypy` with `ty` ([#572]) ([**@denialhaag**]) - 🐛 Fix instruction duration unit in estimated success probability calculation ([#445]) ([**@Shaobo-Zhou**]) - ✨ Remove support for custom names of trained models ([#489]) ([**@bachase**]) @@ -50,6 +52,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#489]: https://github.com/munich-quantum-toolkit/predictor/pull/489 [#445]: https://github.com/munich-quantum-toolkit/predictor/pull/445 [#421]: https://github.com/munich-quantum-toolkit/predictor/pull/421 +[#644]: https://github.com/munich-quantum-toolkit/predictor/pull/644 [#406]: https://github.com/munich-quantum-toolkit/predictor/pull/406 [#405]: https://github.com/munich-quantum-toolkit/predictor/pull/405 [#403]: https://github.com/munich-quantum-toolkit/predictor/pull/403 From c398324e98862000bff5c499b786e109001e9a10 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 15:37:00 +0200 Subject: [PATCH 15/42] =?UTF-8?q?=E2=9C=85=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_integration_further_SDKs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index d4e00706a..78106ab3b 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, cast import pytest -from bqskit.ext import bqskit_to_qiskit, qiskit_to_bqskit +from bqskit.ext import qiskit_to_bqskit from bqskit.ir.circuit import Circuit from mqt.bench.targets import get_available_device_names, get_device from pytket.circuit import Qubit @@ -24,7 +24,7 @@ from qiskit.transpiler.layout import TranspileLayout from qiskit.transpiler.passes import CheckMap, GatesInBasis -from mqt.predictor.rl.actions import CompilationOrigin, PassType, get_actions_by_pass_type +from mqt.predictor.rl.actions import CompilationOrigin, PassType, bqskit_to_qiskit, get_actions_by_pass_type from mqt.predictor.rl.parsing import ( final_layout_bqskit_to_qiskit, final_layout_pytket_to_qiskit, @@ -83,14 +83,14 @@ def test_bqskit_synthesis_action(device: Target, available_actions_dict: dict[Pa qc.h(0) qc.cx(0, 1) - check_nat_gates = GatesInBasis(target=device) + check_nat_gates = GatesInBasis(basis_gates=device.operation_names) check_nat_gates(qc) assert not check_nat_gates.property_set["all_gates_in_basis"] factory = cast("Callable[[Target], Callable[[Circuit], Circuit]]", action_bqskit_synthesis_action.transpile_pass) lambda_ = factory(device) bqskit_qc = qiskit_to_bqskit(qc) - if "rigetti" in device.description or "ionq" in device.description or "iqm" in device.description: + if "rigetti" in device.description or "ionq" in device.description: with pytest.raises(ValueError, match=re.escape("not supported in BQSKIT")): bqskit_qc_compiled = lambda_(bqskit_qc) return @@ -98,7 +98,7 @@ def test_bqskit_synthesis_action(device: Target, available_actions_dict: dict[Pa assert isinstance(bqskit_qc_compiled, Circuit) native_gates_qc = bqskit_to_qiskit(bqskit_qc_compiled) - check_nat_gates = GatesInBasis(target=device) + check_nat_gates = GatesInBasis(basis_gates=device.operation_names) check_nat_gates(native_gates_qc) only_nat_gates = check_nat_gates.property_set["all_gates_in_basis"] assert only_nat_gates From 010fa687120a882b6c0e839bb067dbf171ec4302 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 16:30:32 +0200 Subject: [PATCH 16/42] =?UTF-8?q?=F0=9F=8E=A8=20add=20changelog=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b636e604..5b840d39d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Changed +- 🎨 Improve the RL state machine logic ([#677]) ([**@flowerthrower**]) - 🔧 Replace `mypy` with `ty` ([#572]) ([**@denialhaag**]) - 🐛 Fix instruction duration unit in estimated success probability calculation ([#445]) ([**@Shaobo-Zhou**]) - ✨ Remove support for custom names of trained models ([#489]) ([**@bachase**]) @@ -46,6 +47,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool +[#677]: https://github.com/munich-quantum-toolkit/predictor/pull/677 [#572]: https://github.com/munich-quantum-toolkit/predictor/pull/572 [#489]: https://github.com/munich-quantum-toolkit/predictor/pull/489 [#445]: https://github.com/munich-quantum-toolkit/predictor/pull/445 From 59bdf52374e47aaf2ba6107453430adf37c458b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 14:35:50 +0000 Subject: [PATCH 17/42] =?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/actions/__init__.py | 6 +++--- src/mqt/predictor/rl/actions/bqskit_actions.py | 10 +++++++--- src/mqt/predictor/rl/actions/qiskit_actions.py | 8 ++++++-- src/mqt/predictor/rl/actions/tket_actions.py | 11 +++++++---- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 2858d9ddd..36f00050c 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -101,9 +101,9 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]: return result -from mqt.predictor.rl.actions import bqskit_actions as _bqskit_actions # noqa: E402 -from mqt.predictor.rl.actions import qiskit_actions as _qiskit_actions # noqa: E402 -from mqt.predictor.rl.actions import tket_actions as _tket_actions # noqa: E402 +from mqt.predictor.rl.actions import bqskit_actions as _bqskit_actions +from mqt.predictor.rl.actions import qiskit_actions as _qiskit_actions +from mqt.predictor.rl.actions import tket_actions as _tket_actions for _action in ( *_qiskit_actions.qiskit_optimization_actions(), diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index c0e795138..05a9e403f 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -11,7 +11,6 @@ from __future__ import annotations import os -from collections.abc import Callable from functools import cache from typing import TYPE_CHECKING, cast @@ -21,18 +20,23 @@ from bqskit.ext.qiskit.translate import OPENQASM2Language from bqskit.ir import gates from qiskit import qasm2 -from qiskit.circuit import Instruction, QuantumRegister +from qiskit.circuit import QuantumRegister from qiskit.circuit.library import RGate from qiskit.transpiler import Layout, TranspileLayout -from mqt.predictor.rl.actions import Action, CompilationOrigin, DeviceDependentAction, PassType +from mqt.predictor.rl.actions import CompilationOrigin, DeviceDependentAction, PassType if TYPE_CHECKING: + from collections.abc import Callable + from bqskit import Circuit from bqskit.ir import Gate from qiskit import QuantumCircuit + from qiskit.circuit import Instruction from qiskit.transpiler import Target + from mqt.predictor.rl.actions import Action + def _r_gate(theta: float, phi: float) -> Instruction: return RGate(theta, phi) diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py index 1b86eaa0f..24f11d25f 100644 --- a/src/mqt/predictor/rl/actions/qiskit_actions.py +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -11,7 +11,6 @@ from __future__ import annotations import logging -from collections.abc import Callable from typing import TYPE_CHECKING, cast from qiskit.circuit import StandardEquivalenceLibrary @@ -64,7 +63,6 @@ from qiskit.transpiler.preset_passmanagers import common from mqt.predictor.rl.actions import ( - Action, CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, @@ -72,11 +70,17 @@ ) if TYPE_CHECKING: + from collections.abc import Callable + from qiskit import QuantumCircuit from qiskit.passmanager import PropertySet from qiskit.passmanager.base_tasks import Task from qiskit.transpiler import Layout, Target + from mqt.predictor.rl.actions import ( + Action, + ) + logger = logging.getLogger("mqt-predictor") diff --git a/src/mqt/predictor/rl/actions/tket_actions.py b/src/mqt/predictor/rl/actions/tket_actions.py index 5ef1fb989..2806b86e0 100644 --- a/src/mqt/predictor/rl/actions/tket_actions.py +++ b/src/mqt/predictor/rl/actions/tket_actions.py @@ -11,26 +11,29 @@ from __future__ import annotations import operator -from collections.abc import Callable from typing import TYPE_CHECKING, cast from pytket import Qubit +from pytket._tket.passes import BasePass as TketBasePass from pytket.architecture import Architecture from pytket.circuit import Node from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit -from pytket.placement import place_with_map from pytket.passes import CliffordSimp, FullPeepholeOptimise, PeepholeOptimise2Q, RemoveRedundancies, RoutingPass +from pytket.placement import place_with_map from qiskit.transpiler import Layout -from mqt.predictor.rl.actions import Action, CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, PassType +from mqt.predictor.rl.actions import CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, PassType if TYPE_CHECKING: + from collections.abc import Callable + from pytket import Circuit - from pytket._tket.passes import BasePass as TketBasePass from qiskit import QuantumCircuit from qiskit.passmanager.base_tasks import Task from qiskit.transpiler import Target, TranspileLayout + from mqt.predictor.rl.actions import Action + class PreProcessTKETRoutingAfterQiskitLayout: """Pre-process TKET routing for circuits that already carry a Qiskit layout. From 2dd86dd437bbd30d367ee3ff76728481fb669d9c Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 18:01:12 +0200 Subject: [PATCH 18/42] =?UTF-8?q?=F0=9F=94=80=20pull=20changes=20from=20#6?= =?UTF-8?q?79?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- src/mqt/predictor/rl/actions/__init__.py | 6 ++--- .../predictor/rl/actions/bqskit_actions.py | 16 ++++++++++---- .../predictor/rl/actions/qiskit_actions.py | 8 +++++-- src/mqt/predictor/rl/actions/tket_actions.py | 11 ++++++---- src/mqt/predictor/rl/parsing.py | 2 ++ tests/compilation/test_helper_rl.py | 22 ++++++++++++++++++- 7 files changed, 53 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a48cf7220..0e7cbdfff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Changed - ♻️ Restructure existing RL passes into SDK-level action modules, including their wrapper logic ([#644]) ([**@flowerthrower**]) -- ✨ Support BQSKit conversion of IQM's native `r` gate. +- 🐛 Support BQSKit conversion of IQM's native `r` gate ([#679]) ([**@flowerthrower**]) - 🔧 Replace `mypy` with `ty` ([#572]) ([**@denialhaag**]) - 🐛 Fix instruction duration unit in estimated success probability calculation ([#445]) ([**@Shaobo-Zhou**]) - ✨ Remove support for custom names of trained models ([#489]) ([**@bachase**]) @@ -53,6 +53,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#445]: https://github.com/munich-quantum-toolkit/predictor/pull/445 [#421]: https://github.com/munich-quantum-toolkit/predictor/pull/421 [#644]: https://github.com/munich-quantum-toolkit/predictor/pull/644 +[#679]: https://github.com/munich-quantum-toolkit/predictor/pull/679 [#406]: https://github.com/munich-quantum-toolkit/predictor/pull/406 [#405]: https://github.com/munich-quantum-toolkit/predictor/pull/405 [#403]: https://github.com/munich-quantum-toolkit/predictor/pull/403 diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 2858d9ddd..36f00050c 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -101,9 +101,9 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]: return result -from mqt.predictor.rl.actions import bqskit_actions as _bqskit_actions # noqa: E402 -from mqt.predictor.rl.actions import qiskit_actions as _qiskit_actions # noqa: E402 -from mqt.predictor.rl.actions import tket_actions as _tket_actions # noqa: E402 +from mqt.predictor.rl.actions import bqskit_actions as _bqskit_actions +from mqt.predictor.rl.actions import qiskit_actions as _qiskit_actions +from mqt.predictor.rl.actions import tket_actions as _tket_actions for _action in ( *_qiskit_actions.qiskit_optimization_actions(), diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index c0e795138..592ae5e89 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -11,7 +11,6 @@ from __future__ import annotations import os -from collections.abc import Callable from functools import cache from typing import TYPE_CHECKING, cast @@ -21,18 +20,23 @@ from bqskit.ext.qiskit.translate import OPENQASM2Language from bqskit.ir import gates from qiskit import qasm2 -from qiskit.circuit import Instruction, QuantumRegister +from qiskit.circuit import QuantumRegister from qiskit.circuit.library import RGate from qiskit.transpiler import Layout, TranspileLayout -from mqt.predictor.rl.actions import Action, CompilationOrigin, DeviceDependentAction, PassType +from mqt.predictor.rl.actions import CompilationOrigin, DeviceDependentAction, PassType if TYPE_CHECKING: + from collections.abc import Callable + from bqskit import Circuit from bqskit.ir import Gate from qiskit import QuantumCircuit + from qiskit.circuit import Instruction from qiskit.transpiler import Target + from mqt.predictor.rl.actions import Action + def _r_gate(theta: float, phi: float) -> Instruction: return RGate(theta, phi) @@ -50,7 +54,11 @@ def _bqskit_compilation_options() -> dict[str, float | int]: def bqskit_to_qiskit(circuit: Circuit) -> QuantumCircuit: - """Convert a BQSKit circuit to Qiskit, including BQSKit's IQM-compatible U1q gate.""" + """Convert a BQSKit circuit to Qiskit. + + This mirrors BQSKit's Qiskit converter and supplements it with handling + for IQM's native ``r`` gate, which BQSKit emits as ``U1q``. + """ qasm = OPENQASM2Language().encode(circuit).replace("U1q(", "r(") return qasm2.loads( qasm, diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py index 1b86eaa0f..24f11d25f 100644 --- a/src/mqt/predictor/rl/actions/qiskit_actions.py +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -11,7 +11,6 @@ from __future__ import annotations import logging -from collections.abc import Callable from typing import TYPE_CHECKING, cast from qiskit.circuit import StandardEquivalenceLibrary @@ -64,7 +63,6 @@ from qiskit.transpiler.preset_passmanagers import common from mqt.predictor.rl.actions import ( - Action, CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, @@ -72,11 +70,17 @@ ) if TYPE_CHECKING: + from collections.abc import Callable + from qiskit import QuantumCircuit from qiskit.passmanager import PropertySet from qiskit.passmanager.base_tasks import Task from qiskit.transpiler import Layout, Target + from mqt.predictor.rl.actions import ( + Action, + ) + logger = logging.getLogger("mqt-predictor") diff --git a/src/mqt/predictor/rl/actions/tket_actions.py b/src/mqt/predictor/rl/actions/tket_actions.py index 5ef1fb989..b77f3f749 100644 --- a/src/mqt/predictor/rl/actions/tket_actions.py +++ b/src/mqt/predictor/rl/actions/tket_actions.py @@ -11,26 +11,29 @@ from __future__ import annotations import operator -from collections.abc import Callable from typing import TYPE_CHECKING, cast from pytket import Qubit +from pytket._tket.passes import BasePass as TketBasePass # noqa: PLC2701 from pytket.architecture import Architecture from pytket.circuit import Node from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit -from pytket.placement import place_with_map from pytket.passes import CliffordSimp, FullPeepholeOptimise, PeepholeOptimise2Q, RemoveRedundancies, RoutingPass +from pytket.placement import place_with_map from qiskit.transpiler import Layout -from mqt.predictor.rl.actions import Action, CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, PassType +from mqt.predictor.rl.actions import CompilationOrigin, DeviceDependentAction, DeviceIndependentAction, PassType if TYPE_CHECKING: + from collections.abc import Callable + from pytket import Circuit - from pytket._tket.passes import BasePass as TketBasePass from qiskit import QuantumCircuit from qiskit.passmanager.base_tasks import Task from qiskit.transpiler import Target, TranspileLayout + from mqt.predictor.rl.actions import Action + class PreProcessTKETRoutingAfterQiskitLayout: """Pre-process TKET routing for circuits that already carry a Qiskit layout. diff --git a/src/mqt/predictor/rl/parsing.py b/src/mqt/predictor/rl/parsing.py index 8588d1be9..f7ba54301 100644 --- a/src/mqt/predictor/rl/parsing.py +++ b/src/mqt/predictor/rl/parsing.py @@ -12,6 +12,7 @@ from mqt.predictor.rl.actions import ( PreProcessTKETRoutingAfterQiskitLayout, + bqskit_to_qiskit, final_layout_bqskit_to_qiskit, final_layout_pytket_to_qiskit, get_bqskit_native_gates, @@ -20,6 +21,7 @@ __all__ = [ "PreProcessTKETRoutingAfterQiskitLayout", + "bqskit_to_qiskit", "final_layout_bqskit_to_qiskit", "final_layout_pytket_to_qiskit", "get_bqskit_native_gates", diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index a933f1f31..612960129 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -14,6 +14,8 @@ from typing import TYPE_CHECKING, cast import numpy as np +from bqskit.ir import gates +from bqskit.ir.circuit import Circuit from mqt.bench import BenchmarkLevel, get_benchmark from mqt.bench.targets import get_device from qiskit import transpile @@ -22,7 +24,7 @@ from mqt.predictor.rl.actions import PassType, get_actions_by_pass_type from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model, get_path_training_circuits -from mqt.predictor.rl.parsing import postprocess_vf2postlayout +from mqt.predictor.rl.parsing import bqskit_to_qiskit, get_bqskit_native_gates, postprocess_vf2postlayout if TYPE_CHECKING: from collections.abc import Callable @@ -53,6 +55,24 @@ def test_get_path_training_circuits() -> None: assert isinstance(path, Path) +def test_get_bqskit_native_gates_supports_iqm_r_gate() -> None: + """Test that IQM's native RGate is represented in BQSKit.""" + native_gates = get_bqskit_native_gates(get_device("iqm_crystal_20")) + + assert any(isinstance(gate, gates.U1qGate) for gate in native_gates) + + +def test_bqskit_to_qiskit_converts_u1q_to_r_gate() -> None: + """Test that BQSKit's U1qGate is converted to Qiskit's RGate.""" + circuit = Circuit(1) + circuit.append_gate(gates.U1qGate(), [0], [0.1, 0.2]) + + qc = bqskit_to_qiskit(circuit) + + assert qc.data[0].operation.name == "r" + assert qc.data[0].operation.params == [0.1, 0.2] + + def test_vf2_layout_and_postlayout() -> None: """Test the VF2Layout and VF2PostLayout passes.""" qc = get_benchmark("ghz", BenchmarkLevel.ALG, 3) From 86d078a0d6108d084d4c1b5c14bdb9438c28f06a Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 12 May 2026 18:12:07 +0200 Subject: [PATCH 19/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/bqskit_actions.py | 18 ++++++++++++++---- src/mqt/predictor/rl/actions/tket_actions.py | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index 592ae5e89..5482d8e43 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -11,6 +11,7 @@ from __future__ import annotations import os +import re from functools import cache from typing import TYPE_CHECKING, cast @@ -39,6 +40,7 @@ def _r_gate(theta: float, phi: float) -> Instruction: + """Construct an RGate with the given parameters.""" return RGate(theta, phi) @@ -54,12 +56,20 @@ def _bqskit_compilation_options() -> dict[str, float | int]: def bqskit_to_qiskit(circuit: Circuit) -> QuantumCircuit: - """Convert a BQSKit circuit to Qiskit. + """Convert a BQSKit Circuit to Qiskit's QuantumCircuit. - This mirrors BQSKit's Qiskit converter and supplements it with handling - for IQM's native ``r`` gate, which BQSKit emits as ``U1q``. + This function extends BQSKit's built-in conversion by adding support for + IQM's native 'r' gate. BQSKit represents this as U1qGate, which is converted + to Qiskit's RGate by rewriting the OpenQASM 2 output. + + Args: + circuit: The BQSKit circuit to convert. + + Returns: + The equivalent Qiskit QuantumCircuit with 'r' gates properly mapped. """ - qasm = OPENQASM2Language().encode(circuit).replace("U1q(", "r(") + qasm = OPENQASM2Language().encode(circuit) + qasm = re.sub(r"\bU1q\(", "r(", qasm) return qasm2.loads( qasm, custom_instructions=( diff --git a/src/mqt/predictor/rl/actions/tket_actions.py b/src/mqt/predictor/rl/actions/tket_actions.py index 2806b86e0..b77f3f749 100644 --- a/src/mqt/predictor/rl/actions/tket_actions.py +++ b/src/mqt/predictor/rl/actions/tket_actions.py @@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, cast from pytket import Qubit -from pytket._tket.passes import BasePass as TketBasePass +from pytket._tket.passes import BasePass as TketBasePass # noqa: PLC2701 from pytket.architecture import Architecture from pytket.circuit import Node from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit From a474a8fe4b5f9f3b5a9ecff1b220fa13ddc01ad4 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 29 May 2026 16:24:03 +0200 Subject: [PATCH 20/42] =?UTF-8?q?=F0=9F=8E=A8=20imporve=20error=20reportin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/reward.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mqt/predictor/reward.py b/src/mqt/predictor/reward.py index a8cd76952..e8c1b4742 100644 --- a/src/mqt/predictor/reward.py +++ b/src/mqt/predictor/reward.py @@ -191,7 +191,11 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: res *= 1 - device[gate_type][first_qubit_idx,].error else: second_qubit_idx = scheduled_circ.find_bit(qargs[1]).index - res *= 1 - device[gate_type][first_qubit_idx, second_qubit_idx].error + try: + res *= 1 - device[gate_type][first_qubit_idx, second_qubit_idx].error + except KeyError: + msg = f"Error rate for gate {gate_type} on qubits {first_qubit_idx} and {second_qubit_idx} not found in device properties." + raise KeyError(msg) from None if qiskit_version >= "2.0.0": for i in range(device.num_qubits): From 51b20af4a8ef811adae609cb4e17439122bd52c4 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 29 May 2026 16:51:55 +0200 Subject: [PATCH 21/42] =?UTF-8?q?=E2=9C=85=20improve=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_predictor_rl.py | 71 +++++++++++++++++++++++++- tests/compilation/test_reward.py | 40 +++++++++++++-- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 9fb68914b..f141bebbf 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -16,12 +16,14 @@ import pytest from mqt.bench import BenchmarkLevel, get_benchmark from mqt.bench.targets import get_device +from qiskit import QuantumCircuit from qiskit.circuit.library import CXGate from qiskit.qasm2 import dump -from qiskit.transpiler import InstructionProperties, Target +from qiskit.transpiler import InstructionProperties, Layout, Target, TranspileLayout from qiskit.transpiler.passes import GatesInBasis from mqt.predictor.rl import Predictor, rl_compile +from mqt.predictor.rl import predictorenv as predictorenv_module from mqt.predictor.rl.actions import ( CompilationOrigin, DeviceIndependentAction, @@ -31,6 +33,7 @@ remove_action, ) from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model +from mqt.predictor.rl.predictorenv import PredictorEnv def test_predictor_env_reset_from_string() -> None: @@ -117,6 +120,72 @@ def test_warning_for_unidirectional_device() -> None: Predictor(figure_of_merit="expected_fidelity", device=target) +def test_predictor_env_actions_after_layout_with_non_native_unrouted_circuit() -> None: + """Test valid actions for a laid-out circuit that still needs synthesis and routing.""" + device = get_device("ibm_falcon_27") + env = PredictorEnv(device=device) + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 2) + env.reset(qc) + + env.layout = TranspileLayout( + initial_layout=Layout({qubit: index for index, qubit in enumerate(qc.qubits)}), + input_qubit_mapping={qubit: index for index, qubit in enumerate(qc.qubits)}, + final_layout=None, + _output_qubit_list=qc.qubits, + _input_qubit_count=qc.num_qubits, + ) + + valid_actions = env.determine_valid_actions_for_state() + + assert set(env.actions_synthesis_indices).issubset(valid_actions) + assert set(env.actions_routing_indices).issubset(valid_actions) + assert set(env.actions_opt_indices).issubset(valid_actions) + assert env.action_terminate_index not in valid_actions + + +def test_predictor_env_qiskit_routing_updates_final_layout(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that Qiskit routing actions update the tracked final layout.""" + device = get_device("ibm_falcon_27") + env = PredictorEnv(device=device) + qc = QuantumCircuit(2) + qc.cx(0, 1) + env.reset(qc) + + initial_layout = Layout({qubit: index for index, qubit in enumerate(qc.qubits)}) + final_layout = Layout({qc.qubits[0]: 1, qc.qubits[1]: 0}) + env.layout = TranspileLayout( + initial_layout=initial_layout, + input_qubit_mapping={qubit: index for index, qubit in enumerate(qc.qubits)}, + final_layout=None, + _output_qubit_list=qc.qubits, + _input_qubit_count=qc.num_qubits, + ) + + class FakePassManager: + """Minimal PassManager replacement that exposes a final layout.""" + + def __init__(self, _passes: object) -> None: + self.property_set = {"final_layout": final_layout} + + def run(self, circuit: QuantumCircuit) -> QuantumCircuit: + return circuit + + monkeypatch.setattr(predictorenv_module, "PassManager", FakePassManager) + action = DeviceIndependentAction( + name="SyntheticQiskitRouting", + pass_type=PassType.ROUTING, + transpile_pass=[], + origin=CompilationOrigin.QISKIT, + ) + + altered_qc = env._apply_qiskit_action(action, env.actions_routing_indices[0]) # noqa: SLF001 + + assert altered_qc is env.state + assert env.layout.final_layout is final_layout + + def test_register_action() -> None: """Test the register_action function.""" action = DeviceIndependentAction( diff --git a/tests/compilation/test_reward.py b/tests/compilation/test_reward.py index 5e36fcc8a..e1bd57e31 100644 --- a/tests/compilation/test_reward.py +++ b/tests/compilation/test_reward.py @@ -10,15 +10,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import re import pytest from mqt.bench import BenchmarkLevel, get_benchmark from mqt.bench.targets import get_device -from qiskit import transpile +from qiskit import QuantumCircuit, transpile from qiskit.circuit.library import CXGate, Measure, XGate from qiskit.transpiler import InstructionProperties, Target +from mqt.predictor import reward as reward_module from mqt.predictor.reward import crit_depth, esp_data_available, estimated_success_probability, expected_fidelity try: @@ -30,9 +31,6 @@ QISKIT_PRE_2_0 = True -if TYPE_CHECKING: - from qiskit import QuantumCircuit - @pytest.fixture def device() -> Target: @@ -132,3 +130,35 @@ def test_esp_data_available_invalid_target(kwargs: dict[str, float | bool]) -> N """Test that `esp_data_available` returns False for invalid device configurations.""" target = make_target(**kwargs) # ty: ignore[invalid-argument-type] assert not esp_data_available(target) + + +@pytest.mark.parametrize("reward_function", ["expected_fidelity", "estimated_success_probability"]) +def test_reward_missing_two_qubit_error(reward_function: str, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that reward functions report missing two-qubit error rates descriptively.""" + target = make_target() + del target["cx"][0, 1] + + qc = QuantumCircuit(2) + if reward_function == "estimated_success_probability": + qc.x(0) + + scheduled_qc = QuantumCircuit(2) + scheduled_qc.cx(0, 1) + + def fake_transpile(*_args: object, **_kwargs: object) -> QuantumCircuit: + return scheduled_qc + + def estimate_duration(*, target: Target) -> float: + assert target.num_qubits == 2 + return 0.0 + + monkeypatch.setattr(reward_module, "qiskit_version", "2.0.0") + monkeypatch.setattr(reward_module, "transpile", fake_transpile) + monkeypatch.setattr(scheduled_qc, "estimate_duration", estimate_duration) + else: + qc.cx(0, 1) + + reward = estimated_success_probability if reward_function == "estimated_success_probability" else expected_fidelity + expected_message = "Error rate for gate cx on qubits 0 and 1 not found in device properties." + with pytest.raises(KeyError, match=re.escape(expected_message)): + reward(qc, target) From 7e9a3699af3face518b2984163d38ac0bd37bd20 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 1 Jun 2026 13:24:44 +0200 Subject: [PATCH 22/42] =?UTF-8?q?=E2=9C=85=20fix=20test=20for=20qiskit<2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_reward.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compilation/test_reward.py b/tests/compilation/test_reward.py index e1bd57e31..5b3512f02 100644 --- a/tests/compilation/test_reward.py +++ b/tests/compilation/test_reward.py @@ -154,7 +154,7 @@ def estimate_duration(*, target: Target) -> float: monkeypatch.setattr(reward_module, "qiskit_version", "2.0.0") monkeypatch.setattr(reward_module, "transpile", fake_transpile) - monkeypatch.setattr(scheduled_qc, "estimate_duration", estimate_duration) + monkeypatch.setattr(scheduled_qc, "estimate_duration", estimate_duration, raising=False) # not available in qiskit<2.0 else: qc.cx(0, 1) From 069474973d4e960067c73ad3090d2da33a9d2acd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:25:04 +0000 Subject: [PATCH 23/42] =?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 --- tests/compilation/test_reward.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/compilation/test_reward.py b/tests/compilation/test_reward.py index 5b3512f02..e0843dbc9 100644 --- a/tests/compilation/test_reward.py +++ b/tests/compilation/test_reward.py @@ -154,7 +154,9 @@ def estimate_duration(*, target: Target) -> float: monkeypatch.setattr(reward_module, "qiskit_version", "2.0.0") monkeypatch.setattr(reward_module, "transpile", fake_transpile) - monkeypatch.setattr(scheduled_qc, "estimate_duration", estimate_duration, raising=False) # not available in qiskit<2.0 + monkeypatch.setattr( + scheduled_qc, "estimate_duration", estimate_duration, raising=False + ) # not available in qiskit<2.0 else: qc.cx(0, 1) From 59c0beb1dd1e0e90f27f4da97acd1c93513a3d40 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 2 Jun 2026 16:31:45 +0200 Subject: [PATCH 24/42] =?UTF-8?q?=E2=9C=85=20fix=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_integration_further_SDKs.py | 2 +- tests/compilation/test_predictor_rl.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index dcd0d24a7..b9b5c077f 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -24,7 +24,7 @@ from qiskit.transpiler.layout import TranspileLayout from qiskit.transpiler.passes import CheckMap, GatesInBasis -from mqt.predictor.rl.actions import CompilationOrigin, PassType, bqskit_to_qiskit, get_actions_by_pass_type +from mqt.predictor.rl.actions import CompilationOrigin, PassType, get_actions_by_pass_type from mqt.predictor.rl.parsing import ( bqskit_to_qiskit, final_layout_bqskit_to_qiskit, diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index f141bebbf..baabafa56 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -23,7 +23,6 @@ from qiskit.transpiler.passes import GatesInBasis from mqt.predictor.rl import Predictor, rl_compile -from mqt.predictor.rl import predictorenv as predictorenv_module from mqt.predictor.rl.actions import ( CompilationOrigin, DeviceIndependentAction, @@ -32,6 +31,7 @@ register_action, remove_action, ) +from mqt.predictor.rl.actions import qiskit_actions as qiskit_actions_module from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model from mqt.predictor.rl.predictorenv import PredictorEnv @@ -172,7 +172,7 @@ def __init__(self, _passes: object) -> None: def run(self, circuit: QuantumCircuit) -> QuantumCircuit: return circuit - monkeypatch.setattr(predictorenv_module, "PassManager", FakePassManager) + monkeypatch.setattr(qiskit_actions_module, "PassManager", FakePassManager) action = DeviceIndependentAction( name="SyntheticQiskitRouting", pass_type=PassType.ROUTING, @@ -180,7 +180,7 @@ def run(self, circuit: QuantumCircuit) -> QuantumCircuit: origin=CompilationOrigin.QISKIT, ) - altered_qc = env._apply_qiskit_action(action, env.actions_routing_indices[0]) # noqa: SLF001 + altered_qc = env._apply_qiskit_action(action) # noqa: SLF001 assert altered_qc is env.state assert env.layout.final_layout is final_layout From 19ed9c11b4120b0854738f3a19c6c3d9ae92fd04 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 09:51:21 +0200 Subject: [PATCH 25/42] =?UTF-8?q?=F0=9F=8E=A8=20imporve=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74cdbb558..edadbbabf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,6 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#489]: https://github.com/munich-quantum-toolkit/predictor/pull/489 [#445]: https://github.com/munich-quantum-toolkit/predictor/pull/445 [#421]: https://github.com/munich-quantum-toolkit/predictor/pull/421 -[#644]: https://github.com/munich-quantum-toolkit/predictor/pull/644 -[#679]: https://github.com/munich-quantum-toolkit/predictor/pull/679 [#406]: https://github.com/munich-quantum-toolkit/predictor/pull/406 [#405]: https://github.com/munich-quantum-toolkit/predictor/pull/405 [#403]: https://github.com/munich-quantum-toolkit/predictor/pull/403 From 3d600d14576b04dcc29fbaafb5667ae0b7467b6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:54:45 +0000 Subject: [PATCH 26/42] =?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 --- tests/compilation/test_predictor_rl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 7883c4919..f0b9aa67a 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -32,9 +32,7 @@ register_action, remove_action, ) -from mqt.predictor.rl.actions import qiskit_actions as qiskit_actions_module from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model -from mqt.predictor.rl.predictorenv import PredictorEnv def test_predictor_env_reset_from_string() -> None: From b73c286b19b8182be6f6cef26e5f24567100e217 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 10:09:31 +0200 Subject: [PATCH 27/42] =?UTF-8?q?=F0=9F=94=A5=20remove=20GENERAL=20pass=20?= =?UTF-8?q?origin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 7 ++-- src/mqt/predictor/rl/predictorenv.py | 53 ------------------------ tests/compilation/test_predictor_rl.py | 2 - 3 files changed, 3 insertions(+), 59 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 36f00050c..f8075714d 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -6,7 +6,7 @@ # # Licensed under the MIT License -"""Actions available in the reinforcement learning environment.""" +"""Actions (i.e. compiler passes) available in the reinforcement learning environment.""" from __future__ import annotations @@ -27,7 +27,6 @@ class CompilationOrigin(str, Enum): QISKIT = "qiskit" TKET = "tket" BQSKIT = "bqskit" - GENERAL = "general" class PassType(str, Enum): @@ -47,7 +46,7 @@ class Action: """Base class for all actions in the reinforcement learning environment.""" name: str - origin: CompilationOrigin + origin: CompilationOrigin | None pass_type: PassType transpile_pass: Any @@ -119,7 +118,7 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]: _bqskit_actions.bqskit_synthesis_action(), DeviceIndependentAction( "terminate", - CompilationOrigin.GENERAL, + None, PassType.TERMINATE, transpile_pass=[], ), diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index aafaa6e71..81c6166a7 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -407,59 +407,6 @@ def is_circuit_routed(self, circuit: QuantumCircuit, coupling_map: CouplingMap) return False return True - def is_circuit_laid_out(self, circuit: QuantumCircuit, layout: TranspileLayout | Layout) -> bool: - """True if every logical qubit in the circuit has a physical assignment.""" - if isinstance(layout, TranspileLayout): - # Use final_layout if available; otherwise fallback to initial_layout - layout = layout.final_layout or layout.initial_layout - - v2p = layout.get_virtual_bits() - return all(q in v2p for q in circuit.qubits) - - def is_circuit_synthesized(self, circuit: QuantumCircuit) -> bool: - """Check if the circuit uses only native gates of the device. - - Verifies that every gate name in the circuit is present in - ``device.operation_names``, equivalent to the ``GatesInBasis`` pass. - - Args: - circuit: QuantumCircuit to check. - - Returns: - True if all gates are native to the device. - """ - native_names = set(self.device.operation_names) - return all( - instr.operation.name in native_names or instr.operation.name in ("barrier", "measure") - for instr in circuit.data - ) - - def is_circuit_routed(self, circuit: QuantumCircuit, coupling_map: CouplingMap) -> bool: - """Check if a circuit is fully routed to the device, including directionality. - - A circuit is considered routed if all two-qubit gates are on qubit pairs - that exist as directed edges in the device coupling map. - - After a layout pass the circuit's qubits are already physical qubits, so - ``circuit.find_bit(q).index`` gives the physical index directly — - consistent with how ``reward.py`` looks up gate calibrations. - - Args: - circuit: QuantumCircuit to check. - coupling_map: CouplingMap of the target device. - - Returns: - True if fully routed, False otherwise. - """ - directed_edges = set(coupling_map.get_edges()) - for instr in circuit.data: - if len(instr.qubits) == 2: - q0 = circuit.find_bit(instr.qubits[0]).index - q1 = circuit.find_bit(instr.qubits[1]).index - if (q0, q1) not in directed_edges: - return False - return True - def determine_valid_actions_for_state(self) -> list[int]: """Determine valid actions based on circuit state: synthesized, mapped, routed.""" synthesized = self.is_circuit_synthesized(self.state) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 7883c4919..f0b9aa67a 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -32,9 +32,7 @@ register_action, remove_action, ) -from mqt.predictor.rl.actions import qiskit_actions as qiskit_actions_module from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model -from mqt.predictor.rl.predictorenv import PredictorEnv def test_predictor_env_reset_from_string() -> None: From 95c2f7b609c17a004c8c54876ef0c3da9f6f0fd2 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 12:47:45 +0200 Subject: [PATCH 28/42] =?UTF-8?q?=F0=9F=8E=A8=20move=20validity=20check=20?= =?UTF-8?q?into=20sdk=20specific=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .../predictor/rl/actions/bqskit_actions.py | 7 + .../predictor/rl/actions/qiskit_actions.py | 6 + src/mqt/predictor/rl/actions/tket_actions.py | 8 + src/mqt/predictor/rl/predictorenv.py | 189 ++++++++++-------- 5 files changed, 126 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 17b4dd8c9..455a1bf13 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ venv.bak/ .mypy_cache/ src/**/_version.py + +# RL training checkpoints and logs +events.out.* diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index 5482d8e43..9250277fa 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -284,3 +284,10 @@ def run_bqskit_action( raise ValueError(msg) return bqskit_to_qiskit(bqskit_compiled_qc), layout + + +def is_bqskit_action_available(*, has_layout: bool, has_parameterized_gates: bool) -> bool: + """Return whether a BQSKit action is available for the current circuit state.""" + # Using BQSKit after a layout is set can result in an error # TODO: Why? + # BQSKit does not support parameterized gates + return not (has_layout or has_parameterized_gates) diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py index 24f11d25f..b57f57156 100644 --- a/src/mqt/predictor/rl/actions/qiskit_actions.py +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -353,3 +353,9 @@ def run_qiskit_action( altered_qc = altered_qc.decompose(gates_to_decompose="unitary") return altered_qc, layout + + +def is_qiskit_action_available(action: Action, device: Target) -> bool: + """Return whether a Qiskit action is available for the current device.""" + # Only allow VF2PostLayout if "ibm" is in the device name # TODO: Why? + return action.name != "VF2PostLayout" or "ibm" in device.description diff --git a/src/mqt/predictor/rl/actions/tket_actions.py b/src/mqt/predictor/rl/actions/tket_actions.py index b77f3f749..c6b60c1b0 100644 --- a/src/mqt/predictor/rl/actions/tket_actions.py +++ b/src/mqt/predictor/rl/actions/tket_actions.py @@ -140,3 +140,11 @@ def run_tket_action( layout.final_layout = final_layout_pytket_to_qiskit(tket_qc, altered_qc) return altered_qc, layout + + +def is_tket_action_available(*, action: Action, has_layout: bool) -> bool: + """Return whether a TKET action is available for the current layout state.""" + # TKET layout/optimization actions must not run after a Qiskit layout has been set + # (it is not clear how tket will handle the layout). TKET routing actions, however, are + # designed to work after a Qiskit layout via PreProcessTKETRoutingAfterQiskitLayout. + return not has_layout or action.pass_type == PassType.ROUTING diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 81c6166a7..810fc7447 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging +import warnings from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -20,10 +21,6 @@ from qiskit.transpiler import Layout, Target from mqt.predictor.reward import figure_of_merit - from mqt.predictor.rl.actions import Action - - -import warnings import numpy as np from gymnasium import Env @@ -43,10 +40,13 @@ from mqt.predictor.rl.actions import ( CompilationOrigin, PassType, + bqskit_actions, get_actions_by_pass_type, + qiskit_actions, run_bqskit_action, run_qiskit_action, run_tket_action, + tket_actions, ) from mqt.predictor.rl.helper import create_feature_dict, get_path_training_circuits, get_state_sample @@ -138,8 +138,8 @@ def __init__( self.num_qubits_uncompiled_circuit = 0 # Canonical layout state for the current circuit. It is mirrored to - # QuantumCircuit.layout for callers, but kept here because TKET and - # BQSKit conversions do not preserve Qiskit's layout metadata. + # QuantumCircuit.layout for callers, but kept here because some action + # implementations do not preserve layout metadata across conversions. self.layout: TranspileLayout | None = None self.has_parameterized_gates = False @@ -169,25 +169,32 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any Raises: RuntimeError: If no valid actions are left. """ - self.used_actions.append(str(self.action_set[action].name)) - altered_qc = self.apply_action(action) + try: + self.used_actions.append(str(self.action_set[action].name)) + altered_qc = self.apply_action(action) + except Exception as exc: # noqa: BLE001 + self.error_occurred = True + return ( + create_feature_dict(self.state), # features + 0, # reward + False, # terminated + True, # truncated + {"error": f"{type(exc).__name__}: {exc}"}, # info + ) + if not altered_qc: + # TODO: Remove as dead code? return ( - create_feature_dict(self.state), - 0, - True, - False, - {}, + create_feature_dict(self.state), # features + 0, # reward + True, # terminated + False, # truncated + {}, # info ) self.state: QuantumCircuit = altered_qc self.num_steps += 1 - # in case a Qiskit.QuantumCircuit has `unitary` or `u`` gates in it, decompose them (otherwise qiskit will throw an error when applying BasisTranslator) - # TODO: will be improved by addressing issue https://github.com/munich-quantum-toolkit/predictor/issues/668 - if self.state.count_ops().get("unitary"): - self.state = self.state.decompose(gates_to_decompose="unitary") - self.state._layout = self.layout # noqa: SLF001 self.valid_actions = self.determine_valid_actions_for_state() @@ -196,7 +203,6 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any raise RuntimeError(msg) if action == self.action_terminate_index: - assert action in self.valid_actions, "Terminate action is not valid but was chosen." reward_val = self.calculate_reward() done = True else: @@ -262,47 +268,59 @@ def reset( return create_feature_dict(self.state), {} 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] + """Build the boolean action mask exposed to the RL policy. + ``self.valid_actions`` contains the structurally valid action indices + for the current circuit state. This method expands that sparse list to + one boolean per registered action and applies SDK-specific availability + filters that depend on circuit features or the selected device. + Terminate has no SDK origin and is accepted solely from the structural + candidate list. + + Returns: + A dense boolean mask ordered like ``self.action_set``. + """ has_layout = self.layout is not None + valid_action_indices = set(self.valid_actions) + action_mask: list[bool] = [] + + for action_index in range(len(self.action_set)): + action = self.action_set[action_index] + + if action_index not in valid_action_indices: + action_mask.append(False) + continue + if action.pass_type == PassType.TERMINATE: + action_mask.append(True) + continue + if action.origin == CompilationOrigin.QISKIT: + action_mask.append(qiskit_actions.is_qiskit_action_available(action, self.device)) + elif action.origin == CompilationOrigin.TKET: + action_mask.append(tket_actions.is_tket_action_available(action=action, has_layout=has_layout)) + elif action.origin == CompilationOrigin.BQSKIT: + action_mask.append( + bqskit_actions.is_bqskit_action_available( + has_layout=has_layout, + has_parameterized_gates=self.has_parameterized_gates, + ) + ) + else: + msg = f"Origin {action.origin} not supported." + raise ValueError(msg) - if has_layout: - # TKET layout/optimization actions must not run after a Qiskit layout has been set - # (it is not clear how tket will handle the layout). TKET routing actions are - # designed to work after a Qiskit layout via PreProcessTKETRoutingAfterQiskitLayout. - action_mask = [ - action_mask[i] - and (self.action_set[i].origin != CompilationOrigin.TKET or i in self.actions_routing_indices) - for i in range(len(action_mask)) - ] - - if self.has_parameterized_gates or has_layout: - # remove all actions that are from "origin"=="bqskit" because they are not supported for parameterized gates - # or after layout since using BQSKit after a layout is set can result in an error - action_mask = [ - action_mask[i] and self.action_set[i].origin != CompilationOrigin.BQSKIT - for i in range(len(action_mask)) - ] - - # only allow VF2PostLayout if "ibm" is in the device name - if "ibm" not in self.device.description: - action_mask = [ - action_mask[i] and self.action_set[i].name != "VF2PostLayout" for i in range(len(action_mask)) - ] return action_mask - def apply_action(self, action_index: int) -> QuantumCircuit | None: + def apply_action(self, action_index: int) -> QuantumCircuit: """Applies the given action to the current state and returns the altered state. Arguments: action_index: The index of the action to be applied, which must be in the action set. Returns: - The altered quantum circuit after applying the action, or None if the action is to terminate the compilation. + The altered quantum circuit after applying the action. Raises: - ValueError: If the action index is not in the action set or if the action origin is not supported. + ValueError: If the action index is not in the action set or if the action cannot be applied. """ if action_index not in self.action_set: msg = f"Action {action_index} not supported." @@ -310,48 +328,35 @@ def apply_action(self, action_index: int) -> QuantumCircuit | None: action = self.action_set[action_index] - if action.name == "terminate": + if action.pass_type == PassType.TERMINATE: return self.state - if action.origin == CompilationOrigin.QISKIT: - return self._apply_qiskit_action(action) - if action.origin == CompilationOrigin.TKET: - return self._apply_tket_action(action) - if action.origin == CompilationOrigin.BQSKIT: - return self._apply_bqskit_action(action) - msg = f"Origin {action.origin} not supported." - raise ValueError(msg) - - def _apply_qiskit_action(self, action: Action) -> QuantumCircuit: - altered_qc, self.layout = run_qiskit_action( - action=action, - circuit=self.state, - device=self.device, - layout=self.layout, - input_qubit_count=self.num_qubits_uncompiled_circuit, - ) - return altered_qc - - def _apply_tket_action(self, action: Action) -> QuantumCircuit: - altered_qc, self.layout = run_tket_action( - action=action, circuit=self.state, device=self.device, layout=self.layout - ) - return altered_qc - def _apply_bqskit_action(self, action: Action) -> QuantumCircuit: - """Applies the given BQSKit action to the current state and returns the altered state. - - Arguments: - action: The BQSKit action to be applied. - - Returns: - The altered quantum circuit after applying the action. + if action.origin == CompilationOrigin.QISKIT: + altered_qc, self.layout = run_qiskit_action( + action=action, + circuit=self.state, + device=self.device, + layout=self.layout, + input_qubit_count=self.num_qubits_uncompiled_circuit, + ) + elif action.origin == CompilationOrigin.TKET: + altered_qc, self.layout = run_tket_action( + action=action, + circuit=self.state, + device=self.device, + layout=self.layout, + ) + elif action.origin == CompilationOrigin.BQSKIT: + altered_qc, self.layout = run_bqskit_action( + action=action, + circuit=self.state, + device=self.device, + layout=self.layout, + ) + else: + msg = f"Origin {action.origin} not supported." + raise ValueError(msg) - Raises: - ValueError: If the action index is not in the action set or if the action origin is not supported. - """ - altered_qc, self.layout = run_bqskit_action( - action=action, circuit=self.state, device=self.device, layout=self.layout - ) return altered_qc def is_circuit_laid_out(self, circuit: QuantumCircuit, layout: TranspileLayout | Layout) -> bool: @@ -408,7 +413,17 @@ def is_circuit_routed(self, circuit: QuantumCircuit, coupling_map: CouplingMap) return True def determine_valid_actions_for_state(self) -> list[int]: - """Determine valid actions based on circuit state: synthesized, mapped, routed.""" + """Select structurally valid action indices for the current circuit state. + + The circuit is classified by compilation progress: synthesized to the + target gate set, laid out to physical qubits, and routed against the + directed coupling map. This method only determines which pass types can + advance that state; SDK/backend-specific availability filters are applied later + in ``action_masks``. + + Returns: + Action indices whose pass type can be attempted from the current state. + """ synthesized = self.is_circuit_synthesized(self.state) laid_out = self.is_circuit_laid_out(self.state, self.layout) if self.layout else False # Routing is only allowed after layout From 1815f5b0c4899c6b7a0d48610efbaac7348e50d7 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 13:00:20 +0200 Subject: [PATCH 29/42] =?UTF-8?q?=F0=9F=94=A5=20remove=20parsing=20shell?= =?UTF-8?q?=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/parsing.py | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 src/mqt/predictor/rl/parsing.py diff --git a/src/mqt/predictor/rl/parsing.py b/src/mqt/predictor/rl/parsing.py deleted file mode 100644 index f7ba54301..000000000 --- a/src/mqt/predictor/rl/parsing.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -"""Compatibility exports for circuit parsing helpers.""" - -from __future__ import annotations - -from mqt.predictor.rl.actions import ( - PreProcessTKETRoutingAfterQiskitLayout, - bqskit_to_qiskit, - final_layout_bqskit_to_qiskit, - final_layout_pytket_to_qiskit, - get_bqskit_native_gates, - postprocess_vf2postlayout, -) - -__all__ = [ - "PreProcessTKETRoutingAfterQiskitLayout", - "bqskit_to_qiskit", - "final_layout_bqskit_to_qiskit", - "final_layout_pytket_to_qiskit", - "get_bqskit_native_gates", - "postprocess_vf2postlayout", -] From 7ddf26712fb74f4dc02098d5c23cba1318d27c09 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 13:42:09 +0200 Subject: [PATCH 30/42] =?UTF-8?q?=F0=9F=8E=A8=20add=20invariants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../predictor/rl/actions/bqskit_actions.py | 3 +++ .../predictor/rl/actions/qiskit_actions.py | 24 +++++++++++++++++++ src/mqt/predictor/rl/actions/tket_actions.py | 12 ++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index 9250277fa..bf66f1849 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -88,6 +88,9 @@ def bqskit_optimization_action() -> Action: CompilationOrigin.BQSKIT, PassType.OPT, transpile_pass=lambda circuit: bqskit_compile(circuit, **_bqskit_compilation_options()), + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=False, ) diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py index b57f57156..977516a8b 100644 --- a/src/mqt/predictor/rl/actions/qiskit_actions.py +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -92,24 +92,36 @@ def qiskit_optimization_actions() -> list[Action]: CompilationOrigin.QISKIT, PassType.OPT, [Optimize1qGatesDecomposition()], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=False, ), DeviceIndependentAction( "CommutativeCancellation", CompilationOrigin.QISKIT, PassType.OPT, [CommutativeCancellation()], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=True, ), DeviceIndependentAction( "CommutativeInverseCancellation", CompilationOrigin.QISKIT, PassType.OPT, [CommutativeInverseCancellation()], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=True, ), DeviceIndependentAction( "RemoveDiagonalGatesBeforeMeasure", CompilationOrigin.QISKIT, PassType.OPT, [RemoveDiagonalGatesBeforeMeasure()], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=True, ), DeviceIndependentAction( "InverseCancellation", @@ -131,18 +143,27 @@ def qiskit_optimization_actions() -> list[Action]: (SXGate(), SXdgGate()), ]) ], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=True, ), DeviceIndependentAction( "OptimizeCliffords", CompilationOrigin.QISKIT, PassType.OPT, [OptimizeCliffords()], + preserves_layout=True, + preserves_routing=False, + preserves_synthesis=False, ), DeviceIndependentAction( "Opt2qBlocks", CompilationOrigin.QISKIT, PassType.OPT, [Collect2qBlocks(), ConsolidateBlocks(), UnitarySynthesis()], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=False, ), ] @@ -153,6 +174,9 @@ def qiskit_o3_action() -> Action: "QiskitO3", CompilationOrigin.QISKIT, PassType.OPT, + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=True, transpile_pass=lambda native_gate, coupling_map: cast( "list[Task]", [ diff --git a/src/mqt/predictor/rl/actions/tket_actions.py b/src/mqt/predictor/rl/actions/tket_actions.py index c6b60c1b0..42cef28f0 100644 --- a/src/mqt/predictor/rl/actions/tket_actions.py +++ b/src/mqt/predictor/rl/actions/tket_actions.py @@ -57,24 +57,36 @@ def tket_optimization_actions() -> list[Action]: CompilationOrigin.TKET, PassType.OPT, [PeepholeOptimise2Q()], + preserves_layout=False, + preserves_routing=False, + preserves_synthesis=False, ), DeviceIndependentAction( "CliffordSimp", CompilationOrigin.TKET, PassType.OPT, [CliffordSimp()], + preserves_layout=False, + preserves_routing=False, + preserves_synthesis=False, ), DeviceIndependentAction( "FullPeepholeOptimiseCX", CompilationOrigin.TKET, PassType.OPT, [FullPeepholeOptimise()], + preserves_layout=False, + preserves_routing=False, + preserves_synthesis=False, ), DeviceIndependentAction( "RemoveRedundancies", CompilationOrigin.TKET, PassType.OPT, [RemoveRedundancies()], + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=True, ), ] From 3ec22c772ad83932346212f65274c9a772ef19dc Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 14:50:47 +0200 Subject: [PATCH 31/42] =?UTF-8?q?=E2=9C=85=20update=20tests=20with=20indiv?= =?UTF-8?q?idual=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 3 + .../predictor/rl/actions/qiskit_actions.py | 7 +- tests/compilation/test_helper_rl.py | 9 +- .../test_integration_further_SDKs.py | 439 +++++++++--------- tests/compilation/test_predictor_rl.py | 7 +- 5 files changed, 241 insertions(+), 224 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index f8075714d..70012faeb 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -49,6 +49,9 @@ class Action: origin: CompilationOrigin | None pass_type: PassType transpile_pass: Any + preserves_layout: bool = False + preserves_routing: bool = False + preserves_synthesis: bool = False @dataclass diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py index 977516a8b..23ab09898 100644 --- a/src/mqt/predictor/rl/actions/qiskit_actions.py +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -320,7 +320,7 @@ def _postprocess_layout_action( property_set: PropertySet, altered_qc: QuantumCircuit, layout: TranspileLayout | None, - input_qubit_count: int, + input_qubit_count: int | None = None, ) -> tuple[QuantumCircuit, TranspileLayout | None]: """Update Qiskit's layout metadata after passes that can create or alter layouts.""" if action.name == "VF2PostLayout": @@ -345,8 +345,8 @@ def _postprocess_layout_action( initial_layout=property_set["layout"], input_qubit_mapping=property_set["original_qubit_indices"], final_layout=property_set["final_layout"], - _output_qubit_list=altered_qc.qubits, _input_qubit_count=input_qubit_count, + _output_qubit_list=altered_qc.qubits, ) return altered_qc, layout @@ -356,7 +356,7 @@ def run_qiskit_action( circuit: QuantumCircuit, device: Target, layout: TranspileLayout | None, - input_qubit_count: int, + input_qubit_count: int | None = None, ) -> tuple[QuantumCircuit, TranspileLayout | None]: """Apply a Qiskit action and return the updated circuit and layout metadata.""" passes = _qiskit_passes(action, device, layout) @@ -374,6 +374,7 @@ def run_qiskit_action( layout.final_layout = pm.property_set["final_layout"] if altered_qc.count_ops().get("unitary"): + # Custom "unitary" gates can not be processed further by other passes altered_qc = altered_qc.decompose(gates_to_decompose="unitary") return altered_qc, layout diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 612960129..6ad4d48a4 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -22,9 +22,14 @@ from qiskit.transpiler import PassManager from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason -from mqt.predictor.rl.actions import PassType, get_actions_by_pass_type +from mqt.predictor.rl.actions import ( + PassType, + bqskit_to_qiskit, + get_actions_by_pass_type, + get_bqskit_native_gates, + postprocess_vf2postlayout, +) from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model, get_path_training_circuits -from mqt.predictor.rl.parsing import bqskit_to_qiskit, get_bqskit_native_gates, postprocess_vf2postlayout if TYPE_CHECKING: from collections.abc import Callable diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index b9b5c077f..98f8f0552 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -10,247 +10,254 @@ from __future__ import annotations -import re -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import pytest -from bqskit.ext import qiskit_to_bqskit -from bqskit.ir.circuit import Circuit -from mqt.bench.targets import get_available_device_names, get_device -from pytket.circuit import Qubit -from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit +from mqt.bench.targets import get_device from qiskit import QuantumCircuit -from qiskit.transpiler import PassManager -from qiskit.transpiler.layout import TranspileLayout -from qiskit.transpiler.passes import CheckMap, GatesInBasis - -from mqt.predictor.rl.actions import CompilationOrigin, PassType, get_actions_by_pass_type -from mqt.predictor.rl.parsing import ( - bqskit_to_qiskit, - final_layout_bqskit_to_qiskit, - final_layout_pytket_to_qiskit, +from qiskit.circuit import StandardEquivalenceLibrary +from qiskit.transpiler import PassManager, TranspileLayout +from qiskit.transpiler.passes import ( + ApplyLayout, + BasisTranslator, + EnlargeWithAncilla, + FullAncillaAllocation, + SabreSwap, + TrivialLayout, ) -if TYPE_CHECKING: - from collections.abc import Callable +from mqt.predictor.rl.actions import PassType +from mqt.predictor.rl.predictorenv import PredictorEnv - from pytket._tket.passes import BasePass as TketBasePass - from qiskit.passmanager.base_tasks import Task +if TYPE_CHECKING: from qiskit.transpiler import Target - from mqt.predictor.rl.actions import Action - from mqt.predictor.rl.parsing import ( - PreProcessTKETRoutingAfterQiskitLayout, - ) - - -@pytest.fixture -def available_actions_dict() -> dict[PassType, list[Action]]: - """Return a dictionary of available actions.""" - return get_actions_by_pass_type() +def _setup_env(env: PredictorEnv, circuit: QuantumCircuit, layout: TranspileLayout | None, n_qubits: int) -> None: + """Reset env to the given circuit/layout state without starting a full RL episode.""" + env.reset(qc=circuit.copy()) + env.layout = layout + env.num_qubits_uncompiled_circuit = n_qubits -def test_bqskit_o2_action(available_actions_dict: dict[PassType, list[Action]]) -> None: - """Test the BQSKitO2 action.""" - action_bqskit_o2: Action | None = None - for action in available_actions_dict[PassType.OPT]: - if action.name == "BQSKitO2": - action_bqskit_o2 = action - assert action_bqskit_o2 is not None - - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - bqskit_qc = qiskit_to_bqskit(qc) - factory = cast("Callable[[Circuit], Circuit]", action_bqskit_o2.transpile_pass) - bqskit_qc_optimized = factory(bqskit_qc) - assert isinstance(bqskit_qc_optimized, Circuit) - optimized_qc = bqskit_to_qiskit(bqskit_qc_optimized) +def _is_available(env: PredictorEnv, idx: int) -> bool: + """Return True if action idx is structurally and SDK-valid for the current env state.""" + env.valid_actions = env.determine_valid_actions_for_state() + return env.action_masks()[idx] - assert optimized_qc != qc +@pytest.fixture +def target() -> Target: + """Fixture to provide the target device for testing.""" + return get_device("ibm_falcon_27") -@pytest.mark.parametrize("device", [get_device(name) for name in get_available_device_names()]) -def test_bqskit_synthesis_action(device: Target, available_actions_dict: dict[PassType, list[Action]]) -> None: - """Test the BQSKitSynthesis action for all devices.""" - action_bqskit_synthesis_action: Action | None = None - for action in available_actions_dict[PassType.SYNTHESIS]: - if action.name == "BQSKitSynthesis": - action_bqskit_synthesis_action = action - assert action_bqskit_synthesis_action is not None - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - - check_nat_gates = GatesInBasis(basis_gates=device.operation_names) - check_nat_gates(qc) - assert not check_nat_gates.property_set["all_gates_in_basis"] - - factory = cast("Callable[[Target], Callable[[Circuit], Circuit]]", action_bqskit_synthesis_action.transpile_pass) - lambda_ = factory(device) - bqskit_qc = qiskit_to_bqskit(qc) - if "rigetti" in device.description or "ionq" in device.description: - with pytest.raises(ValueError, match=re.escape("not supported in BQSKIT")): - bqskit_qc_compiled = lambda_(bqskit_qc) - return - bqskit_qc_compiled = lambda_(bqskit_qc) - assert isinstance(bqskit_qc_compiled, Circuit) - native_gates_qc = bqskit_to_qiskit(bqskit_qc_compiled) - - check_nat_gates = GatesInBasis(basis_gates=device.operation_names) - check_nat_gates(native_gates_qc) - only_nat_gates = check_nat_gates.property_set["all_gates_in_basis"] - assert only_nat_gates - - -def test_bqskit_mapping_action_swaps_necessary(available_actions_dict: dict[PassType, list[Action]]) -> None: - """Test the BQSKitMapping action for quantum circuit that requires SWAP gates.""" - bqskit_mapping_action = None - for action in available_actions_dict[PassType.MAPPING]: - if action.name == "BQSKitMapping": - bqskit_mapping_action = action - - assert bqskit_mapping_action is not None - - qc = QuantumCircuit(8) +@pytest.fixture +def simple_circuit() -> QuantumCircuit: + """Return a small circuit used to probe action invariants. + + CX(0, 2) is intentional: qubits 0 and 2 are not adjacent on ibm_falcon_27 + (qubit 0 only connects to 1), so SabreSwap inserts at least one SWAP. + This ensures the routed fixture carries a real SWAP so routing-preservation + checks are non-trivial. + """ + qc = QuantumCircuit(3) qc.h(0) - qc.cx(0, 1) qc.cx(0, 2) - qc.cx(0, 3) - qc.cx(0, 4) - qc.cx(0, 5) - qc.cx(0, 6) - qc.cx(0, 7) - - device = get_device("ibm_falcon_27") - bqskit_qc = qiskit_to_bqskit(qc) - factory = cast( - "Callable[[Target], Callable[[Circuit], tuple[Circuit, tuple[int, ...], tuple[int, ...]]]]", - bqskit_mapping_action.transpile_pass, - ) - bqskit_qc_mapped, input_mapping, output_mapping = factory(device)(bqskit_qc) - mapped_qc = bqskit_to_qiskit(bqskit_qc_mapped) - layout = final_layout_bqskit_to_qiskit(input_mapping, output_mapping, mapped_qc, qc) + qc.cx(1, 2) + return 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: Target, layout: TranspileLayout -) -> None: - """Check if the mapped quantum circuit is correctly mapped to the device.""" - # check if the altered circuit is correctly mapped to the device - check_mapping = CheckMap(coupling_map=device.build_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.num_qubits - if layout.final_layout is not None: - 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 - qreg = assigned_physical_qubit._register # noqa: SLF001 - 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.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 - 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_bqskit_mapping_action_no_swaps_necessary(available_actions_dict: dict[PassType, list[Action]]) -> None: - """Test the BQSKitMapping action for a simple quantum circuit that does not require SWAP gates.""" - bqskit_mapping_action: Action | None = None - for action in available_actions_dict[PassType.MAPPING]: - if action.name == "BQSKitMapping": - bqskit_mapping_action = action - assert bqskit_mapping_action is not None - - qc_no_swap_needed = QuantumCircuit(2) - qc_no_swap_needed.h(0) - qc_no_swap_needed.cx(0, 1) - - device = get_device("quantinuum_h2_56") - - bqskit_qc = qiskit_to_bqskit(qc_no_swap_needed) - factory = cast( - "Callable[[Target], Callable[[Circuit], tuple[Circuit, tuple[int, ...], tuple[int, ...]]]]", - bqskit_mapping_action.transpile_pass, +@pytest.fixture +def laid_out_circuit(simple_circuit: QuantumCircuit, target: Target) -> tuple[QuantumCircuit, TranspileLayout]: + """Return the simple circuit after layout with its TranspileLayout.""" + coupling_map = target.build_coupling_map() + pm = PassManager([ + TrivialLayout(coupling_map), + FullAncillaAllocation(coupling_map), + EnlargeWithAncilla(), + ApplyLayout(), + ]) + laid_out = pm.run(simple_circuit.copy()) + layout = TranspileLayout( + initial_layout=pm.property_set["layout"], + input_qubit_mapping=dict(pm.property_set["original_qubit_indices"]), + final_layout=pm.property_set.get("final_layout"), + _output_qubit_list=laid_out.qubits, + _input_qubit_count=simple_circuit.num_qubits, ) - bqskit_qc_mapped, input_mapping, output_mapping = factory(device)(bqskit_qc) - mapped_qc = bqskit_to_qiskit(bqskit_qc_mapped) - layout = 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) + return laid_out, layout -def test_tket_routing(available_actions_dict: dict[PassType, list[Action]]) -> 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 = get_device("quantinuum_h2_56") - - layout_action = available_actions_dict[PassType.LAYOUT][0] - factory = cast("Callable[[Target], list[Task]]", layout_action.transpile_pass) - passes_ = factory(device) - pm = PassManager(passes_) - layouted_qc = pm.run(qc) - initial_layout = pm.property_set["layout"] - input_qubit_mapping = pm.property_set["original_qubit_indices"] - - routing_action: Action | None = None - for action in available_actions_dict[PassType.ROUTING]: - if action.origin == CompilationOrigin.TKET: - routing_action = action - assert routing_action is not None - - tket_qc = qiskit_to_tk(layouted_qc, preserve_param_uuid=True) - factory = cast( - "Callable[[Target], list[TketBasePass | PreProcessTKETRoutingAfterQiskitLayout]]", routing_action.transpile_pass +@pytest.fixture +def laid_out_and_routed_circuit( + laid_out_circuit: tuple[QuantumCircuit, TranspileLayout], + target: Target, +) -> tuple[QuantumCircuit, TranspileLayout]: + """Return the laid-out circuit after SabreSwap routing with its TranspileLayout.""" + coupling_map = target.build_coupling_map() + laid_out, layout_before = laid_out_circuit + pm = PassManager([SabreSwap(coupling_map=coupling_map)]) + routed = pm.run(laid_out.copy()) + layout_after = TranspileLayout( + initial_layout=layout_before.initial_layout, + input_qubit_mapping=dict(layout_before.input_qubit_mapping), + final_layout=pm.property_set.get("final_layout"), + _output_qubit_list=routed.qubits, + _input_qubit_count=layout_before._input_qubit_count, # noqa: SLF001 ) - passes = factory(device) - for pass_ in passes: - pass_.apply(tket_qc) + return routed, layout_after - qbs = tket_qc.qubits - tket_qc.rename_units({qbs[i]: Qubit("q", i) for i in range(len(qbs))}) - mapped_qc = tk_to_qiskit(tket_qc, replace_implicit_swaps=True) +@pytest.fixture +def laid_out_and_routed_and_synthesized_circuit( + laid_out_and_routed_circuit: tuple[QuantumCircuit, TranspileLayout], + target: Target, +) -> tuple[QuantumCircuit, TranspileLayout]: + """Return the routed circuit translated to the device basis with the same layout.""" + routed, layout = laid_out_and_routed_circuit + pm = PassManager([BasisTranslator(StandardEquivalenceLibrary, target_basis=target.operation_names)]) + synthesized = pm.run(routed.copy()) + synthesized_layout = TranspileLayout( + initial_layout=layout.initial_layout, + input_qubit_mapping=dict(layout.input_qubit_mapping), + final_layout=layout.final_layout, + _output_qubit_list=synthesized.qubits, + _input_qubit_count=layout._input_qubit_count, # noqa: SLF001 + ) + return synthesized, synthesized_layout - final_layout = 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, - ) +@pytest.fixture +def env(target: Target) -> PredictorEnv: + """Create a PredictorEnv for state-based invariant checking.""" + return PredictorEnv(device=target, reward_function="expected_fidelity") - check_mapped_circuit(qc, mapped_qc, device, layout) + +def test_synthesis_actions_produce_native_gates( + simple_circuit: QuantumCircuit, + laid_out_circuit: tuple[QuantumCircuit, TranspileLayout], + laid_out_and_routed_circuit: tuple[QuantumCircuit, TranspileLayout], + env: PredictorEnv, +) -> None: + """Invariant: every synthesis action produces only native gates for all circuit states.""" + n_qubits = simple_circuit.num_qubits + qc_laid_out, laid_layout = laid_out_circuit + qc_routed, routed_layout = laid_out_and_routed_circuit + + test_cases = [ + ("uncompiled", simple_circuit, None), + ("laid-out", qc_laid_out, laid_layout), + ("routed", qc_routed, routed_layout), + ] + + for idx, action in env.action_set.items(): + if action.pass_type != PassType.SYNTHESIS: + continue + for kind, circuit, layout in test_cases: + _setup_env(env, circuit, layout, n_qubits) + if not _is_available(env, idx): + continue + compiled = env.apply_action(idx) + assert env.is_circuit_synthesized(compiled), ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT: " + f"synthesis produced non-native gates for {kind} circuit. " + f"Device native gates: {env.device.operation_names}. " + f"Circuit gates: {set(compiled.count_ops().keys())}" + ) + + +def test_layout_actions_establish_layout( + simple_circuit: QuantumCircuit, + env: PredictorEnv, +) -> None: + """Invariant: every layout action establishes a valid qubit assignment.""" + n_qubits = simple_circuit.num_qubits + + for idx, action in env.action_set.items(): + if action.pass_type != PassType.LAYOUT: + continue + _setup_env(env, simple_circuit, None, n_qubits) + if not _is_available(env, idx): + continue + compiled = env.apply_action(idx) + assert env.layout is not None, ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT: failed to establish layout" + ) + assert env.is_circuit_laid_out(compiled, env.layout), ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT: " + f"did not establish valid layout. Layout: {env.layout}" + ) + + +def test_routing_actions_route_circuit( + laid_out_circuit: tuple[QuantumCircuit, TranspileLayout], + env: PredictorEnv, +) -> None: + """Invariant: every routing action produces a circuit where all 2-qubit gates respect the coupling map.""" + qc_laid_out, layout = laid_out_circuit + n_qubits = qc_laid_out.num_qubits + coupling_map = env.device.build_coupling_map() + + for idx, action in env.action_set.items(): + if action.pass_type != PassType.ROUTING: + continue + _setup_env(env, qc_laid_out, layout, n_qubits) + if not _is_available(env, idx): + continue + routed = env.apply_action(idx) + assert env.is_circuit_routed(routed, coupling_map), ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT: circuit not properly routed after action" + ) + + +def test_optimization_actions_preserve_invariants( + laid_out_and_routed_circuit: tuple[QuantumCircuit, TranspileLayout], + laid_out_and_routed_and_synthesized_circuit: tuple[QuantumCircuit, TranspileLayout], + env: PredictorEnv, +) -> None: + """Invariant: OPT actions honour their declared preserves_layout/routing/synthesis contracts.""" + qc_routed, layout = laid_out_and_routed_circuit + qc_synthesized, layout_synth = laid_out_and_routed_and_synthesized_circuit + n_qubits = qc_routed.num_qubits + coupling_map = env.device.build_coupling_map() + + for idx, action in env.action_set.items(): + if action.pass_type != PassType.OPT: + continue + + if action.preserves_layout: + pre_v2p = dict(layout.initial_layout.get_virtual_bits()) + _setup_env(env, qc_routed, layout, n_qubits) + if _is_available(env, idx): + compiled = env.apply_action(idx) + assert env.is_circuit_laid_out(compiled, layout), ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_layout: " + f"circuit no longer has a valid layout after action" + ) + if env.layout is not None: + post_v2p = dict(env.layout.initial_layout.get_virtual_bits()) + assert post_v2p == pre_v2p, ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_layout: " + f"initial qubit assignment changed. Before: {pre_v2p}, After: {post_v2p}" + ) + + if action.preserves_routing: + _setup_env(env, qc_routed, layout, n_qubits) + if _is_available(env, idx): + compiled = env.apply_action(idx) + assert env.is_circuit_routed(compiled, coupling_map), ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_routing: " + f"produced gates on non-adjacent qubits" + ) + + if action.preserves_synthesis: + _setup_env(env, qc_synthesized, layout_synth, n_qubits) + if _is_available(env, idx): + compiled = env.apply_action(idx) + assert env.is_circuit_synthesized(compiled), ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_synthesis: " + f"introduced non-native gates. " + f"Device native gates: {env.device.operation_names}. " + f"Circuit gates: {set(compiled.count_ops().keys())}" + ) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index f0b9aa67a..0eb709a5a 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -29,6 +29,7 @@ DeviceIndependentAction, PassType, get_actions_by_pass_type, + qiskit_actions, register_action, remove_action, ) @@ -171,15 +172,15 @@ def __init__(self, _passes: object) -> None: def run(self, circuit: QuantumCircuit) -> QuantumCircuit: return circuit - monkeypatch.setattr(predictorenv_module, "PassManager", FakePassManager) + monkeypatch.setattr(qiskit_actions, "PassManager", FakePassManager) action = DeviceIndependentAction( name="SyntheticQiskitRouting", pass_type=PassType.ROUTING, transpile_pass=[], origin=CompilationOrigin.QISKIT, ) - - altered_qc = env._apply_qiskit_action(action) # noqa: SLF001 + env.action_set[0] = action + altered_qc = env.apply_action(action_index=0) assert altered_qc is env.state assert env.layout.final_layout is final_layout From 32c1edd75163bcb398f2fdc3e236a4ee0e4a2625 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 16:17:56 +0200 Subject: [PATCH 32/42] =?UTF-8?q?=F0=9F=8E=A8=20fix=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edadbbabf..c837e4992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Changed -- ♻️ Restructure existing RL passes into SDK-level action modules ([#644]) ([**@flowerthrower**]) +- ♻️ Restructure existing RL passes into SDK-level action modules ([#680]) ([**@flowerthrower**]) - 🎨 Improve the RL state machine logic ([#677]) ([**@flowerthrower**]) - 🐛 Support BQSKit conversion of IQM's native `r` gate ([#679]) ([**@flowerthrower**]) - 🔧 Replace `mypy` with `ty` ([#572]) ([**@denialhaag**]) @@ -49,6 +49,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool +[#680]: https://github.com/munich-quantum-toolkit/predictor/pull/680 [#677]: https://github.com/munich-quantum-toolkit/predictor/pull/677 [#679]: https://github.com/munich-quantum-toolkit/predictor/pull/679 [#572]: https://github.com/munich-quantum-toolkit/predictor/pull/572 From 760fc57c0bd21edf8fb84a178ee7e06fded30df1 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 16:37:07 +0200 Subject: [PATCH 33/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 21 +++++++++++++++---- .../predictor/rl/actions/bqskit_actions.py | 16 +++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 70012faeb..4e347ba79 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -71,7 +71,13 @@ class DeviceDependentAction(Action): def register_action(action: Action) -> Action: - """Registers a new action in the global actions registry. + """Registers a new Action in the global _ACTIONS registry. + + Args: + action: Action to register. + + Returns: + The registered Action. Raises: ValueError: If an action with the same name is already registered. @@ -84,10 +90,13 @@ def register_action(action: Action) -> Action: def remove_action(name: str) -> None: - """Removes an action from the global actions registry by name. + """Removes an Action from the global _ACTIONS registry by name. + + Args: + name: Name of the Action to remove. Raises: - ValueError: If no action with the given name is registered. + KeyError: If no action with the given name is registered. """ if name not in _ACTIONS: msg = f"No action with name {name} is registered." @@ -96,7 +105,11 @@ def remove_action(name: str) -> None: def get_actions_by_pass_type() -> dict[PassType, list[Action]]: - """Returns a dictionary mapping each PassType to a list of Actions of that type.""" + """Groups registered Actions from the global _ACTIONS registry by PassType. + + Returns: + A dictionary mapping each PassType to the list of registered Actions of that type. + """ result: dict[PassType, list[Action]] = defaultdict(list) for action in _ACTIONS.values(): result[action.pass_type].append(action) diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index bf66f1849..f0f2d5835 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -135,11 +135,11 @@ def bqskit_synthesis_action() -> Action: def get_bqskit_native_gates(device: Target) -> list[Gate]: """Returns the native gates of the given device. - Arguments: - device: The device for which the native gates are returned. + Args: + device: Target whose operation names are translated to BQSKit native gates. Returns: - The native gates of the given device as BQSKit gates. + The native gates of the given Target as a list of BQSKit Gate objects. Raises: ValueError: If a gate in the device is not supported in BQSKit. @@ -228,6 +228,16 @@ def final_layout_bqskit_to_qiskit( 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. + + Args: + bqskit_initial_layout: Tuple mapping each BQSKit virtual qubit index to its initial physical qubit index. + bqskit_final_layout: Tuple mapping each BQSKit virtual qubit index to its final physical qubit index. + compiled_qc: Compiled QuantumCircuit whose qubits define the output qubit list and final layout values. + initial_qc: Initial QuantumCircuit whose qubits define the input layout values. + + Returns: + A TranspileLayout with a Qiskit Layout for the initial layout, an input-qubit-to-index mapping, and a final + Layout mapping physical qubit indices to compiled QuantumCircuit qubits when BQSKit changed the layout. """ ancilla = QuantumRegister(compiled_qc.num_qubits - initial_qc.num_qubits, "ancilla") qiskit_initial_layout = {} From 65527f3d2121ffb6c58080c3d9e54e12b69072e0 Mon Sep 17 00:00:00 2001 From: Patrick Hopf <81010725+flowerthrower@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:37:18 +0200 Subject: [PATCH 34/42] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Patrick Hopf <81010725+flowerthrower@users.noreply.github.com> --- src/mqt/predictor/rl/actions/__init__.py | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 70012faeb..f85c65ec5 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -41,9 +41,19 @@ class PassType(str, Enum): TERMINATE = "terminate" -@dataclass +`@dataclass` class Action: - """Base class for all actions in the reinforcement learning environment.""" + """Base class for all actions in the reinforcement learning environment. + + Attributes: + name: Unique action name. + origin: SDK origin of the action; ``None`` for terminate. + pass_type: Category of pass represented by this action. + transpile_pass: Pass object(s) executed for this action. + preserves_layout: Whether action preserves existing layout. + preserves_routing: Whether action preserves existing routing. + preserves_synthesis: Whether action preserves synthesis state. + """ name: str origin: CompilationOrigin | None @@ -54,19 +64,22 @@ class Action: preserves_synthesis: bool = False -@dataclass +`@dataclass` class DeviceIndependentAction(Action): """Action that represents a static compilation pass that can be applied directly.""" -@dataclass +`@dataclass` class DeviceDependentAction(Action): - """Action that represents a device-specific compilation pass that can be applied to a specific device.""" + """Device-specific action that depends on a target device. + + Attributes: + do_while: Optional do-while predicate for pass-manager execution. + """ transpile_pass: Any do_while: Callable[[PropertySet], bool] | None = None - _ACTIONS: dict[str, Action] = {} @@ -91,7 +104,7 @@ def remove_action(name: str) -> None: """ if name not in _ACTIONS: msg = f"No action with name {name} is registered." - raise KeyError(msg) + raise ValueError(msg) del _ACTIONS[name] From a7f5bc8674cffda8029c9f63112e7d7379a904c4 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 16:46:40 +0200 Subject: [PATCH 35/42] =?UTF-8?q?=F0=9F=8E=A8=20update=20post-layout=20in?= =?UTF-8?q?=20qiskit=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 6 +++--- src/mqt/predictor/rl/actions/qiskit_actions.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index d0c9ad887..325b268dc 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -41,7 +41,7 @@ class PassType(str, Enum): TERMINATE = "terminate" -`@dataclass` +@dataclass class Action: """Base class for all actions in the reinforcement learning environment. @@ -64,12 +64,12 @@ class Action: preserves_synthesis: bool = False -`@dataclass` +@dataclass class DeviceIndependentAction(Action): """Action that represents a static compilation pass that can be applied directly.""" -`@dataclass` +@dataclass class DeviceDependentAction(Action): """Device-specific action that depends on a target device. diff --git a/src/mqt/predictor/rl/actions/qiskit_actions.py b/src/mqt/predictor/rl/actions/qiskit_actions.py index 23ab09898..1e5c20e31 100644 --- a/src/mqt/predictor/rl/actions/qiskit_actions.py +++ b/src/mqt/predictor/rl/actions/qiskit_actions.py @@ -328,7 +328,8 @@ def _postprocess_layout_action( post_layout = property_set["post_layout"] if post_layout: assert layout is not None - altered_qc, _ = postprocess_vf2postlayout(altered_qc, post_layout, layout) + altered_qc, apply_layout = postprocess_vf2postlayout(altered_qc, post_layout, layout) + property_set = apply_layout.property_set elif action.name == "VF2Layout": if property_set["VF2Layout_stop_reason"] != VF2LayoutStopReason.SOLUTION_FOUND: logger.warning( From a02ecec5a1284ccd179ea7af07b7030ea9221535 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 5 Jun 2026 16:50:58 +0200 Subject: [PATCH 36/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/compilation/test_predictor_rl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 0eb709a5a..5864c4e7a 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -179,8 +179,9 @@ def run(self, circuit: QuantumCircuit) -> QuantumCircuit: transpile_pass=[], origin=CompilationOrigin.QISKIT, ) - env.action_set[0] = action - altered_qc = env.apply_action(action_index=0) + routing_action_index = next(iter(env.actions_routing_indices)) + env.action_set[routing_action_index] = action + altered_qc = env.apply_action(action_index=routing_action_index) assert altered_qc is env.state assert env.layout.final_layout is final_layout From 9774e4d98079b1aa2560245e1b366a970132c6e0 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 8 Jun 2026 15:01:06 +0200 Subject: [PATCH 37/42] =?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/actions/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 325b268dc..f814fb55a 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -80,6 +80,7 @@ class DeviceDependentAction(Action): transpile_pass: Any do_while: Callable[[PropertySet], bool] | None = None + _ACTIONS: dict[str, Action] = {} From 06f6f61268930c9b2b0ce3816c72e795930c5cd3 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 8 Jun 2026 15:11:33 +0200 Subject: [PATCH 38/42] =?UTF-8?q?=F0=9F=8E=A8=20comply=20with=20goolge=20d?= =?UTF-8?q?ocstings?= 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 810fc7447..0e1f8778d 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -257,7 +257,7 @@ def reset( self.num_steps = 0 self.used_actions = [] - self.layout = None + self.layout = self.is_circuit_laid_out(self.state, self.layout) if self.layout else None self.valid_actions = self.actions_synthesis_indices + self.actions_opt_indices @@ -313,7 +313,7 @@ def action_masks(self) -> list[bool]: def apply_action(self, action_index: int) -> QuantumCircuit: """Applies the given action to the current state and returns the altered state. - Arguments: + Args: action_index: The index of the action to be applied, which must be in the action set. Returns: From aa24d036d7121b5e3cd0559bdb175373d17ce57c Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 8 Jun 2026 15:16:09 +0200 Subject: [PATCH 39/42] =?UTF-8?q?=F0=9F=8E=A8=20calm=20down=20rabbit=20com?= =?UTF-8?q?plaining=20about=20Arguments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/hellinger/utils.py | 2 +- src/mqt/predictor/ml/helper.py | 2 +- src/mqt/predictor/ml/predictor.py | 16 ++++++++-------- src/mqt/predictor/qcompile.py | 2 +- src/mqt/predictor/reward.py | 8 ++++---- src/mqt/predictor/rl/helper.py | 4 ++-- src/mqt/predictor/rl/predictor.py | 8 ++++---- src/mqt/predictor/rl/predictorenv.py | 11 ++++++----- src/mqt/predictor/utils.py | 2 +- 9 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/mqt/predictor/hellinger/utils.py b/src/mqt/predictor/hellinger/utils.py index 3383f0ace..d50454c0e 100644 --- a/src/mqt/predictor/hellinger/utils.py +++ b/src/mqt/predictor/hellinger/utils.py @@ -39,7 +39,7 @@ def calc_device_specific_features( ) -> NDArray[np.float64]: """Creates and returns a device-specific feature vector for a given quantum circuit and device. - Arguments: + Args: qc: The quantum circuit for which the features are calculated. device: The device for which the features are calculated. ignore_gates: A list of gates to be ignored when calculating the features. Defaults to ["barrier", "id", "measure", "if_else"]. diff --git a/src/mqt/predictor/ml/helper.py b/src/mqt/predictor/ml/helper.py index d73a7d85c..22dc15d5d 100644 --- a/src/mqt/predictor/ml/helper.py +++ b/src/mqt/predictor/ml/helper.py @@ -112,7 +112,7 @@ def dict_to_featurevector(gate_dict: dict[str, int]) -> dict[str, int]: def create_feature_vector(qc: QuantumCircuit) -> list[int | float]: """Creates and returns a feature dictionary for a given quantum circuit. - Arguments: + Args: qc: The quantum circuit to be compiled. Returns: diff --git a/src/mqt/predictor/ml/predictor.py b/src/mqt/predictor/ml/predictor.py index 8863f337e..664ce5e0f 100644 --- a/src/mqt/predictor/ml/predictor.py +++ b/src/mqt/predictor/ml/predictor.py @@ -66,7 +66,7 @@ def setup_device_predictor( ) -> bool: """Sets up the device predictor for the given figure of merit. - Arguments: + Args: devices: The devices to be used for training. figure_of_merit: The figure of merit to be used for training. Defaults to "expected_fidelity". path_uncompiled_circuits: The path to the directory containing the circuits to be compiled. Defaults to None. @@ -127,7 +127,7 @@ def __init__( ) -> None: """Initializes the Predictor class. - Arguments: + Args: figure_of_merit: The figure of merit to be used for training. devices: The devices to be used for training. logger_level: The level of the logger. Defaults to logging.INFO. @@ -151,7 +151,7 @@ def _compile_all_circuits_devicewise( ) -> None: """Compiles all circuits in the given directory with the given timeout and saves them in the given directory. - Arguments: + Args: device: The device to be used for compilation. timeout: The timeout in seconds for the compilation of a single circuit. path_uncompiled_circuits: The path to the directory containing the circuits to be compiled. Defaults to None. @@ -204,7 +204,7 @@ def compile_training_circuits( ) -> None: """Compiles all circuits in the given directory with the given timeout and saves them in the given directory. - Arguments: + Args: path_uncompiled_circuits: The path to the directory containing the circuits to be compiled. Defaults to None. path_compiled_circuits: The path to the directory where the compiled circuits should be saved. Defaults to None. timeout: The timeout in seconds for the compilation of a single circuit. Defaults to 600. @@ -237,7 +237,7 @@ def generate_training_data( ) -> None: """Creates and saves training data from all generated training samples. - Arguments: + Args: path_uncompiled_circuits: The path to the directory containing the uncompiled circuits. Defaults to None. path_compiled_circuits: The path to the directory containing the compiled circuits. Defaults to None. path_training_data: The path to the directory where the generated training data should be saved. Defaults to None. @@ -295,7 +295,7 @@ def _generate_training_sample( ) -> tuple[tuple[list[Any], Any], str, list[float]]: """Handles to create a training sample from a given file. - Arguments: + Args: file: The name of the file to be used for training. path_uncompiled_circuit: The path to the directory containing the uncompiled circuits. Defaults to None. path_compiled_circuits: The path to the directory containing the compiled circuits. Defaults to None. @@ -365,7 +365,7 @@ def train_random_forest_model( ) -> RandomForestRegressor | RandomForestClassifier: """Trains a random forest model for the given figure of merit. - Arguments: + Args: training_data: The training data to be used for training the model. If None, the training data is loaded from the pre-prepared training data files. Returns: @@ -453,7 +453,7 @@ def predict_device_for_figure_of_merit( ) -> Target: """Returns the probabilities for all supported quantum devices to be the most suitable one for the given quantum circuit. - Arguments: + Args: qc: The QuantumCircuit or Path to the respective qasm file. figure_of_merit: The figure of merit to be used for compilation. diff --git a/src/mqt/predictor/qcompile.py b/src/mqt/predictor/qcompile.py index d65be982e..91cd88a54 100644 --- a/src/mqt/predictor/qcompile.py +++ b/src/mqt/predictor/qcompile.py @@ -27,7 +27,7 @@ def qcompile( ) -> tuple[QuantumCircuit, list[str], str]: """Compiles a given quantum circuit to a device with the highest predicted figure of merit. - Arguments: + Args: qc: The quantum circuit to be compiled. figure_of_merit: The figure of merit to be used for compilation. Defaults to "expected_fidelity". diff --git a/src/mqt/predictor/reward.py b/src/mqt/predictor/reward.py index e8c1b4742..2009eba36 100644 --- a/src/mqt/predictor/reward.py +++ b/src/mqt/predictor/reward.py @@ -46,7 +46,7 @@ def crit_depth(qc: QuantumCircuit, precision: int = 10) -> float: def expected_fidelity(qc: QuantumCircuit, device: Target, precision: int = 10) -> float: """Calculates the expected fidelity of a given quantum circuit on a given device. - Arguments: + Args: qc: The quantum circuit to be compiled. device: The device to be used for compilation. precision: The precision of the returned value. Defaults to 10. @@ -83,7 +83,7 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: It is calculated by multiplying the expected fidelity with a min(T1,T2)-dependent decay factor during qubit idle times. To this end, the circuit is scheduled using ASAP scheduling. - Arguments: + Args: qc: The quantum circuit to be compiled. device: The device to be used for compilation. precision: The precision of the returned value. Defaults to 10. @@ -210,7 +210,7 @@ def estimated_success_probability(qc: QuantumCircuit, device: Target, precision: def esp_data_available(device: Target) -> bool: """Check if calibration data to calculate ESP is available for the device. - Arguments: + Args: device: The device to be checked for calibration data. Returns: @@ -310,7 +310,7 @@ def estimated_hellinger_distance( ) -> float: """Calculates the estimated Hellinger distance of a given quantum circuit on a given device. - Arguments: + Args: qc: The quantum circuit to be compiled. device: The device to be used for compilation. model: The pre-trained model to use for prediction (optional). If not provided, the model will try to be loaded from files. diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index f2d8c01e8..49532414f 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -32,7 +32,7 @@ def get_state_sample(max_qubits: int, path_training_circuits: Path, rng: Generator) -> tuple[QuantumCircuit, str]: """Returns a random quantum circuit from the training circuits folder. - Arguments: + Args: max_qubits: The maximum number of qubits the returned quantum circuit may have. If no limit is set, it defaults to None. path_training_circuits: The path to the training circuits folder. rng: A random number generator to select a random quantum circuit. @@ -73,7 +73,7 @@ def get_state_sample(max_qubits: int, path_training_circuits: Path, rng: Generat def create_feature_dict(qc: QuantumCircuit) -> dict[str, int | NDArray[np.float32]]: """Creates a feature dictionary for a given quantum circuit. - Arguments: + Args: qc: The quantum circuit for which the feature dictionary is created. Returns: diff --git a/src/mqt/predictor/rl/predictor.py b/src/mqt/predictor/rl/predictor.py index c1578125a..3c57eb647 100644 --- a/src/mqt/predictor/rl/predictor.py +++ b/src/mqt/predictor/rl/predictor.py @@ -54,7 +54,7 @@ def compile_as_predicted( ) -> tuple[QuantumCircuit, list[str]]: """Compiles a given quantum circuit such that the given figure of merit is maximized by using the respectively trained optimized compiler. - Arguments: + Args: qc: The quantum circuit to be compiled or the path to a qasm file containing the quantum circuit. Returns: @@ -93,7 +93,7 @@ def train_model( ) -> None: """Trains all models for the given reward functions and device. - Arguments: + Args: timesteps: The number of timesteps to train the model. Defaults to 1000. verbose: The verbosity level. Defaults to 2. test: Whether to train the model for testing purposes. Defaults to False. @@ -136,7 +136,7 @@ def train_model( def load_model(model_name: str) -> MaskablePPO: """Loads a trained model from the trained model folder. - Arguments: + Args: model_name: The name of the model to be loaded. Returns: @@ -162,7 +162,7 @@ def rl_compile( ) -> tuple[QuantumCircuit, list[str]]: """Compiles a given quantum circuit to a device optimizing for the given figure of merit. - Arguments: + Args: qc: The quantum circuit to be compiled. If a string is given, it is assumed to be a path to a qasm file. device: The device to compile to. figure_of_merit: The figure of merit to be used for compilation. Defaults to "expected_fidelity". diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 0e1f8778d..ee744d400 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -64,7 +64,7 @@ def __init__( ) -> None: """Initializes the PredictorEnv object. - Arguments: + Args: device: The target device to be used for compilation. reward_function: The figure of merit to be used for the reward function. Defaults to "expected_fidelity". path_training_circuits: The path to the training circuits folder. Defaults to None, which uses the default path. @@ -160,7 +160,7 @@ def __init__( def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any, Any]]: """Executes the given action and returns the new state, the reward, whether the episode is done, whether the episode is truncated and additional information. - Arguments: + Args: action: The action to be executed, represented by its index in the action set. Returns: @@ -173,13 +173,14 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any self.used_actions.append(str(self.action_set[action].name)) altered_qc = self.apply_action(action) except Exception as exc: # noqa: BLE001 + # Different passes may fail for various reasons (e.g., found no routing solution). self.error_occurred = True return ( create_feature_dict(self.state), # features 0, # reward False, # terminated True, # truncated - {"error": f"{type(exc).__name__}: {exc}"}, # info + {"Truncated because of error": f"{type(exc).__name__}: {exc}"}, # info ) if not altered_qc: @@ -237,7 +238,7 @@ def reset( ) -> tuple[dict[str, Any], dict[str, Any]]: """Resets the environment to the given state or a random state. - Arguments: + Args: qc: The quantum circuit to be compiled or the path to a qasm file containing the quantum circuit. Defaults to None. seed: The seed to be used for the random number generator. Defaults to None. options: Additional options. Defaults to None. @@ -257,7 +258,7 @@ def reset( self.num_steps = 0 self.used_actions = [] - self.layout = self.is_circuit_laid_out(self.state, self.layout) if self.layout else None + self.layout = None self.valid_actions = self.actions_synthesis_indices + self.actions_opt_indices diff --git a/src/mqt/predictor/utils.py b/src/mqt/predictor/utils.py index 82631bd07..c30d2cfb0 100644 --- a/src/mqt/predictor/utils.py +++ b/src/mqt/predictor/utils.py @@ -39,7 +39,7 @@ def timeout_watcher( ) -> tuple[QuantumCircuit, list[str]] | bool: """Method that stops a function call after a given timeout limit. - Arguments: + Args: func: The function to be called. args: The arguments to be passed to the function. timeout: The timeout limit in seconds. From 93a714d37ddb4ad591c6926521722f4e8e38afbc Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 8 Jun 2026 15:40:28 +0200 Subject: [PATCH 40/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_integration_further_SDKs.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 98f8f0552..81a904135 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -34,14 +34,29 @@ def _setup_env(env: PredictorEnv, circuit: QuantumCircuit, layout: TranspileLayout | None, n_qubits: int) -> None: - """Reset env to the given circuit/layout state without starting a full RL episode.""" + """Reset env to the given circuit/layout state without starting a full RL episode. + + Args: + env: the PredictorEnv to set up + circuit: the QuantumCircuit to set as the current state + layout: the TranspileLayout to set as the current layout (if any) + n_qubits: the number of qubits in the uncompiled circuit (used for invariant check) + """ env.reset(qc=circuit.copy()) env.layout = layout env.num_qubits_uncompiled_circuit = n_qubits def _is_available(env: PredictorEnv, idx: int) -> bool: - """Return True if action idx is structurally and SDK-valid for the current env state.""" + """Check if action idx is available in the current env state. + + Args: + env: the PredictorEnv to check the action availability in + idx: the index of the action to check + + Returns: + True if action idx is structurally and SDK-valid for the current env state, False otherwise + """ env.valid_actions = env.determine_valid_actions_for_state() return env.action_masks()[idx] @@ -104,7 +119,7 @@ def laid_out_and_routed_circuit( input_qubit_mapping=dict(layout_before.input_qubit_mapping), final_layout=pm.property_set.get("final_layout"), _output_qubit_list=routed.qubits, - _input_qubit_count=layout_before._input_qubit_count, # noqa: SLF001 + _input_qubit_count=len(layout_before.input_qubit_mapping), ) return routed, layout_after @@ -123,7 +138,7 @@ def laid_out_and_routed_and_synthesized_circuit( input_qubit_mapping=dict(layout.input_qubit_mapping), final_layout=layout.final_layout, _output_qubit_list=synthesized.qubits, - _input_qubit_count=layout._input_qubit_count, # noqa: SLF001 + _input_qubit_count=len(layout.input_qubit_mapping), ) return synthesized, synthesized_layout @@ -235,12 +250,15 @@ def test_optimization_actions_preserve_invariants( f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_layout: " f"circuit no longer has a valid layout after action" ) - if env.layout is not None: - post_v2p = dict(env.layout.initial_layout.get_virtual_bits()) - assert post_v2p == pre_v2p, ( - f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_layout: " - f"initial qubit assignment changed. Before: {pre_v2p}, After: {post_v2p}" - ) + assert env.layout is not None, ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_layout: " + f"layout metadata was removed" + ) + post_v2p = dict(env.layout.initial_layout.get_virtual_bits()) + assert post_v2p == pre_v2p, ( + f"{action.name} on {env.device.description} VIOLATED INVARIANT preserves_layout: " + f"initial qubit assignment changed. Before: {pre_v2p}, After: {post_v2p}" + ) if action.preserves_routing: _setup_env(env, qc_routed, layout, n_qubits) From 69f42b3a84238e3442421cba08c2138a82c75c22 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 8 Jun 2026 16:32:41 +0200 Subject: [PATCH 41/42] =?UTF-8?q?=F0=9F=8E=A8=20remove=20import=20forwardi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 23 +------------------ src/mqt/predictor/rl/predictorenv.py | 15 +++++------- tests/compilation/test_helper_rl.py | 5 ++-- .../test_integration_further_SDKs.py | 19 ++------------- tests/compilation/test_predictor_rl.py | 2 +- 5 files changed, 12 insertions(+), 52 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index f814fb55a..c059ad12d 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -114,7 +114,7 @@ def remove_action(name: str) -> None: """ if name not in _ACTIONS: msg = f"No action with name {name} is registered." - raise ValueError(msg) + raise KeyError(msg) del _ACTIONS[name] @@ -155,34 +155,13 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]: ): register_action(_action) -PreProcessTKETRoutingAfterQiskitLayout = _tket_actions.PreProcessTKETRoutingAfterQiskitLayout -final_layout_pytket_to_qiskit = _tket_actions.final_layout_pytket_to_qiskit -run_tket_action = _tket_actions.run_tket_action - -final_layout_bqskit_to_qiskit = _bqskit_actions.final_layout_bqskit_to_qiskit -bqskit_to_qiskit = _bqskit_actions.bqskit_to_qiskit -get_bqskit_native_gates = _bqskit_actions.get_bqskit_native_gates -run_bqskit_action = _bqskit_actions.run_bqskit_action - -postprocess_vf2postlayout = _qiskit_actions.postprocess_vf2postlayout -run_qiskit_action = _qiskit_actions.run_qiskit_action - __all__ = [ "Action", "CompilationOrigin", "DeviceDependentAction", "DeviceIndependentAction", "PassType", - "PreProcessTKETRoutingAfterQiskitLayout", - "bqskit_to_qiskit", - "final_layout_bqskit_to_qiskit", - "final_layout_pytket_to_qiskit", "get_actions_by_pass_type", - "get_bqskit_native_gates", - "postprocess_vf2postlayout", "register_action", "remove_action", - "run_bqskit_action", - "run_qiskit_action", - "run_tket_action", ] diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index ee744d400..b3be49793 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -40,14 +40,11 @@ from mqt.predictor.rl.actions import ( CompilationOrigin, PassType, - bqskit_actions, get_actions_by_pass_type, - qiskit_actions, - run_bqskit_action, - run_qiskit_action, - run_tket_action, - tket_actions, ) +from mqt.predictor.rl.actions.bqskit_actions import is_bqskit_action_available, run_bqskit_action +from mqt.predictor.rl.actions.qiskit_actions import is_qiskit_action_available, run_qiskit_action +from mqt.predictor.rl.actions.tket_actions import is_tket_action_available, run_tket_action from mqt.predictor.rl.helper import create_feature_dict, get_path_training_circuits, get_state_sample logger = logging.getLogger("mqt-predictor") @@ -295,12 +292,12 @@ def action_masks(self) -> list[bool]: action_mask.append(True) continue if action.origin == CompilationOrigin.QISKIT: - action_mask.append(qiskit_actions.is_qiskit_action_available(action, self.device)) + action_mask.append(is_qiskit_action_available(action, self.device)) elif action.origin == CompilationOrigin.TKET: - action_mask.append(tket_actions.is_tket_action_available(action=action, has_layout=has_layout)) + action_mask.append(is_tket_action_available(action=action, has_layout=has_layout)) elif action.origin == CompilationOrigin.BQSKIT: action_mask.append( - bqskit_actions.is_bqskit_action_available( + is_bqskit_action_available( has_layout=has_layout, has_parameterized_gates=self.has_parameterized_gates, ) diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 6ad4d48a4..b99ad0fca 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -24,11 +24,10 @@ from mqt.predictor.rl.actions import ( PassType, - bqskit_to_qiskit, get_actions_by_pass_type, - get_bqskit_native_gates, - postprocess_vf2postlayout, ) +from mqt.predictor.rl.actions.bqskit_actions import bqskit_to_qiskit, get_bqskit_native_gates +from mqt.predictor.rl.actions.qiskit_actions import postprocess_vf2postlayout from mqt.predictor.rl.helper import create_feature_dict, get_path_trained_model, get_path_training_circuits if TYPE_CHECKING: diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 81a904135..057c7aa02 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -34,29 +34,14 @@ def _setup_env(env: PredictorEnv, circuit: QuantumCircuit, layout: TranspileLayout | None, n_qubits: int) -> None: - """Reset env to the given circuit/layout state without starting a full RL episode. - - Args: - env: the PredictorEnv to set up - circuit: the QuantumCircuit to set as the current state - layout: the TranspileLayout to set as the current layout (if any) - n_qubits: the number of qubits in the uncompiled circuit (used for invariant check) - """ + """Reset env to the given circuit/layout state without starting a full RL episode.""" env.reset(qc=circuit.copy()) env.layout = layout env.num_qubits_uncompiled_circuit = n_qubits def _is_available(env: PredictorEnv, idx: int) -> bool: - """Check if action idx is available in the current env state. - - Args: - env: the PredictorEnv to check the action availability in - idx: the index of the action to check - - Returns: - True if action idx is structurally and SDK-valid for the current env state, False otherwise - """ + """Return whether action idx is structurally and SDK-valid for the current env state.""" env.valid_actions = env.determine_valid_actions_for_state() return env.action_masks()[idx] diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 5864c4e7a..763ea54f9 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -22,6 +22,7 @@ from qiskit.transpiler import InstructionProperties, Layout, Target, TranspileLayout from qiskit.transpiler.passes import GatesInBasis +import mqt.predictor.rl.actions.qiskit_actions as qiskit_actions from mqt.predictor.rl import Predictor, rl_compile from mqt.predictor.rl import predictorenv as predictorenv_module from mqt.predictor.rl.actions import ( @@ -29,7 +30,6 @@ DeviceIndependentAction, PassType, get_actions_by_pass_type, - qiskit_actions, register_action, remove_action, ) From 846bb3993c088aa7e393e337ae5363048688332c Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 8 Jun 2026 18:28:51 +0200 Subject: [PATCH 42/42] =?UTF-8?q?=F0=9F=8E=A8=20improve=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_integration_further_SDKs.py | 117 ++++++++---------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 057c7aa02..713d24f03 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -46,78 +46,47 @@ def _is_available(env: PredictorEnv, idx: int) -> bool: return env.action_masks()[idx] -@pytest.fixture -def target() -> Target: - """Fixture to provide the target device for testing.""" - return get_device("ibm_falcon_27") - - -@pytest.fixture -def simple_circuit() -> QuantumCircuit: - """Return a small circuit used to probe action invariants. - - CX(0, 2) is intentional: qubits 0 and 2 are not adjacent on ibm_falcon_27 - (qubit 0 only connects to 1), so SabreSwap inserts at least one SWAP. - This ensures the routed fixture carries a real SWAP so routing-preservation - checks are non-trivial. - """ - qc = QuantumCircuit(3) - qc.h(0) - qc.cx(0, 2) - qc.cx(1, 2) - return qc - - -@pytest.fixture -def laid_out_circuit(simple_circuit: QuantumCircuit, target: Target) -> tuple[QuantumCircuit, TranspileLayout]: - """Return the simple circuit after layout with its TranspileLayout.""" +def _lay_out(circuit: QuantumCircuit, target: Target) -> tuple[QuantumCircuit, TranspileLayout]: + """Apply a trivial Qiskit layout to the circuit.""" coupling_map = target.build_coupling_map() - pm = PassManager([ + layout_pm = PassManager([ TrivialLayout(coupling_map), FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout(), ]) - laid_out = pm.run(simple_circuit.copy()) + laid_out = layout_pm.run(circuit.copy()) layout = TranspileLayout( - initial_layout=pm.property_set["layout"], - input_qubit_mapping=dict(pm.property_set["original_qubit_indices"]), - final_layout=pm.property_set.get("final_layout"), + initial_layout=layout_pm.property_set["layout"], + input_qubit_mapping=dict(layout_pm.property_set["original_qubit_indices"]), + final_layout=layout_pm.property_set.get("final_layout"), _output_qubit_list=laid_out.qubits, - _input_qubit_count=simple_circuit.num_qubits, + _input_qubit_count=circuit.num_qubits, ) return laid_out, layout -@pytest.fixture -def laid_out_and_routed_circuit( - laid_out_circuit: tuple[QuantumCircuit, TranspileLayout], - target: Target, -) -> tuple[QuantumCircuit, TranspileLayout]: - """Return the laid-out circuit after SabreSwap routing with its TranspileLayout.""" +def _route(circuit: QuantumCircuit, layout: TranspileLayout, target: Target) -> tuple[QuantumCircuit, TranspileLayout]: + """Route the laid-out circuit with SabreSwap.""" coupling_map = target.build_coupling_map() - laid_out, layout_before = laid_out_circuit - pm = PassManager([SabreSwap(coupling_map=coupling_map)]) - routed = pm.run(laid_out.copy()) - layout_after = TranspileLayout( - initial_layout=layout_before.initial_layout, - input_qubit_mapping=dict(layout_before.input_qubit_mapping), - final_layout=pm.property_set.get("final_layout"), + routing_pm = PassManager([SabreSwap(coupling_map=coupling_map)]) + routed = routing_pm.run(circuit.copy()) + routed_layout = TranspileLayout( + initial_layout=layout.initial_layout, + input_qubit_mapping=dict(layout.input_qubit_mapping), + final_layout=routing_pm.property_set.get("final_layout"), _output_qubit_list=routed.qubits, - _input_qubit_count=len(layout_before.input_qubit_mapping), + _input_qubit_count=len(layout.input_qubit_mapping), ) - return routed, layout_after + return routed, routed_layout -@pytest.fixture -def laid_out_and_routed_and_synthesized_circuit( - laid_out_and_routed_circuit: tuple[QuantumCircuit, TranspileLayout], - target: Target, +def _synthesize( + circuit: QuantumCircuit, layout: TranspileLayout, target: Target ) -> tuple[QuantumCircuit, TranspileLayout]: - """Return the routed circuit translated to the device basis with the same layout.""" - routed, layout = laid_out_and_routed_circuit - pm = PassManager([BasisTranslator(StandardEquivalenceLibrary, target_basis=target.operation_names)]) - synthesized = pm.run(routed.copy()) + """Translate the circuit to the target basis without changing its layout.""" + synthesis_pm = PassManager([BasisTranslator(StandardEquivalenceLibrary, target_basis=target.operation_names)]) + synthesized = synthesis_pm.run(circuit.copy()) synthesized_layout = TranspileLayout( initial_layout=layout.initial_layout, input_qubit_mapping=dict(layout.input_qubit_mapping), @@ -128,6 +97,28 @@ def laid_out_and_routed_and_synthesized_circuit( return synthesized, synthesized_layout +@pytest.fixture +def target() -> Target: + """Fixture to provide the target device for testing.""" + return get_device("ibm_falcon_27") + + +@pytest.fixture +def simple_circuit() -> QuantumCircuit: + """Return a small circuit used to probe action invariants. + + CX(0, 2) is intentional: qubits 0 and 2 are not adjacent on ibm_falcon_27 + (qubit 0 only connects to 1), so SabreSwap inserts at least one SWAP. + This ensures the routed fixture carries a real SWAP so routing-preservation + checks are non-trivial. + """ + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 2) + qc.cx(1, 2) + return qc + + @pytest.fixture def env(target: Target) -> PredictorEnv: """Create a PredictorEnv for state-based invariant checking.""" @@ -136,14 +127,12 @@ def env(target: Target) -> PredictorEnv: def test_synthesis_actions_produce_native_gates( simple_circuit: QuantumCircuit, - laid_out_circuit: tuple[QuantumCircuit, TranspileLayout], - laid_out_and_routed_circuit: tuple[QuantumCircuit, TranspileLayout], env: PredictorEnv, ) -> None: """Invariant: every synthesis action produces only native gates for all circuit states.""" n_qubits = simple_circuit.num_qubits - qc_laid_out, laid_layout = laid_out_circuit - qc_routed, routed_layout = laid_out_and_routed_circuit + qc_laid_out, laid_layout = _lay_out(simple_circuit, env.device) + qc_routed, routed_layout = _route(qc_laid_out, laid_layout, env.device) test_cases = [ ("uncompiled", simple_circuit, None), @@ -191,11 +180,11 @@ def test_layout_actions_establish_layout( def test_routing_actions_route_circuit( - laid_out_circuit: tuple[QuantumCircuit, TranspileLayout], + simple_circuit: QuantumCircuit, env: PredictorEnv, ) -> None: """Invariant: every routing action produces a circuit where all 2-qubit gates respect the coupling map.""" - qc_laid_out, layout = laid_out_circuit + qc_laid_out, layout = _lay_out(simple_circuit, env.device) n_qubits = qc_laid_out.num_qubits coupling_map = env.device.build_coupling_map() @@ -212,13 +201,13 @@ def test_routing_actions_route_circuit( def test_optimization_actions_preserve_invariants( - laid_out_and_routed_circuit: tuple[QuantumCircuit, TranspileLayout], - laid_out_and_routed_and_synthesized_circuit: tuple[QuantumCircuit, TranspileLayout], + simple_circuit: QuantumCircuit, env: PredictorEnv, ) -> None: """Invariant: OPT actions honour their declared preserves_layout/routing/synthesis contracts.""" - qc_routed, layout = laid_out_and_routed_circuit - qc_synthesized, layout_synth = laid_out_and_routed_and_synthesized_circuit + qc_laid_out, laid_layout = _lay_out(simple_circuit, env.device) + qc_routed, layout = _route(qc_laid_out, laid_layout, env.device) + qc_synthesized, layout_synth = _synthesize(qc_routed, layout, env.device) n_qubits = qc_routed.num_qubits coupling_map = env.device.build_coupling_map()