Skip to content
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ releases may include breaking changes.

### Added

- ✨ Add Qiskit's `TrivialLayout`, `ElidePermutations`, `SabreSwap`,
`BasicSwap`, `LookaheadSwap`, `RemoveIdentityEquivalent`, and
`Optimize1qGatesSimpleCommutation` passes and the optional IBM-backed
`AIRouting` and `AIRouting_opt` passes to the RL actions ([#794])
([**@flowerthrower**])
- ✨ Expand and compact the RL observation with normalized OpenQASM operation
frequencies and one-element `float32` qubit-count and depth arrays, and
include measurements in the shared ML feature schema ([#758])
Expand All @@ -23,6 +28,8 @@ releases may include breaking changes.

### Changed

- 🐛 Make the `OptimizeCliffords` RL action collect standard Clifford gates
before optimizing them ([#794]) ([**@flowerthrower**])
- 🔥 Drop support for Python 3.10 ([#773]) ([**@denialhaag**])
- ♻️ Split RL actions package into `base` and `registry` modules ([#769])
([**@denialhaag**])
Expand Down Expand Up @@ -93,6 +100,7 @@ for previous changelogs._

[#773]: https://github.com/munich-quantum-toolkit/predictor/pull/771
[#769]: https://github.com/munich-quantum-toolkit/predictor/pull/769
[#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
[#731]: https://github.com/munich-quantum-toolkit/predictor/pull/731
Expand Down
29 changes: 29 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ of changes including minor and patch releases, please refer to the

## [Unreleased]

### Expanded Qiskit action set

The RL action space now includes the following Qiskit passes:

- the `TrivialLayout` and `ElidePermutations` layout actions;
- the `SabreSwap`, `BasicSwap`, `LookaheadSwap`, and `AIRouting` routing
actions;
- the combined layout-and-routing action `AIRouting_opt`; and
- the `RemoveIdentityEquivalent` and `Optimize1qGatesSimpleCommutation`
optimization actions.

`ElidePermutations` establishes a trivial layout in the same action so its
output permutation remains part of the canonical layout. `OptimizeCliffords` now
collects standard Clifford gates before optimizing and decomposes the result for
subsequent passes.

`AIRouting` and `AIRouting_opt` are masked when IBM's optional
`qiskit-ibm-transpiler` package cannot be imported. MQT Predictor does not
install that package because its current release pins NetworkX 2.8.5 while MQT
Bench requires NetworkX 2.8.8 or newer, excludes Python 3.14, and imports Qiskit
internals removed in Qiskit 2.5. Consequently, there is currently no supported
MQT Predictor installation that enables these actions. A future compatible IBM
release can be loaded without changing the action schema. Its routing model is
downloaded on first use.

Existing RL models must be retrained because the action-space size and the
indices of later actions have changed. Code that persists or selects actions by
numeric index must be updated.

### RL observation features

The RL observation now includes normalized frequencies for supported OpenQASM
Expand Down
190 changes: 186 additions & 4 deletions src/mqt/predictor/rl/actions/qiskit_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, cast
from functools import cache
from importlib import import_module
from typing import TYPE_CHECKING, Any, cast

from qiskit.circuit import StandardEquivalenceLibrary
from qiskit.circuit.library import (
Expand All @@ -37,24 +39,33 @@
from qiskit.transpiler import CouplingMap, PassManager, TranspileLayout
from qiskit.transpiler.passes import (
ApplyLayout,
BasicSwap,
BasisTranslator,
Collect2qBlocks,
CollectCliffords,
CommutativeCancellation,
CommutativeInverseCancellation,
ConsolidateBlocks,
Decompose,
DenseLayout,
Depth,
ElidePermutations,
EnlargeWithAncilla,
FixedPoint,
FullAncillaAllocation,
GatesInBasis,
InverseCancellation,
LookaheadSwap,
MinimumPoint,
Optimize1qGatesDecomposition,
Optimize1qGatesSimpleCommutation,
OptimizeCliffords,
RemoveDiagonalGatesBeforeMeasure,
RemoveIdentityEquivalent,
SabreLayout,
SabreSwap,
Size,
TrivialLayout,
UnitarySynthesis,
VF2Layout,
VF2PostLayout,
Expand All @@ -81,6 +92,39 @@

logger = logging.getLogger("mqt-predictor")

_AI_ROUTING_ACTION_NAMES = frozenset({"AIRouting", "AIRouting_opt"})


@cache
def _load_airouting() -> type[Any]:
"""Load IBM's optional AI routing pass."""
try:
module = import_module("qiskit_ibm_transpiler.ai.routing")
except ImportError as exc:
msg = "AIRouting requires a qiskit-ibm-transpiler installation compatible with this environment."
raise RuntimeError(msg) from exc
return cast("type[Any]", vars(module)["AIRouting"])


def _airouting_pass(*, coupling_map: CouplingMap, layout_mode: str) -> Task:
"""Construct IBM's local AI routing pass."""
return _load_airouting()(
coupling_map=coupling_map,
optimization_level=3,
layout_mode=layout_mode,
local_mode=True,
)


@cache
def _is_ai_routing_available() -> bool:
"""Return whether IBM's AI routing pass can be imported."""
try:
_load_airouting()
except RuntimeError:
return False
return True


def qiskit_optimization_actions() -> list[Action]:
"""Returns the Qiskit optimization actions."""
Expand Down Expand Up @@ -149,7 +193,11 @@ def qiskit_optimization_actions() -> list[Action]:
"OptimizeCliffords",
CompilationOrigin.QISKIT,
PassType.OPT,
[OptimizeCliffords()],
[
CollectCliffords(),
OptimizeCliffords(),
Decompose(gates_to_decompose="clifford", apply_synthesis=True),
],
preserves_layout=True,
preserves_routing=False,
preserves_synthesis=False,
Expand All @@ -163,6 +211,32 @@ def qiskit_optimization_actions() -> list[Action]:
preserves_routing=True,
preserves_synthesis=False,
),
DeviceIndependentAction(
"RemoveIdentityEquivalent",
CompilationOrigin.QISKIT,
PassType.OPT,
[RemoveIdentityEquivalent()],
preserves_layout=True,
preserves_routing=True,
preserves_synthesis=True,
),
DeferredDeviceAction(
"Optimize1qGatesSimpleCommutation",
CompilationOrigin.QISKIT,
PassType.OPT,
transpile_pass=lambda device: cast(
"list[Task]",
[
Optimize1qGatesSimpleCommutation(
basis=device.operation_names,
run_to_completion=True,
)
],
),
preserves_layout=True,
preserves_routing=True,
preserves_synthesis=True,
),
]


Expand Down Expand Up @@ -249,9 +323,93 @@ def qiskit_layout_actions() -> list[Action]:
],
),
),
DeferredDeviceAction(
"TrivialLayout",
CompilationOrigin.QISKIT,
PassType.LAYOUT,
transpile_pass=lambda device: cast(
"list[Task]",
[
TrivialLayout(coupling_map=CouplingMap(device.build_coupling_map())),
FullAncillaAllocation(coupling_map=CouplingMap(device.build_coupling_map())),
EnlargeWithAncilla(),
ApplyLayout(),
],
),
),
DeferredDeviceAction(
"ElidePermutations",
CompilationOrigin.QISKIT,
PassType.LAYOUT,
transpile_pass=lambda device: cast(
"list[Task]",
[
ElidePermutations(),
TrivialLayout(coupling_map=CouplingMap(device.build_coupling_map())),
FullAncillaAllocation(coupling_map=CouplingMap(device.build_coupling_map())),
EnlargeWithAncilla(),
ApplyLayout(),
],
),
),
]
Comment on lines +340 to +355

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a strange addition. The Elision pass is a regular pass, not a Layout per-se. I see no reason why it should be coupled to a trivial layout.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the thing I was trying to workaround here is that elidepermuations alone do produce a virtual_permutation_layout, which would be lost after the pass if not directly followed by a layout. The cleaner solution would probably be to track that along in our layout bookkeeping, but i have to investigate first what that would do in a cross-compiler scenario (i.e., when interleaving with TKET for example).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Then I'd rather defer the addition of this to a later PR. I do not believe the current elide+trivial-layout pass provides any meaningful value.



def qiskit_routing_actions() -> list[Action]:
"""Return the Qiskit routing actions."""
return [
DeferredDeviceAction(
"SabreSwap",
CompilationOrigin.QISKIT,
PassType.ROUTING,
transpile_pass=lambda device: cast(
"list[Task]", [SabreSwap(coupling_map=CouplingMap(device.build_coupling_map()), heuristic="decay")]
),
),
DeferredDeviceAction(
"BasicSwap",
CompilationOrigin.QISKIT,
PassType.ROUTING,
transpile_pass=lambda device: cast(
"list[Task]", [BasicSwap(coupling_map=CouplingMap(device.build_coupling_map()))]
),
),
DeferredDeviceAction(
"LookaheadSwap",
CompilationOrigin.QISKIT,
PassType.ROUTING,
transpile_pass=lambda device: cast(
"list[Task]",
[
LookaheadSwap(
coupling_map=CouplingMap(device.build_coupling_map()),
search_depth=1,
search_width=1,
)
],
),
),
Comment on lines +358 to +391

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything that is not SABRE here is a bit pointless to add because these outer routing methods aren't really developed or improved anymore and there is hardly ever any reason to choose them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i see. my naive thought was "cool more passes -> more room for experimantation for the agent", but you are right they seem hardly useful compared to SABRE

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually argue that more passes can be quite counter-productive if the passes being added do not really add value but just enlarge the search space.

]


def qiskit_ai_routing_action() -> Action:
"""Return IBM's AI routing action."""
return DeferredDeviceAction(
"AIRouting",
CompilationOrigin.QISKIT,
PassType.ROUTING,
transpile_pass=lambda device: cast(
"list[Task]",
[
_airouting_pass(
coupling_map=device.build_coupling_map(),
layout_mode="improve",
)
],
),
)


def qiskit_mapping_action() -> Action:
"""Returns the Qiskit mapping action."""
return DeferredDeviceAction(
Expand All @@ -264,6 +422,24 @@ def qiskit_mapping_action() -> Action:
)


def qiskit_ai_mapping_action() -> Action:
"""Return the combined AI layout and routing action."""
return DeferredDeviceAction(
"AIRouting_opt",
CompilationOrigin.QISKIT,
PassType.MAPPING,
transpile_pass=lambda device: cast(
"list[Task]",
[
_airouting_pass(
coupling_map=device.build_coupling_map(),
layout_mode="optimize",
),
],
),
)


def qiskit_synthesis_action() -> Action:
"""Returns the Qiskit synthesis action."""
return DeferredDeviceAction(
Expand Down Expand Up @@ -365,16 +541,22 @@ def run_qiskit_action(
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"]
routing_layout = pm.property_set["final_layout"]
layout.final_layout = (
layout.final_layout.compose(routing_layout, circuit.qubits)
if layout.final_layout is not None
else routing_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


def is_qiskit_action_available(action: Action, device: Target) -> bool:
"""Return whether a Qiskit action is available for the current device."""
if action.name in _AI_ROUTING_ACTION_NAMES and not _is_ai_routing_available():
return False
# Only allow VF2PostLayout if "ibm" is in the device name # TODO: Why?
return action.name != "VF2PostLayout" or "ibm" in device.description
3 changes: 3 additions & 0 deletions src/mqt/predictor/rl/actions/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ def get_actions_by_pass_type() -> dict[PassType, list[Action]]:

for _action in (
*qiskit_actions.qiskit_layout_actions(),
*qiskit_actions.qiskit_routing_actions(),
qiskit_actions.qiskit_ai_routing_action(),
qiskit_actions.qiskit_mapping_action(),
qiskit_actions.qiskit_ai_mapping_action(),
qiskit_actions.qiskit_synthesis_action(),
qiskit_actions.qiskit_o3_action(),
*qiskit_actions.qiskit_optimization_actions(),
Expand Down
Loading
Loading