From def93a8cccba9814ccb7f9c384d491b73bd29708 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Thu, 27 Aug 2026 15:04:08 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A1=20Use=20compact=20scalar=20RL=20o?= =?UTF-8?q?bservations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: flowerthrower --- CHANGELOG.md | 5 +++-- UPGRADING.md | 10 +++++++--- src/mqt/predictor/rl/helper.py | 12 ++++++++---- src/mqt/predictor/rl/predictorenv.py | 10 +++++----- src/mqt/predictor/rl/tracer.py | 8 +++----- tests/compilation/test_helper_rl.py | 16 +++++++--------- tests/compilation/test_predictor_rl.py | 4 ++-- tests/compilation/test_tracer.py | 4 ++-- 8 files changed, 37 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c23d6add..3886aaa4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ releases may include breaking changes. ### Added -- ✨ Expand the RL observation with normalized OpenQASM operation frequencies - and include measurements in the shared ML feature schema ([#758]) +- ✨ 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]) ([**@flowerthrower**]) - 👷 Enable testing on Python 3.14 ([#488]) ([**@denialhaag**]) - ✨ Add selectable `v2` and `v3` RL MDP strategies, make `v3` the default, and diff --git a/UPGRADING.md b/UPGRADING.md index 6bd5fc3fb..fd0caf47f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,11 +6,15 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] -### RL operation features +### RL observation features The RL observation now includes normalized frequencies for supported OpenQASM -gates and measurements. Existing RL models must be retrained, and code that -consumes `PredictorEnv` observations directly must handle the additional keys. +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 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 44921e6d8..25812b453 100644 --- a/src/mqt/predictor/rl/helper.py +++ b/src/mqt/predictor/rl/helper.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging +from math import log1p from pathlib import Path from typing import TYPE_CHECKING @@ -28,6 +29,7 @@ logger = logging.getLogger("mqt-predictor") +MAX_CIRCUIT_DEPTH = 999_999 OBSERVATION_OPERATIONS = tuple(get_openqasm_gates()) @@ -72,11 +74,13 @@ 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]]: +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") - feature_dict: dict[str, int | NDArray[np.float32]] = { + 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], @@ -84,8 +88,8 @@ def create_feature_dict(qc: QuantumCircuit) -> dict[str, int | NDArray[np.float3 ) for operation in OBSERVATION_OPERATIONS }, - "num_qubits": qc.num_qubits, - "depth": qc.depth(), + "num_qubits": np.array([normalized_num_qubits], dtype=np.float32), + "depth": np.array([normalized_depth], dtype=np.float32), } supermarq_features = calc_supermarq_features(qc) diff --git a/src/mqt/predictor/rl/predictorenv.py b/src/mqt/predictor/rl/predictorenv.py index 123f1b3f9..c433ccaa7 100644 --- a/src/mqt/predictor/rl/predictorenv.py +++ b/src/mqt/predictor/rl/predictorenv.py @@ -183,8 +183,8 @@ def __init__( 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), @@ -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/rl/tracer.py b/src/mqt/predictor/rl/tracer.py index 171b1c323..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: - """Extract a float from a scalar or one-element feature array.""" - 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/tests/compilation/test_helper_rl.py b/tests/compilation/test_helper_rl.py index 8a87031bf..0749f0830 100644 --- a/tests/compilation/test_helper_rl.py +++ b/tests/compilation/test_helper_rl.py @@ -45,22 +45,20 @@ 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 name, feature in features.items(): - if name in {"num_qubits", "depth"}: - assert isinstance(feature, int) - else: - assert isinstance(feature, np.ndarray) - assert feature.dtype == np.float32 + 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]) - assert features["num_qubits"] == qc.num_qubits - assert features["depth"] == qc.depth() + 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)]) def test_get_path_trained_model() -> None: diff --git a/tests/compilation/test_predictor_rl.py b/tests/compilation/test_predictor_rl.py index af2825d89..c057431d6 100644 --- a/tests/compilation/test_predictor_rl.py +++ b/tests/compilation/test_predictor_rl.py @@ -50,8 +50,8 @@ def test_predictor_env_reset_from_string() -> None: dump(qc, f) observation, _ = predictor.env.reset(qc=qasm_path) - assert observation == create_feature_dict(qc) - assert observation["num_qubits"] == qc.num_qubits + 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( From 96a46c1aae3525f3ed30a8fb603f700e7155fd22 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Thu, 27 Aug 2026 15:15:11 +0200 Subject: [PATCH 2/3] Document compact scalar observations separately Signed-off-by: flowerthrower Assisted-by: GPT 5.6 via Codex --- CHANGELOG.md | 8 +++++--- UPGRADING.md | 14 +++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3886aaa4f..d82b1e6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,10 @@ releases may include breaking changes. ### Added -- ✨ 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]) +- ✨ Encode the RL qubit-count and depth observations as normalized + one-element `float32` arrays ([#784]) ([**@flowerthrower**]) +- ✨ Expand the RL observation with normalized OpenQASM operation frequencies + 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 @@ -93,6 +94,7 @@ for previous changelogs._ [#773]: https://github.com/munich-quantum-toolkit/predictor/pull/771 [#769]: https://github.com/munich-quantum-toolkit/predictor/pull/769 +[#784]: https://github.com/munich-quantum-toolkit/predictor/pull/784 [#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 diff --git a/UPGRADING.md b/UPGRADING.md index fd0caf47f..4d1f5046d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,15 +6,19 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] -### RL observation features +### Compact scalar RL observations -The RL observation now includes normalized frequencies for supported OpenQASM -gates and measurements. The `num_qubits` and `depth` entries are now one-element +The `num_qubits` and `depth` entries in RL observations are now one-element `float32` arrays in `[0, 1]` instead of discrete integers. The qubit count is 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. +consumes `PredictorEnv` observations directly must handle the new array values. + +### RL operation features + +The RL observation now includes normalized frequencies for supported OpenQASM +gates and measurements. Existing RL models must be retrained, and code that +consumes `PredictorEnv` observations directly must handle the additional keys. ### End of support for Python 3.10 From f514c31e881ac4ac8efadbef16dd87874eaf95b5 Mon Sep 17 00:00:00 2001 From: flowerthrower Date: Thu, 27 Aug 2026 15:26:58 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8E=A8=20Wrap=20the=20compact-observa?= =?UTF-8?q?tion=20changelog=20entry?= 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82b1e6e6..c0fa01d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ releases may include breaking changes. ### Added -- ✨ Encode the RL qubit-count and depth observations as normalized - one-element `float32` arrays ([#784]) ([**@flowerthrower**]) +- ✨ Encode the RL qubit-count and depth observations as normalized one-element + `float32` arrays ([#784]) ([**@flowerthrower**]) - ✨ Expand the RL observation with normalized OpenQASM operation frequencies and include measurements in the shared ML feature schema ([#758]) ([**@flowerthrower**])