From b83b114ae24210219a541f1200487835671b74ab Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 13:23:55 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20RL=20feature=20vec?= =?UTF-8?q?tor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize the OpenQASM operation schema and encode every observation as a compact normalized float32 value. Assisted-by: GPT-5.6 via Codex Signed-off-by: flowerthrower --- src/mqt/predictor/ml/helper.py | 51 +------------------- src/mqt/predictor/rl/helper.py | 36 +++++++++------ src/mqt/predictor/rl/predictorenv.py | 15 ++++-- src/mqt/predictor/rl/tracer.py | 8 ++-- src/mqt/predictor/utils.py | 69 ++++++++++++++++++++++++++++ tests/compilation/test_helper_rl.py | 19 ++++++-- 6 files changed, 123 insertions(+), 75 deletions(-) diff --git a/src/mqt/predictor/ml/helper.py b/src/mqt/predictor/ml/helper.py index d73a7d85c..4d87bcaea 100644 --- a/src/mqt/predictor/ml/helper.py +++ b/src/mqt/predictor/ml/helper.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from mqt.predictor.utils import calc_supermarq_features +from mqt.predictor.utils import calc_supermarq_features, get_openqasm_gates if TYPE_CHECKING: import numpy as np @@ -50,55 +50,6 @@ def get_path_training_circuits_compiled() -> Path: return get_path_training_data() / "training_circuits_compiled" -def get_openqasm_gates() -> list[str]: - """Returns a list of all quantum gates within the openQASM 2.0 standard header.""" - # according to https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/qasm/libs/qelib1.inc - return [ - "u3", - "u2", - "u1", - "cx", - "id", - "u0", - "u", - "p", - "x", - "y", - "z", - "h", - "s", - "sdg", - "t", - "tdg", - "rx", - "ry", - "rz", - "sx", - "sxdg", - "cz", - "cy", - "swap", - "ch", - "ccx", - "cswap", - "crx", - "cry", - "crz", - "cu1", - "cp", - "cu3", - "csx", - "cu", - "rxx", - "rzz", - "rccx", - "rc3x", - "c3x", - "c3sqrtx", - "c4x", - ] - - def dict_to_featurevector(gate_dict: dict[str, int]) -> dict[str, int]: """Calculates and returns the feature vector of a given quantum circuit gate dictionary.""" res_dct = dict.fromkeys(get_openqasm_gates(), 0) diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index f2d8c01e8..12ec23493 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -11,13 +11,14 @@ from __future__ import annotations import logging +from math import log1p from pathlib import Path from typing import TYPE_CHECKING import numpy as np from qiskit import QuantumCircuit -from mqt.predictor.utils import calc_supermarq_features +from mqt.predictor.utils import calc_supermarq_features, get_openqasm_gates_for_rl if TYPE_CHECKING: from numpy.random import Generator @@ -28,6 +29,10 @@ logger = logging.getLogger("mqt-predictor") +MAX_NUM_QUBITS = 127 +MAX_CIRCUIT_DEPTH = 999_999 +OBSERVATION_OPERATIONS = (*get_openqasm_gates_for_rl(), "measure") + 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. @@ -70,22 +75,25 @@ def get_state_sample(max_qubits: int, path_training_circuits: Path, rng: Generat return qc, str(file_list[random_index]) -def create_feature_dict(qc: QuantumCircuit) -> dict[str, int | NDArray[np.float32]]: - """Creates a feature dictionary for a given quantum circuit. - - Arguments: - qc: The quantum circuit for which the feature dictionary is created. - - Returns: - The feature dictionary for the given quantum circuit. - """ - feature_dict: dict[str, int | NDArray[np.float32]] = { - "num_qubits": qc.num_qubits, - "depth": qc.depth(), +def create_feature_dict(qc: QuantumCircuit) -> dict[str, NDArray[np.float32]]: + """Create a normalized feature dictionary for a quantum circuit.""" + operation_counts = dict(qc.count_ops()) + total_operations = sum(value for gate, value in operation_counts.items() if gate != "barrier") + normalized_num_qubits = min(qc.num_qubits, MAX_NUM_QUBITS) / MAX_NUM_QUBITS + normalized_depth = log1p(min(qc.depth(), MAX_CIRCUIT_DEPTH)) / log1p(MAX_CIRCUIT_DEPTH) + feature_dict = { + **{ + operation: np.array( + [operation_counts.get(operation, 0) / total_operations if total_operations else 0.0], + dtype=np.float32, + ) + for operation in OBSERVATION_OPERATIONS + }, + "num_qubits": np.array([normalized_num_qubits], dtype=np.float32), + "depth": np.array([normalized_depth], dtype=np.float32), } supermarq_features = calc_supermarq_features(qc) - # for all dict values, put them in a list each feature_dict["program_communication"] = np.array([supermarq_features.program_communication], dtype=np.float32) feature_dict["critical_depth"] = np.array([supermarq_features.critical_depth], dtype=np.float32) feature_dict["entanglement_ratio"] = np.array([supermarq_features.entanglement_ratio], dtype=np.float32) diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 0e5958e7e..ee60f1649 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -46,7 +46,12 @@ 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 +from mqt.predictor.rl.helper import ( + OBSERVATION_OPERATIONS, + create_feature_dict, + get_path_training_circuits, + get_state_sample, +) from mqt.predictor.rl.tracer import CompilationTracer, FigureOfMeritMetric, FigureOfMeritMetrics logger = logging.getLogger("mqt-predictor") @@ -174,9 +179,13 @@ def __init__( self.has_parameterized_gates = False self.rng = np.random.default_rng(10) + operation_spaces = { + operation: Box(low=0, high=1, shape=(1,), dtype=np.float32) for operation in OBSERVATION_OPERATIONS + } spaces: dict[str, Space] = { - "num_qubits": Discrete(128), - "depth": Discrete(1000000), + "num_qubits": Box(low=0, high=1, shape=(1,), dtype=np.float32), + "depth": Box(low=0, high=1, shape=(1,), dtype=np.float32), + **operation_spaces, "program_communication": Box(low=0, high=1, shape=(1,), dtype=np.float32), "critical_depth": Box(low=0, high=1, shape=(1,), dtype=np.float32), "entanglement_ratio": Box(low=0, high=1, shape=(1,), dtype=np.float32), diff --git a/src/mqt/predictor/rl/tracer.py b/src/mqt/predictor/rl/tracer.py index a9c493491..868899fd6 100644 --- a/src/mqt/predictor/rl/tracer.py +++ b/src/mqt/predictor/rl/tracer.py @@ -209,7 +209,7 @@ def record_step( reward: float, current_qc: QuantumCircuit, figures_of_merit: FigureOfMeritMetrics, - features: dict[str, int | NDArray[np.float32]], + features: dict[str, NDArray[np.float32]], synthesized: bool, laid_out: bool, routed: bool, @@ -337,8 +337,6 @@ def _extract_device_metadata(device: Target) -> DeviceMetadata: ) @staticmethod - def _extract_float(val: int | NDArray[np.float32]) -> float: - """Safely extracts a float from a scalar or a 1D NumPy array to satisfy linter requirements.""" - if isinstance(val, int): - return float(val) + def _extract_float(val: NDArray[np.float32]) -> float: + """Extract a float from a one-element feature array.""" return float(val.item()) diff --git a/src/mqt/predictor/utils.py b/src/mqt/predictor/utils.py index f3433113a..67350b6d0 100644 --- a/src/mqt/predictor/utils.py +++ b/src/mqt/predictor/utils.py @@ -144,3 +144,72 @@ def calc_supermarq_features( parallelism, liveness, ) + + +def get_openqasm_gates() -> list[str]: + """Return the gates from the OpenQASM 2.0 standard header.""" + return [ + "u3", + "u2", + "u1", + "cx", + "id", + "u0", + "u", + "p", + "x", + "y", + "z", + "h", + "s", + "sdg", + "t", + "tdg", + "rx", + "ry", + "rz", + "sx", + "sxdg", + "cz", + "cy", + "swap", + "ch", + "ccx", + "cswap", + "crx", + "cry", + "crz", + "cu1", + "cp", + "cu3", + "csx", + "cu", + "rxx", + "rzz", + "rccx", + "rc3x", + "c3x", + "c3sqrtx", + "c4x", + ] + + +def get_openqasm_gates_for_rl() -> list[str]: + """Return the OpenQASM gates used as normalized RL features.""" + excluded_gates = { + "u3", + "u2", + "u1", + "u0", + "u", + "ccx", + "cswap", + "cu1", + "cu", + "rccx", + "rc3x", + "c3x", + "c3sqrtx", + "c4x", + } + return [gate for gate in get_openqasm_gates() if gate not in excluded_gates] diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index b99ad0fca..ca5e368a1 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -18,7 +18,7 @@ from bqskit.ir.circuit import Circuit 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.transpiler import PassManager from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason @@ -39,10 +39,23 @@ def test_create_feature_dict() -> None: """Test the creation of a feature dictionary.""" - qc = get_benchmark("dj", BenchmarkLevel.ALG, 5) + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + features = create_feature_dict(qc) + for feature in features.values(): - assert isinstance(feature, np.ndarray | int) + assert isinstance(feature, np.ndarray) + assert feature.dtype == np.float32 + + np.testing.assert_allclose(features["h"], [0.25]) + np.testing.assert_allclose(features["cx"], [0.25]) + np.testing.assert_allclose(features["measure"], [0.5]) + np.testing.assert_allclose(features["x"], [0]) + np.testing.assert_allclose(features["num_qubits"], [2 / 127]) + np.testing.assert_allclose(features["depth"], [np.log1p(qc.depth()) / np.log1p(999_999)]) def test_get_path_trained_model() -> None: From 314135857d46e2cb8cd06e45eb80a474e0577318 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 13:24:10 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9D=20Document=20RL=20observation?= =?UTF-8?q?=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex Signed-off-by: flowerthrower --- CHANGELOG.md | 4 ++++ UPGRADING.md | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bddaf7bea..19ef863d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ releases may include breaking changes. ### Added +- ✨ Expand and compact the RL observation with normalized OpenQASM gate and + measurement frequencies and scalar qubit-count and depth values ([#758]) + ([**@flowerthrower**]) - 👷 Enable testing on Python 3.14 ([#488]) ([**@denialhaag**]) - ✨ Add selectable `v2` and `v3` RL MDP strategies, make `v3` the default, and use the selected strategy in compilation traces and model artifact names @@ -89,6 +92,7 @@ for previous changelogs._ [#773]: https://github.com/munich-quantum-toolkit/predictor/pull/771 [#769]: https://github.com/munich-quantum-toolkit/predictor/pull/769 +[#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 [#714]: https://github.com/munich-quantum-toolkit/predictor/pull/714 diff --git a/UPGRADING.md b/UPGRADING.md index 4540e6dbd..4c002ee2d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,16 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### RL observation features + +The RL observation now includes normalized frequencies for supported OpenQASM +gates and measurements. The `num_qubits` and `depth` entries are now one-element +`float32` arrays in `[0, 1]` instead of discrete integers. The qubit count is +linearly scaled and capped at 127; the depth is `log1p`-scaled and capped at +999,999. Existing RL models must be retrained, and code that consumes +`PredictorEnv` observations directly must handle the expanded schema and array +values. + ### End of support for Python 3.10 Starting with this release, MQT Predictor no longer supports Python 3.10. As a From dca72a05252d4955f1c3ead28ae5fb1c997cdef3 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 13:43:20 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=8E=A8=20Include=20all=20OpenQASM=20g?= =?UTF-8?q?ates=20in=20RL=20observations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the complete shared OpenQASM gate list so unsynthesized multi-qubit and generic gates remain visible at the start of training. Assisted-by: GPT-5.6 via Codex Signed-off-by: flowerthrower --- src/mqt/predictor/rl/helper.py | 4 ++-- src/mqt/predictor/utils.py | 21 --------------------- tests/compilation/test_helper_rl.py | 10 ++++++---- 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index 12ec23493..2b2f3b5d1 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -18,7 +18,7 @@ import numpy as np from qiskit import QuantumCircuit -from mqt.predictor.utils import calc_supermarq_features, get_openqasm_gates_for_rl +from mqt.predictor.utils import calc_supermarq_features, get_openqasm_gates if TYPE_CHECKING: from numpy.random import Generator @@ -31,7 +31,7 @@ MAX_NUM_QUBITS = 127 MAX_CIRCUIT_DEPTH = 999_999 -OBSERVATION_OPERATIONS = (*get_openqasm_gates_for_rl(), "measure") +OBSERVATION_OPERATIONS = (*get_openqasm_gates(), "measure") def get_state_sample(max_qubits: int, path_training_circuits: Path, rng: Generator) -> tuple[QuantumCircuit, str]: diff --git a/src/mqt/predictor/utils.py b/src/mqt/predictor/utils.py index 67350b6d0..6310b3d55 100644 --- a/src/mqt/predictor/utils.py +++ b/src/mqt/predictor/utils.py @@ -192,24 +192,3 @@ def get_openqasm_gates() -> list[str]: "c3sqrtx", "c4x", ] - - -def get_openqasm_gates_for_rl() -> list[str]: - """Return the OpenQASM gates used as normalized RL features.""" - excluded_gates = { - "u3", - "u2", - "u1", - "u0", - "u", - "ccx", - "cswap", - "cu1", - "cu", - "rccx", - "rc3x", - "c3x", - "c3sqrtx", - "c4x", - } - return [gate for gate in get_openqasm_gates() if gate not in excluded_gates] diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index ca5e368a1..940ffa704 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -39,9 +39,10 @@ def test_create_feature_dict() -> None: """Test the creation of a feature dictionary.""" - qc = QuantumCircuit(2) + qc = QuantumCircuit(3) qc.h(0) qc.cx(0, 1) + qc.ccx(0, 1, 2) qc.measure_all() features = create_feature_dict(qc) @@ -50,11 +51,12 @@ def test_create_feature_dict() -> None: assert isinstance(feature, np.ndarray) assert feature.dtype == np.float32 - np.testing.assert_allclose(features["h"], [0.25]) - np.testing.assert_allclose(features["cx"], [0.25]) + np.testing.assert_allclose(features["h"], [1 / 6]) + np.testing.assert_allclose(features["cx"], [1 / 6]) + np.testing.assert_allclose(features["ccx"], [1 / 6]) np.testing.assert_allclose(features["measure"], [0.5]) np.testing.assert_allclose(features["x"], [0]) - np.testing.assert_allclose(features["num_qubits"], [2 / 127]) + np.testing.assert_allclose(features["num_qubits"], [3 / 127]) np.testing.assert_allclose(features["depth"], [np.log1p(qc.depth()) / np.log1p(999_999)]) From 77eec93fb9f27cdaa2e9b7ab8d1865e6f0c2541a Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 14:34:50 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=8E=A8=20Derive=20RL=20feature=20boun?= =?UTF-8?q?ds=20from=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: flowerthrower Assisted-by: GPT 5.6 via Codex --- CHANGELOG.md | 6 +++--- UPGRADING.md | 8 ++++---- src/mqt/predictor/rl/helper.py | 7 +++---- src/mqt/predictor/rl/predictorenv.py | 6 +++--- src/mqt/predictor/utils.py | 3 ++- tests/compilation/test_helper_rl.py | 5 +++-- tests/compilation/test_predictor_rl.py | 7 +++++-- tests/compilation/test_tracer.py | 4 ++-- tests/device_selection/test_helper_ml.py | 9 ++++++--- 9 files changed, 31 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19ef863d5..aad794f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,9 @@ releases may include breaking changes. ### Added -- ✨ Expand and compact the RL observation with normalized OpenQASM gate and - measurement frequencies and scalar qubit-count and depth values ([#758]) - ([**@flowerthrower**]) +- ✨ Expand and compact the RL observation with normalized OpenQASM operation + frequencies and scalar qubit-count and depth values, and include measurements + in the shared ML feature schema ([#758]) ([**@flowerthrower**]) - 👷 Enable testing on Python 3.14 ([#488]) ([**@denialhaag**]) - ✨ Add selectable `v2` and `v3` RL MDP strategies, make `v3` the default, and use the selected strategy in compilation traces and model artifact names diff --git a/UPGRADING.md b/UPGRADING.md index 4c002ee2d..fd0caf47f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -11,10 +11,10 @@ of changes including minor and patch releases, please refer to the The RL observation now includes normalized frequencies for supported OpenQASM gates and measurements. The `num_qubits` and `depth` entries are now one-element `float32` arrays in `[0, 1]` instead of discrete integers. The qubit count is -linearly scaled and capped at 127; the depth is `log1p`-scaled and capped at -999,999. Existing RL models must be retrained, and code that consumes -`PredictorEnv` observations directly must handle the expanded schema and array -values. +linearly scaled by the target device's qubit count; the depth is `log1p`-scaled +and capped at 999,999. Existing RL models must be retrained, and code that +consumes `PredictorEnv` observations directly must handle the expanded schema +and array values. ### End of support for Python 3.10 diff --git a/src/mqt/predictor/rl/helper.py b/src/mqt/predictor/rl/helper.py index 2b2f3b5d1..25812b453 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -29,9 +29,8 @@ logger = logging.getLogger("mqt-predictor") -MAX_NUM_QUBITS = 127 MAX_CIRCUIT_DEPTH = 999_999 -OBSERVATION_OPERATIONS = (*get_openqasm_gates(), "measure") +OBSERVATION_OPERATIONS = tuple(get_openqasm_gates()) def get_state_sample(max_qubits: int, path_training_circuits: Path, rng: Generator) -> tuple[QuantumCircuit, str]: @@ -75,11 +74,11 @@ def get_state_sample(max_qubits: int, path_training_circuits: Path, rng: Generat return qc, str(file_list[random_index]) -def create_feature_dict(qc: QuantumCircuit) -> dict[str, NDArray[np.float32]]: +def create_feature_dict(qc: QuantumCircuit, max_num_qubits: int) -> dict[str, NDArray[np.float32]]: """Create a normalized feature dictionary for a quantum circuit.""" operation_counts = dict(qc.count_ops()) total_operations = sum(value for gate, value in operation_counts.items() if gate != "barrier") - normalized_num_qubits = min(qc.num_qubits, MAX_NUM_QUBITS) / MAX_NUM_QUBITS + normalized_num_qubits = min(qc.num_qubits, max_num_qubits) / max_num_qubits normalized_depth = log1p(min(qc.depth(), MAX_CIRCUIT_DEPTH)) / log1p(MAX_CIRCUIT_DEPTH) feature_dict = { **{ diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index ee60f1649..c433ccaa7 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -297,7 +297,7 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any action_duration = time.perf_counter() - start_time # Different passes may fail for various reasons (e.g., found no routing solution). self.error_occurred = True - obs = create_feature_dict(self.state) + obs = create_feature_dict(self.state, self.device.num_qubits) # Trace the error before aborting self._collect_tracer_data( @@ -337,7 +337,7 @@ def step(self, action: int) -> tuple[dict[str, Any], float, bool, bool, dict[Any reward_val = 0 done = False - obs = create_feature_dict(self.state) + obs = create_feature_dict(self.state, self.device.num_qubits) # Trace+truncate if step limit is reached if not done and self.max_steps is not None and self.num_steps >= self.max_steps: @@ -447,7 +447,7 @@ def reset( self.num_qubits_uncompiled_circuit = self.state.num_qubits self.has_parameterized_gates = len(self.state.parameters) > 0 - obs = create_feature_dict(self.state) + obs = create_feature_dict(self.state, self.device.num_qubits) # Setup tracer for the new episode if self.tracer_output_path is not None: diff --git a/src/mqt/predictor/utils.py b/src/mqt/predictor/utils.py index 6310b3d55..b142edd79 100644 --- a/src/mqt/predictor/utils.py +++ b/src/mqt/predictor/utils.py @@ -147,7 +147,7 @@ def calc_supermarq_features( def get_openqasm_gates() -> list[str]: - """Return the gates from the OpenQASM 2.0 standard header.""" + """Return the canonical operation names from the OpenQASM standard libraries.""" return [ "u3", "u2", @@ -191,4 +191,5 @@ def get_openqasm_gates() -> list[str]: "c3x", "c3sqrtx", "c4x", + "measure", ] diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 940ffa704..0749f0830 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -45,7 +45,8 @@ def test_create_feature_dict() -> None: qc.ccx(0, 1, 2) qc.measure_all() - features = create_feature_dict(qc) + max_num_qubits = 20 + features = create_feature_dict(qc, max_num_qubits) for feature in features.values(): assert isinstance(feature, np.ndarray) @@ -56,7 +57,7 @@ def test_create_feature_dict() -> None: np.testing.assert_allclose(features["ccx"], [1 / 6]) np.testing.assert_allclose(features["measure"], [0.5]) np.testing.assert_allclose(features["x"], [0]) - np.testing.assert_allclose(features["num_qubits"], [3 / 127]) + np.testing.assert_allclose(features["num_qubits"], [3 / max_num_qubits]) np.testing.assert_allclose(features["depth"], [np.log1p(qc.depth()) / np.log1p(999_999)]) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index 48b3648d0..d9f15b6ae 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -42,13 +42,16 @@ def test_predictor_env_reset_from_string() -> None: """Test the reset function of the predictor environment with a quantum circuit given as a string as input.""" - device = get_device("ibm_eagle_127") + device = get_device("ibm_falcon_27") predictor = Predictor(figure_of_merit="expected_fidelity", device=device) qasm_path = Path("test.qasm") qc = get_benchmark("dj", BenchmarkLevel.ALG, 3) with qasm_path.open("w", encoding="utf-8") as f: dump(qc, f) - assert predictor.env.reset(qc=qasm_path)[0] == create_feature_dict(qc) + observation, _ = predictor.env.reset(qc=qasm_path) + + assert observation == create_feature_dict(qc, device.num_qubits) + assert observation["num_qubits"][0] == pytest.approx(qc.num_qubits / device.num_qubits) def test_predictor_env_esp_error() -> None: diff --git a/tests/compilation/test_tracer.py b/tests/compilation/test_tracer.py index 6911a8657..772eb1b8e 100644 --- a/tests/compilation/test_tracer.py +++ b/tests/compilation/test_tracer.py @@ -77,8 +77,8 @@ def test_compilation_tracer_generates_valid_json(tmp_path: Path) -> None: qc_terminal.rz(0.5, 2) # 2. Mock the feature dictionaries that the RL environment normally passes - features_baseline = create_feature_dict(qc_baseline) - features_terminal = create_feature_dict(qc_terminal) + features_baseline = create_feature_dict(qc_baseline, device.num_qubits) + features_terminal = create_feature_dict(qc_terminal, device.num_qubits) # 3. Use the actual `record_step` API instead of manually building CompilationSteps tracer.record_step( diff --git a/tests/device_selection/test_helper_ml.py b/tests/device_selection/test_helper_ml.py index 87f69f009..90ff6e818 100644 --- a/tests/device_selection/test_helper_ml.py +++ b/tests/device_selection/test_helper_ml.py @@ -10,7 +10,7 @@ from __future__ import annotations -from mqt.bench import BenchmarkLevel, get_benchmark +from qiskit import QuantumCircuit from mqt.predictor.ml.helper import ( create_feature_vector, @@ -23,9 +23,12 @@ def test_create_feature_vector() -> None: """Test the creation of a feature dictionary.""" - qc = get_benchmark("dj", BenchmarkLevel.ALG, 3) + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) feature_vector = create_feature_vector(qc) - assert feature_vector is not None + measure_index = get_openqasm_gates().index("measure") + + assert feature_vector[measure_index] == 1 def test_get_openqasm_gates() -> None: From e17ff33b75b560cd4085ce73acb397517eee275e Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 14:59:48 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=85=20Restore=20Eagle=20test=20fixtur?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: flowerthrower Assisted-by: GPT 5.6 via Codex --- tests/compilation/test_predictor_rl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index d9f15b6ae..c057431d6 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -42,7 +42,7 @@ def test_predictor_env_reset_from_string() -> None: """Test the reset function of the predictor environment with a quantum circuit given as a string as input.""" - device = get_device("ibm_falcon_27") + device = get_device("ibm_eagle_127") predictor = Predictor(figure_of_merit="expected_fidelity", device=device) qasm_path = Path("test.qasm") qc = get_benchmark("dj", BenchmarkLevel.ALG, 3) From bd90e7281a69801b3f3561dddbb793c332030ed6 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 15:26:40 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=93=9D=20Clarify=20RL=20observation?= =?UTF-8?q?=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: flowerthrower Assisted-by: GPT 5.6 via Codex --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aad794f7c..3886aaa4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ releases may include breaking changes. ### Added - ✨ Expand and compact the RL observation with normalized OpenQASM operation - frequencies and scalar qubit-count and depth values, and include measurements - in the shared ML feature schema ([#758]) ([**@flowerthrower**]) + frequencies and one-element `float32` qubit-count and depth arrays, and + include measurements in the shared ML feature schema ([#758]) + ([**@flowerthrower**]) - 👷 Enable testing on Python 3.14 ([#488]) ([**@denialhaag**]) - ✨ Add selectable `v2` and `v3` RL MDP strategies, make `v3` the default, and use the selected strategy in compilation traces and model artifact names From 16d7c15fe069c14f5f72c32103322ceb1c71335b Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Wed, 26 Aug 2026 17:09:45 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=A7=AA=20Validate=20benchmark=20featu?= =?UTF-8?q?re=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: flowerthrower Assisted-by: GPT 5.6 via Codex --- tests/compilation/test_helper_rl.py | 38 ++++++++++++++---------- tests/device_selection/test_helper_ml.py | 13 ++++---- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 0749f0830..5acc1f75e 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -18,7 +18,7 @@ from bqskit.ir.circuit import Circuit from mqt.bench import BenchmarkLevel, get_benchmark from mqt.bench.targets import get_device -from qiskit import QuantumCircuit, transpile +from qiskit import transpile from qiskit.transpiler import PassManager from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason @@ -29,6 +29,7 @@ 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 +from mqt.predictor.utils import get_openqasm_gates if TYPE_CHECKING: from collections.abc import Callable @@ -39,26 +40,31 @@ def test_create_feature_dict() -> None: """Test the creation of a feature dictionary.""" - qc = QuantumCircuit(3) - qc.h(0) - qc.cx(0, 1) - qc.ccx(0, 1, 2) - qc.measure_all() - - max_num_qubits = 20 - features = create_feature_dict(qc, max_num_qubits) + qc = get_benchmark("dj", BenchmarkLevel.ALG, 3) + device = get_device("ibm_eagle_127") + features = create_feature_dict(qc, device.num_qubits) for feature in features.values(): assert isinstance(feature, np.ndarray) assert feature.dtype == np.float32 - np.testing.assert_allclose(features["h"], [1 / 6]) - np.testing.assert_allclose(features["cx"], [1 / 6]) - np.testing.assert_allclose(features["ccx"], [1 / 6]) - np.testing.assert_allclose(features["measure"], [0.5]) - np.testing.assert_allclose(features["x"], [0]) - np.testing.assert_allclose(features["num_qubits"], [3 / max_num_qubits]) - np.testing.assert_allclose(features["depth"], [np.log1p(qc.depth()) / np.log1p(999_999)]) + expected_features = dict.fromkeys(get_openqasm_gates(), 0.0) + expected_features.update({ + "x": 1 / 9, + "h": 5 / 9, + "measure": 2 / 9, + "num_qubits": 3 / device.num_qubits, + "depth": np.log1p(5) / np.log1p(999_999), + "program_communication": 0.0, + "critical_depth": 0.0, + "entanglement_ratio": 0.0, + "parallelism": 1 / 5, + "liveness": 11 / 15, + }) + + assert features.keys() == expected_features.keys() + for name, expected in expected_features.items(): + np.testing.assert_allclose(features[name], [expected]) def test_get_path_trained_model() -> None: diff --git a/tests/device_selection/test_helper_ml.py b/tests/device_selection/test_helper_ml.py index 90ff6e818..7df00be2b 100644 --- a/tests/device_selection/test_helper_ml.py +++ b/tests/device_selection/test_helper_ml.py @@ -10,7 +10,8 @@ from __future__ import annotations -from qiskit import QuantumCircuit +import numpy as np +from mqt.bench import BenchmarkLevel, get_benchmark from mqt.predictor.ml.helper import ( create_feature_vector, @@ -23,12 +24,14 @@ def test_create_feature_vector() -> None: """Test the creation of a feature dictionary.""" - qc = QuantumCircuit(1, 1) - qc.measure(0, 0) + qc = get_benchmark("dj", BenchmarkLevel.ALG, 3) feature_vector = create_feature_vector(qc) - measure_index = get_openqasm_gates().index("measure") - assert feature_vector[measure_index] == 1 + expected_operations = dict.fromkeys(get_openqasm_gates(), 0.0) + expected_operations.update({"x": 1.0, "h": 5.0, "measure": 2.0}) + expected_features = [*expected_operations.values(), 3.0, 5.0, 0.0, 0.0, 0.0, 1 / 5, 11 / 15] + + np.testing.assert_allclose(feature_vector, expected_features) def test_get_openqasm_gates() -> None: From 0518b8347c404246897ab0b16fab06fdb23f66f3 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Fri, 28 Aug 2026 14:05:30 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=8E=A8=20Include=20IQM=20r=20in=20ope?= =?UTF-8?q?ration=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex Signed-off-by: flowerthrower Assisted-by: GPT 5.6 via Codex --- src/mqt/predictor/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mqt/predictor/utils.py b/src/mqt/predictor/utils.py index b142edd79..7eebf6bb7 100644 --- a/src/mqt/predictor/utils.py +++ b/src/mqt/predictor/utils.py @@ -192,4 +192,5 @@ def get_openqasm_gates() -> list[str]: "c3sqrtx", "c4x", "measure", + "r", ]