diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5762d16..e4a70a251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Add BQSKit's `QSDPass` to the RL synthesis actions ([#795]) + ([**@flowerthrower**]) - ✨ Add Qiskit's `TrivialLayout`, `ElidePermutations`, `SabreSwap`, `BasicSwap`, `LookaheadSwap`, `RemoveIdentityEquivalent`, and `Optimize1qGatesSimpleCommutation` passes and the optional IBM-backed @@ -100,6 +102,7 @@ for previous changelogs._ [#773]: https://github.com/munich-quantum-toolkit/predictor/pull/771 [#769]: https://github.com/munich-quantum-toolkit/predictor/pull/769 +[#795]: https://github.com/munich-quantum-toolkit/predictor/pull/795 [#794]: https://github.com/munich-quantum-toolkit/predictor/pull/794 [#758]: https://github.com/munich-quantum-toolkit/predictor/pull/758 [#755]: https://github.com/munich-quantum-toolkit/predictor/pull/755 diff --git a/UPGRADING.md b/UPGRADING.md index 1965cc466..5a147ea78 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -67,7 +67,8 @@ The new actions include: - the `GeneralizedSabreRoutingPass` routing action; - the `BQSKitSABREMapping` mapping action; and - the `QSearchSynthesisPass`, `LEAPSynthesisPass`, `WalshDiagonalSynthesisPass`, - `FullQSDPass`, `BlockZXZPass`, and `FullBlockZXZPass` synthesis actions. + `QSDPass`, `FullQSDPass`, `BlockZXZPass`, and `FullBlockZXZPass` synthesis + actions. See the [framework setup](docs/setup.md#step-2-train-reinforcement-learning-models) for diff --git a/src/mqt/predictor/rl/actions/bqskit_actions.py b/src/mqt/predictor/rl/actions/bqskit_actions.py index 0d78632cc..ea2bbc41c 100644 --- a/src/mqt/predictor/rl/actions/bqskit_actions.py +++ b/src/mqt/predictor/rl/actions/bqskit_actions.py @@ -15,7 +15,7 @@ from functools import cache from typing import TYPE_CHECKING, TypeAlias, cast -from bqskit import MachineModel +from bqskit import Circuit, MachineModel from bqskit.compiler import Compiler, Workflow from bqskit.compiler.compile import ( build_multi_qudit_retarget_workflow, @@ -39,11 +39,13 @@ IfThenElsePass, LEAPSynthesisPass, ManyQuditGatesPredicate, + QSDPass, QSearchSynthesisPass, RestoreMeasurements, SetModelPass, SetRandomSeedPass, StaticPlacementPass, + SynthesisPass, TrivialPlacementPass, UnfoldPass, WalshDiagonalSynthesisPass, @@ -59,11 +61,11 @@ if TYPE_CHECKING: from collections.abc import Callable - from bqskit import Circuit from bqskit.compiler.basepass import BasePass from bqskit.compiler.passdata import PassData from bqskit.compiler.workflow import WorkflowLike from bqskit.ir import Gate + from bqskit.qis import StateSystem, StateVector, UnitaryMatrix from qiskit import QuantumCircuit from qiskit.circuit import Qubit as QiskitQubit from qiskit.transpiler import Target @@ -81,6 +83,22 @@ _BQSKIT_NUM_WORKERS = 1 if os.getenv("GITHUB_ACTIONS") == "true" else -1 +class _QSDUnitarySynthesisPass(SynthesisPass): + """Apply one QSD level to each multi-qubit synthesis target.""" + + async def synthesize( + self, + target: UnitaryMatrix | StateVector | StateSystem, + data: PassData, + ) -> Circuit: + """Synthesize a partition target with one QSD level.""" + del data + unitary = cast("UnitaryMatrix", target) + if unitary.num_qudits == 1: + return Circuit.from_unitary(unitary) + return QSDPass.qsd(unitary) + + def _r_gate(theta: float, phi: float) -> Instruction: """Construct an RGate with the given parameters.""" return RGate(theta, phi) @@ -309,6 +327,12 @@ def bqskit_synthesis_actions() -> list[Action]: IfThenElsePass(DiagonalPredicate(1e-9), WalshDiagonalSynthesisPass()), ), ), + DeferredDeviceAction( + "QSDPass", + CompilationOrigin.BQSKIT, + PassType.SYNTHESIS, + transpile_pass=lambda device: _bqskit_partitioned_synthesis_factory(device, _QSDUnitarySynthesisPass()), + ), DeferredDeviceAction( "FullQSDPass", CompilationOrigin.BQSKIT, diff --git a/tests/compilation/test_integration_further_SDKs.py b/tests/compilation/test_integration_further_SDKs.py index 017794e7b..e29fdc276 100644 --- a/tests/compilation/test_integration_further_SDKs.py +++ b/tests/compilation/test_integration_further_SDKs.py @@ -10,9 +10,12 @@ from __future__ import annotations +import asyncio from typing import TYPE_CHECKING import pytest +from bqskit.compiler.passdata import PassData +from bqskit.ir.gates import MPRYGate, MPRZGate, VariableUnitaryGate from mqt.bench.targets import get_device from qiskit import QuantumCircuit from qiskit.circuit import StandardEquivalenceLibrary @@ -26,7 +29,7 @@ TrivialLayout, ) -from mqt.predictor.rl.actions import CompilationOrigin, PassType +from mqt.predictor.rl.actions import CompilationOrigin, PassType, bqskit_actions from mqt.predictor.rl.predictorenv import PredictorEnv if TYPE_CHECKING: @@ -126,6 +129,22 @@ def env(target: Target) -> PredictorEnv: return PredictorEnv(device=target, reward_function="expected_fidelity") +@pytest.mark.filterwarnings("ignore:__array__ implementation doesn't accept a copy keyword:DeprecationWarning") +def test_qsd_unitary_synthesis_pass_applies_one_qsd_level(simple_circuit: QuantumCircuit) -> None: + """The QSD adapter performs one equivalent decomposition of a reachable partition target.""" + bqskit_circuit = bqskit_actions.qiskit_to_bqskit(simple_circuit) + unitary = bqskit_circuit.get_unitary() + synthesis_pass = bqskit_actions._QSDUnitarySynthesisPass() # ruff: ignore[private-member-access] + + decomposed = asyncio.run(synthesis_pass.synthesize(unitary, PassData(bqskit_circuit))) + + assert decomposed.get_unitary().get_distance_from(unitary) < 1e-7 + assert sum(count for gate, count in decomposed.gate_counts.items() if isinstance(gate, VariableUnitaryGate)) == 4 + assert all(gate.num_qudits == 2 for gate in decomposed.gate_set if isinstance(gate, VariableUnitaryGate)) + assert sum(count for gate, count in decomposed.gate_counts.items() if isinstance(gate, MPRZGate)) == 2 + assert sum(count for gate, count in decomposed.gate_counts.items() if isinstance(gate, MPRYGate)) == 1 + + def test_synthesis_actions_produce_native_gates( simple_circuit: QuantumCircuit, env: PredictorEnv,