From 583201f7a68708f0016eb1f42a7abc8a0fcd98ee Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Mon, 13 Jul 2026 18:41:53 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9A=A7=20init=20draft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/predictor/rl/actions/__init__.py | 4 +- .../predictor/rl/actions/bqskit_actions.py | 523 ++++++++++++++---- 2 files changed, 431 insertions(+), 96 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 500141c96..03ca03b14 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -124,11 +124,13 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]: bqskit_actions.bqskit_optimization_action(), qiskit_actions.qiskit_final_optimization_action(), *qiskit_actions.qiskit_layout_actions(), + *bqskit_actions.bqskit_layout_actions(), tket_actions.tket_routing_action(), + bqskit_actions.bqskit_routing_action(), qiskit_actions.qiskit_mapping_action(), bqskit_actions.bqskit_mapping_action(), qiskit_actions.qiskit_synthesis_action(), - bqskit_actions.bqskit_synthesis_action(), + *bqskit_actions.bqskit_synthesis_actions(), DeviceIndependentAction( "terminate", None, diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index fa6af80a7..d2b7f59cf 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -13,15 +13,45 @@ import os import re from functools import cache -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, TypeAlias, cast +import numpy as np from bqskit import MachineModel from bqskit import compile as bqskit_compile +from bqskit.compiler import Compiler, Workflow +from bqskit.compiler.compile import ( + build_multi_qudit_retarget_workflow, + build_partitioning_workflow, + build_single_qudit_retarget_workflow, + get_instantiate_options, +) from bqskit.ext import qiskit_to_bqskit from bqskit.ext.qiskit.translate import OPENQASM2Language from bqskit.ir import gates +from bqskit.passes import ( + ApplyPlacement, + BlockZXZPass, + ExtractMeasurements, + FullBlockZXZPass, + GeneralizedSabreLayoutPass, + GeneralizedSabreRoutingPass, + GreedyPlacementPass, + IfThenElsePass, + LEAPSynthesisPass, + MGDPass, + PassPredicate, + QSDPass, + QSearchSynthesisPass, + RestoreMeasurements, + SetModelPass, + SetRandomSeedPass, + StaticPlacementPass, + TrivialPlacementPass, + UnfoldPass, + WalshDiagonalSynthesisPass, +) from qiskit import qasm2 -from qiskit.circuit import QuantumRegister +from qiskit.circuit import Instruction, QuantumRegister from qiskit.circuit.library import RGate from qiskit.transpiler import Layout, TranspileLayout @@ -31,43 +61,33 @@ from collections.abc import Callable from bqskit import Circuit + from bqskit.compiler.basepass import BasePass as BQSKitBasePass + from bqskit.compiler.passdata import PassData + from bqskit.compiler.workflow import WorkflowLike from bqskit.ir import Gate from qiskit import QuantumCircuit - from qiskit.circuit import Instruction + from qiskit.circuit import Qubit as QiskitQubit from qiskit.transpiler import Target from mqt.predictor.rl.actions import Action + BQSKitMapping: TypeAlias = tuple[Circuit, tuple[int, ...], tuple[int, ...]] + + +_BQSKIT_OPT_LEVEL = 1 if os.getenv("GITHUB_ACTIONS") == "true" else 2 +_BQSKIT_SYNTHESIS_EPSILON = 1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8 +_BQSKIT_MAX_SYNTHESIS_SIZE = 3 +_BQSKIT_SEED = 10 +_BQSKIT_NUM_WORKERS = 1 if os.getenv("GITHUB_ACTIONS") == "true" else -1 + def _r_gate(theta: float, phi: float) -> Instruction: """Construct an RGate with the given parameters.""" 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's QuantumCircuit. - - 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. - """ + """Convert a BQSKit circuit to Qiskit while preserving IQM's R gate.""" qasm = OPENQASM2Language().encode(circuit) qasm = re.sub(r"\bU1q\(", "r(", qasm) return qasm2.loads( @@ -81,54 +101,34 @@ def bqskit_to_qiskit(circuit: Circuit) -> QuantumCircuit: ) -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()), - preserves_layout=True, - preserves_routing=True, - preserves_synthesis=False, - ) +class _DiagonalUnitaryPredicate(PassPredicate): + """Return ``True`` when the current BQSKit block unitary is diagonal.""" + def __init__(self, atol: float = 1e-9) -> None: + self.atol = atol -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 get_truth_value(self, circuit: Circuit, data: PassData) -> bool: + """Check whether ``circuit`` represents a diagonal unitary.""" + del data + unitary = np.asarray(circuit.get_unitary()) + diagonal = np.diag(np.diag(unitary)) + return np.allclose(unitary, diagonal, atol=self.atol) -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(), - ) - ), - ) +def _run_bqskit_workflow( + circuit: Circuit, workflow: Workflow, request_data: bool = False +) -> Circuit | tuple[Circuit, PassData]: + """Compile ``circuit`` with a custom BQSKit workflow.""" + compiler = Compiler() + try: + result = compiler.compile(circuit, workflow, request_data=request_data) + finally: + compiler.close() + + if request_data: + return cast("tuple[Circuit, PassData]", result) + + return cast("Circuit", result) @cache @@ -217,6 +217,263 @@ def get_bqskit_native_gates(device: Target) -> list[Gate]: return native_gates +def _bqskit_partitioned_synthesis_factory( + device: Target, + synthesis_pass: WorkflowLike, +) -> Callable[[Circuit], Circuit]: + """Create a block-based BQSKit synthesis callable for an RL action.""" + + def _compile(circuit: Circuit) -> Circuit: + model = MachineModel(circuit.num_qudits, gate_set=get_bqskit_native_gates(device)) + workflow = Workflow([ + SetRandomSeedPass(_BQSKIT_SEED), + UnfoldPass(), + ExtractMeasurements(), + SetModelPass(model), + build_partitioning_workflow( + synthesis_pass, + _BQSKIT_MAX_SYNTHESIS_SIZE, + replace_filter_method="less-than-respecting-fully", + ), + build_multi_qudit_retarget_workflow( + _BQSKIT_OPT_LEVEL, + _BQSKIT_SYNTHESIS_EPSILON, + _BQSKIT_MAX_SYNTHESIS_SIZE, + ), + build_single_qudit_retarget_workflow( + _BQSKIT_OPT_LEVEL, + _BQSKIT_SYNTHESIS_EPSILON, + _BQSKIT_MAX_SYNTHESIS_SIZE, + ), + RestoreMeasurements(), + ]) + return cast("Circuit", _run_bqskit_workflow(circuit, workflow)) + + return _compile + + +def _bqskit_mapping_factory( + device: Target, + *mapping_passes: BQSKitBasePass, + apply_placement: bool, +) -> Callable[[Circuit], BQSKitMapping]: + """Create a BQSKit mapping callable for layout or routing actions.""" + + def _compile(circuit: Circuit) -> BQSKitMapping: + model = MachineModel( + num_qudits=device.num_qubits, + gate_set=get_bqskit_native_gates(device), + coupling_graph=[(edge[0], edge[1]) for edge in device.build_coupling_map()], + ) + workflow_passes = [*mapping_passes] + if apply_placement: + workflow_passes.append(ApplyPlacement()) + workflow = Workflow([ + SetRandomSeedPass(_BQSKIT_SEED), + UnfoldPass(), + ExtractMeasurements(), + SetModelPass(model), + *workflow_passes, + RestoreMeasurements(), + ]) + compiled_circuit, data = cast("tuple[Circuit, PassData]", _run_bqskit_workflow(circuit, workflow, True)) + return compiled_circuit, tuple(data.initial_mapping), tuple(data.final_mapping) + + return _compile + + +def bqskit_optimization_action() -> Action: + """Returns the BQSKit optimization action.""" + return DeviceDependentAction( + "BQSKitO2", + CompilationOrigin.BQSKIT, + PassType.OPT, + transpile_pass=lambda circuit: bqskit_compile( + circuit, + optimization_level=_BQSKIT_OPT_LEVEL, + synthesis_epsilon=_BQSKIT_SYNTHESIS_EPSILON, + max_synthesis_size=_BQSKIT_MAX_SYNTHESIS_SIZE, + seed=_BQSKIT_SEED, + num_workers=_BQSKIT_NUM_WORKERS, + ), + preserves_layout=True, + preserves_routing=True, + preserves_synthesis=False, + ) + + +def bqskit_optimization_actions() -> list[Action]: + """Returns the BQSKit optimization actions.""" + return [bqskit_optimization_action()] + + +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=[(edge[0], edge[1]) for edge in device.build_coupling_map()], + ), + with_mapping=True, + optimization_level=_BQSKIT_OPT_LEVEL, + synthesis_epsilon=_BQSKIT_SYNTHESIS_EPSILON, + max_synthesis_size=_BQSKIT_MAX_SYNTHESIS_SIZE, + seed=_BQSKIT_SEED, + num_workers=_BQSKIT_NUM_WORKERS, + ) + ), + ) + + +def bqskit_mapping_actions() -> list[Action]: + """Returns the BQSKit mapping actions.""" + return [bqskit_mapping_action()] + + +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)), + optimization_level=_BQSKIT_OPT_LEVEL, + synthesis_epsilon=_BQSKIT_SYNTHESIS_EPSILON, + max_synthesis_size=_BQSKIT_MAX_SYNTHESIS_SIZE, + seed=_BQSKIT_SEED, + num_workers=_BQSKIT_NUM_WORKERS, + ) + ), + ) + + +def bqskit_synthesis_actions() -> list[Action]: + """Returns the BQSKit synthesis actions.""" + return [ + bqskit_synthesis_action(), + DeviceDependentAction( + "QSearchSynthesisPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory( + device, + QSearchSynthesisPass( + success_threshold=_BQSKIT_SYNTHESIS_EPSILON, + instantiate_options=get_instantiate_options(_BQSKIT_OPT_LEVEL), + ), + ), + ), + DeviceDependentAction( + "LEAPSynthesisPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory( + device, + LEAPSynthesisPass( + success_threshold=_BQSKIT_SYNTHESIS_EPSILON, + min_prefix_size=[3, 4][min(_BQSKIT_OPT_LEVEL, 2) - 1], + instantiate_options=get_instantiate_options(_BQSKIT_OPT_LEVEL), + ), + ), + ), + DeviceDependentAction( + "WalshDiagonalSynthesisPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory( + device, + IfThenElsePass(_DiagonalUnitaryPredicate(), WalshDiagonalSynthesisPass()), + ), + ), + DeviceDependentAction( + "QSDPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, QSDPass()), + ), + DeviceDependentAction( + "MGDPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, MGDPass()), + ), + DeviceDependentAction( + "BlockZXZPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, BlockZXZPass()), + ), + DeviceDependentAction( + "FullBlockZXZPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, FullBlockZXZPass()), + ), + ] + + +def bqskit_layout_actions() -> list[Action]: + """Returns the BQSKit layout actions.""" + return [ + DeviceDependentAction( + "GreedyPlacementPass", + CompilationOrigin.BQSKIT, + PassType.LAYOUT, + transpile_pass=lambda device: _bqskit_mapping_factory(device, GreedyPlacementPass(), apply_placement=True), + ), + DeviceDependentAction( + "TrivialPlacementPass", + CompilationOrigin.BQSKIT, + PassType.LAYOUT, + transpile_pass=lambda device: _bqskit_mapping_factory(device, TrivialPlacementPass(), apply_placement=True), + ), + DeviceDependentAction( + "StaticPlacementPass", + CompilationOrigin.BQSKIT, + PassType.LAYOUT, + transpile_pass=lambda device: _bqskit_mapping_factory(device, StaticPlacementPass(), apply_placement=True), + ), + DeviceDependentAction( + "GeneralizedSabreLayoutPass", + CompilationOrigin.BQSKIT, + PassType.LAYOUT, + transpile_pass=lambda device: _bqskit_mapping_factory( + device, + GreedyPlacementPass(), + GeneralizedSabreLayoutPass(), + apply_placement=True, + ), + ), + ] + + +def bqskit_routing_action() -> Action: + """Returns the BQSKit routing action.""" + return DeviceDependentAction( + "GeneralizedSabreRoutingPass", + CompilationOrigin.BQSKIT, + PassType.ROUTING, + transpile_pass=lambda device: _bqskit_mapping_factory( + device, GeneralizedSabreRoutingPass(), apply_placement=False + ), + ) + + +def bqskit_routing_actions() -> list[Action]: + """Returns the BQSKit routing actions.""" + return [bqskit_routing_action()] + + def final_layout_bqskit_to_qiskit( bqskit_initial_layout: tuple[int, ...], bqskit_final_layout: tuple[int, ...], @@ -240,7 +497,7 @@ def final_layout_bqskit_to_qiskit( 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 = {} + qiskit_initial_layout: dict[int, object] = {} counter_ancilla_qubit = 0 for i in range(compiled_qc.num_qubits): if i in bqskit_initial_layout: @@ -249,17 +506,24 @@ def final_layout_bqskit_to_qiskit( 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)} + initial_qubit_mapping = {bit: index for index, bit in enumerate(initial_qc.qubits)} + initial_qubit_mapping.update({bit: initial_qc.num_qubits + index for index, bit in enumerate(ancilla)}) - if bqskit_initial_layout == bqskit_final_layout: - qiskit_final_layout = None - else: + qiskit_final_layout: dict[int, object] | None = None + if bqskit_initial_layout != bqskit_final_layout: 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] + used_output_wires: set[int] = set() + for initial_position, final_position in zip(bqskit_initial_layout, bqskit_final_layout, strict=False): + qiskit_final_layout[final_position] = compiled_qc.qubits[initial_position] + used_output_wires.add(initial_position) + + remaining_physical_positions = [i for i in range(compiled_qc.num_qubits) if i not in qiskit_final_layout] + remaining_output_wires = [ + compiled_qc.qubits[i] for i in range(compiled_qc.num_qubits) if i not in used_output_wires + ] + + for physical_position, output_wire in zip(remaining_physical_positions, remaining_output_wires, strict=False): + qiskit_final_layout[physical_position] = output_wire return TranspileLayout( initial_layout=Layout(input_dict=qiskit_initial_layout), @@ -270,33 +534,102 @@ def final_layout_bqskit_to_qiskit( ) +def final_layout_bqskit_routing_to_qiskit( + bqskit_final_layout: tuple[int, ...], + output_qubits: list[QiskitQubit], +) -> Layout | None: + """Convert a BQSKit routing permutation into a Qiskit final layout.""" + if bqskit_final_layout == tuple(range(len(bqskit_final_layout))): + return None + + qiskit_final_layout: dict[int, QiskitQubit] = {} + used_output_positions: set[int] = set() + for input_position, final_position in enumerate(bqskit_final_layout): + qiskit_final_layout[final_position] = output_qubits[input_position] + used_output_positions.add(input_position) + + remaining_physical_positions = [i for i in range(len(output_qubits)) if i not in qiskit_final_layout] + remaining_output_positions = [i for i in range(len(output_qubits)) if i not in used_output_positions] + + for physical_position, output_position in zip( + remaining_physical_positions, remaining_output_positions, strict=True + ): + qiskit_final_layout[physical_position] = output_qubits[output_position] + + return Layout(input_dict=qiskit_final_layout) + + 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.""" + """Apply a BQSKit action and update the layout bookkeeping it owns. + + Args: + action: The BQSKit action to apply. + circuit: The current quantum circuit. + device: The target device. + layout: The current layout (if any). + + Returns: + Tuple of (compiled circuit, updated layout). + """ bqskit_qc = qiskit_to_bqskit(circuit) + + # OPT actions don't take a device parameter 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: + compiled_qc = transpile(bqskit_qc) + return bqskit_to_qiskit(compiled_qc), layout + + # SYNTHESIS actions use device factory + if 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) + compiled_qc = factory(device)(bqskit_qc) + return bqskit_to_qiskit(compiled_qc), layout + + # LAYOUT and MAPPING actions establish layout + if action.pass_type in (PassType.LAYOUT, PassType.MAPPING): + factory = cast("Callable[[Target], Callable[[Circuit], BQSKitMapping]]", action.transpile_pass) + compiled_qc, initial, final = factory(device)(bqskit_qc) + compiled_qiskit_qc = bqskit_to_qiskit(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 + # ROUTING actions require existing layout + if action.pass_type == PassType.ROUTING: + assert layout is not None, "BQSKit routing requires an existing layout." + factory = cast("Callable[[Target], Callable[[Circuit], BQSKitMapping]]", action.transpile_pass) + compiled_qc, _initial, final = factory(device)(bqskit_qc) + compiled_qiskit_qc = bqskit_to_qiskit(compiled_qc) + output_qubits = layout._output_qubit_list # noqa: SLF001 + assert output_qubits is not None + layout.final_layout = final_layout_bqskit_routing_to_qiskit(final, list(output_qubits)) + return compiled_qiskit_qc, layout + + msg = f"Unhandled BQSKit action pass type: {action.pass_type}" + raise ValueError(msg) + + +__all__ = [ + "bqskit_layout_actions", + "bqskit_mapping_action", + "bqskit_mapping_actions", + "bqskit_optimization_action", + "bqskit_optimization_actions", + "bqskit_routing_action", + "bqskit_routing_actions", + "bqskit_synthesis_action", + "bqskit_synthesis_actions", + "bqskit_to_qiskit", + "final_layout_bqskit_routing_to_qiskit", + "final_layout_bqskit_to_qiskit", + "get_bqskit_native_gates", + "is_bqskit_action_available", + "run_bqskit_action", +] def is_bqskit_action_available(*, has_parameterized_gates: bool) -> bool: From 37bfd52e9a6220d71ed003ca7420e2aa922c5e6a Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 14 Jul 2026 14:34:07 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=9A=A7=20refactor=20bqskit=20actions?= 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/bqskit_actions.py | 153 +++++++----------- 2 files changed, 56 insertions(+), 100 deletions(-) diff --git a/src/mqt/predictor/rl/actions/__init__.py b/src/mqt/predictor/rl/actions/__init__.py index 03ca03b14..babfd67b7 100644 --- a/src/mqt/predictor/rl/actions/__init__.py +++ b/src/mqt/predictor/rl/actions/__init__.py @@ -121,14 +121,13 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]: *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(), *bqskit_actions.bqskit_layout_actions(), tket_actions.tket_routing_action(), bqskit_actions.bqskit_routing_action(), qiskit_actions.qiskit_mapping_action(), - bqskit_actions.bqskit_mapping_action(), + bqskit_actions.bqskit_pam_mapping_action(), qiskit_actions.qiskit_synthesis_action(), *bqskit_actions.bqskit_synthesis_actions(), DeviceIndependentAction( diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index d2b7f59cf..0a813d883 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -17,11 +17,11 @@ import numpy as np from bqskit import MachineModel -from bqskit import compile as bqskit_compile from bqskit.compiler import Compiler, Workflow from bqskit.compiler.compile import ( build_multi_qudit_retarget_workflow, build_partitioning_workflow, + build_seqpam_mapping_optimization_workflow, build_single_qudit_retarget_workflow, get_instantiate_options, ) @@ -33,14 +33,13 @@ BlockZXZPass, ExtractMeasurements, FullBlockZXZPass, + FullQSDPass, GeneralizedSabreLayoutPass, GeneralizedSabreRoutingPass, GreedyPlacementPass, IfThenElsePass, LEAPSynthesisPass, - MGDPass, PassPredicate, - QSDPass, QSearchSynthesisPass, RestoreMeasurements, SetModelPass, @@ -76,7 +75,7 @@ _BQSKIT_OPT_LEVEL = 1 if os.getenv("GITHUB_ACTIONS") == "true" else 2 _BQSKIT_SYNTHESIS_EPSILON = 1e-1 if os.getenv("GITHUB_ACTIONS") == "true" else 1e-8 -_BQSKIT_MAX_SYNTHESIS_SIZE = 3 +_BQSKIT_BLOCK_SIZE = 4 _BQSKIT_SEED = 10 _BQSKIT_NUM_WORKERS = 1 if os.getenv("GITHUB_ACTIONS") == "true" else -1 @@ -119,7 +118,7 @@ def _run_bqskit_workflow( circuit: Circuit, workflow: Workflow, request_data: bool = False ) -> Circuit | tuple[Circuit, PassData]: """Compile ``circuit`` with a custom BQSKit workflow.""" - compiler = Compiler() + compiler = Compiler(num_workers=_BQSKIT_NUM_WORKERS) try: result = compiler.compile(circuit, workflow, request_data=request_data) finally: @@ -232,18 +231,18 @@ def _compile(circuit: Circuit) -> Circuit: SetModelPass(model), build_partitioning_workflow( synthesis_pass, - _BQSKIT_MAX_SYNTHESIS_SIZE, + block_size=_BQSKIT_BLOCK_SIZE, replace_filter_method="less-than-respecting-fully", ), build_multi_qudit_retarget_workflow( _BQSKIT_OPT_LEVEL, _BQSKIT_SYNTHESIS_EPSILON, - _BQSKIT_MAX_SYNTHESIS_SIZE, + max_synthesis_size=_BQSKIT_BLOCK_SIZE, ), build_single_qudit_retarget_workflow( _BQSKIT_OPT_LEVEL, _BQSKIT_SYNTHESIS_EPSILON, - _BQSKIT_MAX_SYNTHESIS_SIZE, + max_synthesis_size=_BQSKIT_BLOCK_SIZE, ), RestoreMeasurements(), ]) @@ -282,85 +281,9 @@ def _compile(circuit: Circuit) -> BQSKitMapping: return _compile -def bqskit_optimization_action() -> Action: - """Returns the BQSKit optimization action.""" - return DeviceDependentAction( - "BQSKitO2", - CompilationOrigin.BQSKIT, - PassType.OPT, - transpile_pass=lambda circuit: bqskit_compile( - circuit, - optimization_level=_BQSKIT_OPT_LEVEL, - synthesis_epsilon=_BQSKIT_SYNTHESIS_EPSILON, - max_synthesis_size=_BQSKIT_MAX_SYNTHESIS_SIZE, - seed=_BQSKIT_SEED, - num_workers=_BQSKIT_NUM_WORKERS, - ), - preserves_layout=True, - preserves_routing=True, - preserves_synthesis=False, - ) - - -def bqskit_optimization_actions() -> list[Action]: - """Returns the BQSKit optimization actions.""" - return [bqskit_optimization_action()] - - -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=[(edge[0], edge[1]) for edge in device.build_coupling_map()], - ), - with_mapping=True, - optimization_level=_BQSKIT_OPT_LEVEL, - synthesis_epsilon=_BQSKIT_SYNTHESIS_EPSILON, - max_synthesis_size=_BQSKIT_MAX_SYNTHESIS_SIZE, - seed=_BQSKIT_SEED, - num_workers=_BQSKIT_NUM_WORKERS, - ) - ), - ) - - -def bqskit_mapping_actions() -> list[Action]: - """Returns the BQSKit mapping actions.""" - return [bqskit_mapping_action()] - - -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)), - optimization_level=_BQSKIT_OPT_LEVEL, - synthesis_epsilon=_BQSKIT_SYNTHESIS_EPSILON, - max_synthesis_size=_BQSKIT_MAX_SYNTHESIS_SIZE, - seed=_BQSKIT_SEED, - num_workers=_BQSKIT_NUM_WORKERS, - ) - ), - ) - - def bqskit_synthesis_actions() -> list[Action]: """Returns the BQSKit synthesis actions.""" return [ - bqskit_synthesis_action(), DeviceDependentAction( "QSearchSynthesisPass", CompilationOrigin.BQSKIT, @@ -396,22 +319,22 @@ def bqskit_synthesis_actions() -> list[Action]: ), ), DeviceDependentAction( - "QSDPass", - CompilationOrigin.BQSKIT, - PassType.SYNTHESIS, - transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, QSDPass()), - ), - DeviceDependentAction( - "MGDPass", + "FullQSDPass", CompilationOrigin.BQSKIT, PassType.SYNTHESIS, - transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, MGDPass()), + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory( + device, + FullQSDPass(min_qudit_size=2, perform_scan=False), + ), ), DeviceDependentAction( "BlockZXZPass", CompilationOrigin.BQSKIT, PassType.SYNTHESIS, - transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, BlockZXZPass()), + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory( + device, + BlockZXZPass(min_qudit_size=_BQSKIT_BLOCK_SIZE - 1), + ), ), DeviceDependentAction( "FullBlockZXZPass", @@ -422,6 +345,44 @@ def bqskit_synthesis_actions() -> list[Action]: ] +def bqskit_pam_mapping_action() -> Action: + """Returns the BQSKit sequential permutation-aware mapping action.""" + + def _factory(device: Target) -> Callable[[Circuit], BQSKitMapping]: + def _compile(circuit: Circuit) -> BQSKitMapping: + model = MachineModel( + num_qudits=device.num_qubits, + gate_set=get_bqskit_native_gates(device), + coupling_graph=[(edge[0], edge[1]) for edge in device.build_coupling_map()], + ) + workflow = Workflow([ + SetRandomSeedPass(_BQSKIT_SEED), + UnfoldPass(), + ExtractMeasurements(), + SetModelPass(model), + build_seqpam_mapping_optimization_workflow( + _BQSKIT_OPT_LEVEL, + _BQSKIT_SYNTHESIS_EPSILON, + block_size=_BQSKIT_BLOCK_SIZE, + ), + RestoreMeasurements(), + ]) + compiled_circuit, data = cast( + "tuple[Circuit, PassData]", + _run_bqskit_workflow(circuit, workflow, True), + ) + return compiled_circuit, tuple(data.initial_mapping), tuple(data.final_mapping) + + return _compile + + return DeviceDependentAction( + "SeqPAMMapping", + CompilationOrigin.BQSKIT, + PassType.MAPPING, + transpile_pass=_factory, + ) + + def bqskit_layout_actions() -> list[Action]: """Returns the BQSKit layout actions.""" return [ @@ -615,13 +576,9 @@ def run_bqskit_action( __all__ = [ "bqskit_layout_actions", - "bqskit_mapping_action", - "bqskit_mapping_actions", - "bqskit_optimization_action", - "bqskit_optimization_actions", + "bqskit_pam_mapping_action", "bqskit_routing_action", "bqskit_routing_actions", - "bqskit_synthesis_action", "bqskit_synthesis_actions", "bqskit_to_qiskit", "final_layout_bqskit_routing_to_qiskit", From 2a6d51e5c45642476874c0c5374c272dee3abe54 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 14 Jul 2026 15:03:53 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=85=20add=20mapping=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../predictor/rl/actions/bqskit_actions.py | 6 ++++- .../test_integration_further_SDKs.py | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index 0a813d883..548be2a3c 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -39,6 +39,7 @@ GreedyPlacementPass, IfThenElsePass, LEAPSynthesisPass, + ManyQuditGatesPredicate, PassPredicate, QSearchSynthesisPass, RestoreMeasurements, @@ -324,7 +325,10 @@ def bqskit_synthesis_actions() -> list[Action]: PassType.SYNTHESIS, transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory( device, - FullQSDPass(min_qudit_size=2, perform_scan=False), + IfThenElsePass( + ManyQuditGatesPredicate(check_circuit=True, check_model=False), + FullQSDPass(min_qudit_size=2, perform_scan=False), + ), ), ), DeviceDependentAction( diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 713d24f03..d6dff5450 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -179,6 +179,31 @@ def test_layout_actions_establish_layout( ) +def test_mapping_actions_establish_layout( + simple_circuit: QuantumCircuit, + env: PredictorEnv, +) -> None: + """Invariant: every mapping action establishes a valid qubit assignment.""" + synthesis_pm = PassManager([BasisTranslator(StandardEquivalenceLibrary, target_basis=env.device.operation_names)]) + synthesized = synthesis_pm.run(simple_circuit.copy()) + n_qubits = synthesized.num_qubits + + for idx, action in env.action_set.items(): + if action.pass_type != PassType.MAPPING: + continue + _setup_env(env, synthesized, 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( simple_circuit: QuantumCircuit, env: PredictorEnv, From af0977c5677b3e0c068982effb7b7f7343ace801 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Tue, 14 Jul 2026 15:04:21 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20add=20upgrading=20informatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UPGRADING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index 17eabced7..361f1d388 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,9 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +The composite BQSKit actions `BQSKitO2`, `BQSKitSynthesis`, and `BQSKitMapping` +have been removed. + ## [2.4.0] ### Trained RL model names From a2dbe0874275689d337baac35da0d8825a251ae1 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 15 Jul 2026 16:57:27 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20bump=20bqskit=20versio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dec16e75a..e6bb2bc9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ "rich>=12.6.0", "scikit-learn>=1.5.1", "tensorboard>=2.17.0", - "bqskit>=1.2.0", + "bqskit>=1.2.1", "numpy>=2.1; python_version >= '3.13'", "numpy>=1.26; python_version >= '3.12'", "numpy>=1.24; python_version >= '3.11'", diff --git a/uv.lock b/uv.lock index a81f2672b..4a8e4c772 100644 --- a/uv.lock +++ b/uv.lock @@ -1529,7 +1529,7 @@ test = [ [package.metadata] requires-dist = [ - { name = "bqskit", specifier = ">=1.2.0" }, + { name = "bqskit", specifier = ">=1.2.1" }, { name = "mqt-bench", specifier = ">=2.0.0" }, { name = "numpy", specifier = ">=1.22" }, { name = "numpy", marker = "python_full_version >= '3.11'", specifier = ">=1.24" },