♻️ Refactor RL actions by SDK#680
Conversation
## Description This PR addresses critical bugs in the RL training process with the following key changes: **Structure Improvements:** - **Redesigned action validation logic** (`predictorenv.py`): Rewrote `determine_valid_actions_for_state()` with a more structured (but equivalent) state machine that explicitly tracks three circuit states (synthesized, laid_out, routed) and handles 6 different state combinations. - Added helper methods `is_circuit_laid_out()` and `is_circuit_routed()` to replace the buggy `CheckMap` pass with more reliable state checking. The new logic supports both the original restricted MDP and a flexible general MDP mode. - **Fixed type annotation** (`actions.py`): Corrected `do_while` parameter type from `dict[str, Circuit]` to `PropertySet` and added missing import for Qiskit's `PropertySet`. - **Added reproducibility** (`predictor.py`): Set random seed for non-test training runs to ensure reproducible results. - **Improved VF2Layout error handling** (`predictorenv.py`): Replaced assertion failures with warning logs when VF2Layout doesn't find a solution, preventing crashes during training. **Test Updates:** - Suppressed deprecation warnings in tket routing test --------- Signed-off-by: Patrick Hopf <81010725+flowerthrower@users.noreply.github.com> Co-authored-by: flowerthrower <flowerthrower@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ove-action-pass-imports-and-wrappers
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughThe monolithic ChangesRL Actions Architecture Refactor
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 14: Update the changelog entry that currently reads "♻️ Restructure
existing RL passes into SDK-level action modules ([`#644`])" to reference the
correct PR number ([`#680`]) and add a corresponding link definition for [`#680`] in
the PR links section so the markdown link resolves; modify the entry text where
the phrase "Restructure existing RL passes into SDK-level action modules"
appears and add the matching link definition in the PR links block (the section
that contains other bracketed PR references).
In `@src/mqt/predictor/rl/actions/__init__.py`:
- Around line 86-95: The docstring for remove_action promises ValueError on
missing names but the function currently raises KeyError; change the raised
exception to ValueError (e.g., replace raise KeyError(msg) with raise
ValueError(msg)) so the implementation matches the documented API, keeping the
existing message and using the same _ACTIONS lookup logic in remove_action to
locate the correct place to modify.
- Around line 44-68: The public dataclasses Action and DeviceDependentAction are
missing Google-style docstrings for their public fields; update the docstrings
on both classes (Action, DeviceIndependentAction, DeviceDependentAction) to use
Google-style format and add an "Attributes:" section that documents each exposed
field (for Action: name, origin, pass_type, transpile_pass, preserves_layout,
preserves_routing, preserves_synthesis; for DeviceDependentAction also document
transpile_pass override and do_while) with a brief one-line description and type
for each attribute, keeping descriptions concise and matching the existing field
names and types (e.g., Callable[[PropertySet], bool] | None for do_while).
- Around line 73-103: The three functions register_action, remove_action, and
get_actions_by_pass_type lack Google-style docstring sections; update each
function's docstring to include "Args:" documenting parameters (e.g., action:
Action for register_action, name: str for remove_action), "Returns:" documenting
the return types (Action for register_action, None for remove_action,
dict[PassType, list[Action]] for get_actions_by_pass_type), and keep the
existing "Raises:" entries (ValueError/KeyError) as appropriate; reference the
Action and PassType types and the global _ACTIONS registry in the descriptions
to make the purpose clear.
In `@src/mqt/predictor/rl/actions/bqskit_actions.py`:
- Around line 135-145: Update the docstrings to Google style for
get_bqskit_native_gates and final_layout_bqskit_to_qiskit: replace "Arguments:"
with "Args:", add structured "Returns:" and "Raises:" sections (and for
final_layout_bqskit_to_qiskit also add an "Args:" describing the input
types/semantics and a "Returns:" describing the returned layout mapping and its
format), and ensure types (e.g., device: Target, return: list[Gate] or mapping
types) and any error conditions are documented exactly as in the functions
get_bqskit_native_gates and final_layout_bqskit_to_qiskit so they conform to the
repository Google-style docstring guidelines.
In `@src/mqt/predictor/rl/actions/qiskit_actions.py`:
- Around line 326-350: The VF2PostLayout branch currently discards the updated
ApplyLayout property set returned by postprocess_vf2postlayout and rebuilds
TranspileLayout from stale values; instead, after calling
postprocess_vf2postlayout(altered_qc, post_layout, layout) and updating
altered_qc, use the updated entries in property_set (e.g.
property_set["layout"], property_set["original_qubit_indices"],
property_set["final_layout"]) when constructing and returning TranspileLayout so
the returned layout metadata matches the rewritten circuit; keep using the
altered_qc and _input_qubit_count as before.
In `@src/mqt/predictor/rl/actions/tket_actions.py`:
- Around line 150-162: The runtime contract mismatch: is_tket_action_available
currently allows PassType.ROUTING when has_layout is false but run_tket_action
asserts layout is not None before writing final_layout; fix by making
run_tket_action tolerant of a None layout for routing: when action.pass_type ==
PassType.ROUTING and layout is None, construct a new TranspileLayout (or
equivalent empty layout object used elsewhere, e.g. the same type expected by
final_layout_pytket_to_qiskit) before assigning layout.final_layout =
final_layout_pytket_to_qiskit(tket_qc, altered_qc); keep the existing behavior
if layout is provided. Ensure you touch run_tket_action (the layout handling
path) and do not change is_tket_action_available unless you prefer the
alternative of enforcing has_layout for ROUTING.
In `@src/mqt/predictor/rl/predictorenv.py`:
- Around line 172-183: The broad `except Exception: # noqa: BLE001` around the
apply_action call masks unexpected bugs; either narrow it to the specific error
classes `apply_action` can raise (e.g., ValueError, RuntimeError, ActionError)
and handle those, or keep a single broad handler but add a short rationale
comment explaining why catching all exceptions is necessary (e.g., to truncate
the environment on any action failure) and re-raise or log truly unexpected
exceptions if needed; update the block around self.used_actions.append /
altered_qc = self.apply_action(action) and set self.error_occurred as before,
but prefer enumerating concrete exception types or justify the BLE001
suppression in a comment next to the except line referencing apply_action and
error handling policy.
- Around line 325-360: apply_action currently only validates that action_index
exists in self.action_set, allowing masked-out actions to be executed; call the
environment's action mask (e.g., self.action_masks()) at the start of
apply_action, verify that the bit/entry for action_index is True/allowed, and if
not raise an informative ValueError (include action_index and action.origin);
apply this check before handling PassType.TERMINATE and before calling
run_qiskit_action/run_tket_action/run_bqskit_action so masked TERM and SDK
actions are rejected rather than executed.
- Around line 313-324: Update the docstring for the method apply_action to use
Google-style section headers: replace "Arguments:" with "Args:", keep "Returns:"
and "Raises:" as-is but ensure formatting matches Google style (indent param
lines under Args:, describe return under Returns:, and exceptions under
Raises:). Ensure the parameter name action_index, its type and description, the
returned QuantumCircuit, and the ValueError explanation appear under the correct
Google-style headers in the apply_action docstring.
- Around line 140-143: reset() currently clears self.layout causing
determine_valid_actions_for_state() to miscompute laid_out and step() allows
actions without checking valid masks; update reset() to preserve an incoming
circuit layout when present (do not unconditionally set self.layout = None) and
ensure determine_valid_actions_for_state() uses self.layout correctly to compute
laid_out; in step(), validate the requested action against self.valid_actions or
action_masks() before calling apply_action() (disallow TERMINATE or SDK actions
when not in self.valid_actions); narrow the broad "except Exception" to a
specific exception class (or add a comment explaining why a catch-all is
required) and update apply_action() docstring heading from "Arguments:" to
Google-style "Args:" so docs are consistent.
In `@tests/compilation/test_integration_further_SDKs.py`:
- Line 107: Replace the private access layout_before._input_qubit_count by
deriving the count from public metadata (e.g. compute it as
len(layout_before.initial_layout) or another public property that lists the
input qubits) and remove the "# noqa: SLF001" suppression; update the call site
that used _input_qubit_count to use the computed integer instead (reference
symbols: layout_before and _input_qubit_count).
- Around line 234-243: The preserves_layout check can be bypassed when
env.layout is None; change the conditional so the test fails if layout was
dropped: assert env.layout is not None, (f"{action.name} on
{env.device.description} VIOLATED INVARIANT preserves_layout: layout metadata
was removed"), then compute post_v2p =
dict(env.layout.initial_layout.get_virtual_bits()) and assert post_v2p ==
pre_v2p with the existing descriptive message; keep the existing
is_circuit_laid_out(compiled, layout) assertion but replace the optional if
block around env.layout with an explicit assertion that env.layout remains
present before comparing mappings (references: env.layout,
initial_layout.get_virtual_bits(), pre_v2p, preserves_layout).
- Around line 36-45: Both helper functions use short one-line docstrings—replace
them with Google-style docstrings: for _setup_env(PredictorEnv env,
QuantumCircuit circuit, TranspileLayout|None layout, int n_qubits) include an
Args section describing each parameter and a Returns section noting None and the
side-effect of resetting the env; for _is_available(PredictorEnv env, int idx)
include an Args section for env and idx and a Returns section describing that it
returns bool indicating whether the action is structurally and SDK-valid and
that it updates env.valid_actions via determine_valid_actions_for_state. Ensure
parameter types match the signatures and keep descriptions concise.
In `@tests/compilation/test_predictor_rl.py`:
- Around line 182-183: The test currently mutates env.action_set[0] which is
safe because PredictorEnv builds self.action_set per-instance, but it’s brittle
to hardcode index 0; change the test to pick a valid routing index from the
environment (e.g., use an index from env.actions_routing_indices or another
existing key in env.action_set) and assign the action to that index instead of 0
so the test does not depend on internal action ordering in PredictorEnv.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: a836606e-13dc-4e30-9f71-5b2118832d30
📒 Files selected for processing (12)
.gitignoreCHANGELOG.mdsrc/mqt/predictor/rl/actions.pysrc/mqt/predictor/rl/actions/__init__.pysrc/mqt/predictor/rl/actions/bqskit_actions.pysrc/mqt/predictor/rl/actions/qiskit_actions.pysrc/mqt/predictor/rl/actions/tket_actions.pysrc/mqt/predictor/rl/parsing.pysrc/mqt/predictor/rl/predictorenv.pytests/compilation/test_helper_rl.pytests/compilation/test_integration_further_SDKs.pytests/compilation/test_predictor_rl.py
💤 Files with no reviewable changes (2)
- src/mqt/predictor/rl/actions.py
- src/mqt/predictor/rl/parsing.py
| if action_index not in self.action_set: | ||
| msg = f"Action {action_index} not supported." | ||
| raise ValueError(msg) | ||
|
|
||
| action = self.action_set[action_index] | ||
|
|
||
| if action.name == "terminate": | ||
| if action.pass_type == PassType.TERMINATE: | ||
| return self.state | ||
|
|
||
| if action.origin == CompilationOrigin.QISKIT: | ||
| return self._apply_qiskit_action(action, action_index) | ||
| if action.origin == CompilationOrigin.TKET: | ||
| return self._apply_tket_action(action, action_index) | ||
| if action.origin == CompilationOrigin.BQSKIT: | ||
| return self._apply_bqskit_action(action, action_index) | ||
| msg = f"Origin {action.origin} not supported." | ||
| raise ValueError(msg) | ||
|
|
||
| def _apply_qiskit_action(self, action: Action, action_index: int) -> QuantumCircuit: | ||
| if action.name == "QiskitO3" and isinstance(action, DeviceDependentAction): | ||
| factory = cast("Callable[[list[str], CouplingMap | None], list[Task]]", action.transpile_pass) | ||
| passes = factory( | ||
| self.device.operation_names, | ||
| CouplingMap(self.device.build_coupling_map()) if self.layout else None, | ||
| altered_qc, self.layout = run_qiskit_action( | ||
| action=action, | ||
| circuit=self.state, | ||
| device=self.device, | ||
| layout=self.layout, | ||
| input_qubit_count=self.num_qubits_uncompiled_circuit, | ||
| ) | ||
| assert action.do_while is not None | ||
| pm = PassManager([DoWhileController(passes, do_while=action.do_while)]) | ||
| else: | ||
| if callable(action.transpile_pass): | ||
| factory = cast("Callable[[Target], list[Task]]", action.transpile_pass) | ||
| passes = factory(self.device) | ||
| else: | ||
| passes = cast("list[Task]", action.transpile_pass) | ||
| pm = PassManager(passes) | ||
|
|
||
| altered_qc = pm.run(self.state) | ||
|
|
||
| if action_index in ( | ||
| self.actions_layout_indices + self.actions_mapping_indices + self.actions_final_optimization_indices | ||
| ): | ||
| altered_qc = self._handle_qiskit_layout_postprocessing(action, pm, altered_qc) | ||
|
|
||
| elif ( | ||
| action_index in self.actions_routing_indices and self.layout and pm.property_set["final_layout"] is not None | ||
| ): | ||
| self.layout.final_layout = pm.property_set["final_layout"] | ||
|
|
||
| # BasisTranslator errors on unitary gates; decompose them immediately so | ||
| # the circuit is always in a consistent state after a Qiskit action. | ||
| if altered_qc.count_ops().get("unitary"): | ||
| altered_qc = altered_qc.decompose(gates_to_decompose="unitary") | ||
|
|
||
| return altered_qc | ||
|
|
||
| def _handle_qiskit_layout_postprocessing( | ||
| self, action: Action, pm: PassManager, altered_qc: QuantumCircuit | ||
| ) -> QuantumCircuit: | ||
| if action.name == "VF2PostLayout": | ||
| assert pm.property_set["VF2PostLayout_stop_reason"] is not None | ||
| post_layout = pm.property_set["post_layout"] | ||
| if post_layout: | ||
| assert self.layout is not None | ||
| altered_qc, _ = postprocess_vf2postlayout(altered_qc, post_layout, self.layout) | ||
| elif action.name == "VF2Layout": | ||
| if pm.property_set["VF2Layout_stop_reason"] != VF2LayoutStopReason.SOLUTION_FOUND: | ||
| logger.warning( | ||
| "VF2Layout pass did not find a solution. Reason: %s", | ||
| pm.property_set["VF2Layout_stop_reason"], | ||
| ) | ||
| else: | ||
| assert pm.property_set["layout"] | ||
| else: | ||
| assert pm.property_set["layout"] | ||
|
|
||
| if pm.property_set["layout"]: | ||
| # Layout/mapping passes create the base logical-to-physical mapping; | ||
| # later routing actions only update final_layout. | ||
| self.layout = TranspileLayout( | ||
| initial_layout=pm.property_set["layout"], | ||
| input_qubit_mapping=pm.property_set["original_qubit_indices"], | ||
| final_layout=pm.property_set["final_layout"], | ||
| _output_qubit_list=altered_qc.qubits, | ||
| _input_qubit_count=self.num_qubits_uncompiled_circuit, | ||
| elif action.origin == CompilationOrigin.TKET: | ||
| altered_qc, self.layout = run_tket_action( | ||
| action=action, | ||
| circuit=self.state, | ||
| device=self.device, | ||
| layout=self.layout, | ||
| ) | ||
| return altered_qc | ||
|
|
||
| def _apply_tket_action(self, action: Action, action_index: int) -> QuantumCircuit: | ||
| tket_qc = qiskit_to_tk(self.state, preserve_param_uuid=True) | ||
| if callable(action.transpile_pass): | ||
| factory = cast("Callable[[Target], list[Task]]", action.transpile_pass) | ||
| passes = factory(self.device) | ||
| else: | ||
| passes = cast("list[Task]", action.transpile_pass) | ||
| for pass_ in passes: | ||
| assert isinstance(pass_, TketBasePass | PreProcessTKETRoutingAfterQiskitLayout) | ||
| pass_.apply(tket_qc) | ||
|
|
||
| qbs = tket_qc.qubits | ||
| tket_qc.rename_units({qbs[i]: Qubit("q", i) for i in range(len(qbs))}) | ||
| altered_qc = tk_to_qiskit(tket_qc, replace_implicit_swaps=True) | ||
|
|
||
| if action_index in self.actions_routing_indices: | ||
| assert self.layout is not None | ||
| self.layout.final_layout = final_layout_pytket_to_qiskit(tket_qc, altered_qc) | ||
|
|
||
| return altered_qc | ||
|
|
||
| def _apply_bqskit_action(self, action: Action, action_index: int) -> QuantumCircuit: | ||
| """Applies the given BQSKit action to the current state and returns the altered state. | ||
|
|
||
| Arguments: | ||
| action: The BQSKit action to be applied. | ||
| action_index: The index of the action in the action set. | ||
|
|
||
| Returns: | ||
| The altered quantum circuit after applying the action. | ||
|
|
||
| Raises: | ||
| ValueError: If the action index is not in the action set or if the action origin is not supported. | ||
| """ | ||
| bqskit_qc = qiskit_to_bqskit(self.state) | ||
| if action_index in self.actions_opt_indices: | ||
| transpile = cast("Callable[[Circuit], Circuit]", action.transpile_pass) | ||
| bqskit_compiled_qc = transpile(bqskit_qc) | ||
| elif action_index in self.actions_synthesis_indices: | ||
| factory = cast("Callable[[Target], Callable[[Circuit], Circuit]]", action.transpile_pass) | ||
| bqskit_compiled_qc = factory(self.device)(bqskit_qc) | ||
| elif action_index in self.actions_mapping_indices: | ||
| factory = cast( | ||
| "Callable[[Target], Callable[[Circuit], tuple[Circuit, tuple[int, ...], tuple[int, ...]]]]", | ||
| action.transpile_pass, | ||
| elif action.origin == CompilationOrigin.BQSKIT: | ||
| altered_qc, self.layout = run_bqskit_action( | ||
| action=action, | ||
| circuit=self.state, | ||
| device=self.device, | ||
| layout=self.layout, | ||
| ) | ||
| bqskit_compiled_qc, initial, final = factory(self.device)(bqskit_qc) | ||
| compiled_qiskit_qc = bqskit_to_qiskit(bqskit_compiled_qc) | ||
| self.layout = final_layout_bqskit_to_qiskit(initial, final, compiled_qiskit_qc, self.state) | ||
| return compiled_qiskit_qc | ||
| else: | ||
| msg = f"Unhandled BQSKit action index: {action_index}" | ||
| msg = f"Origin {action.origin} not supported." | ||
| raise ValueError(msg) | ||
|
|
||
| return bqskit_to_qiskit(bqskit_compiled_qc) | ||
| return altered_qc |
There was a problem hiding this comment.
Reject actions that are masked out for the current state.
apply_action() now only checks that the index exists. That lets callers bypass action_masks() entirely: TERMINATE succeeds from the initial state, and SDK actions filtered out by availability are still dispatched until a helper fails. Enforce the current mask here before returning self.state or calling any run_*_action helper.
Suggested fix
if action_index not in self.action_set:
msg = f"Action {action_index} not supported."
raise ValueError(msg)
+ if not self.action_masks()[action_index]:
+ msg = f"Action {action_index} is not valid for the current circuit state."
+ raise ValueError(msg)
action = self.action_set[action_index]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mqt/predictor/rl/predictorenv.py` around lines 325 - 360, apply_action
currently only validates that action_index exists in self.action_set, allowing
masked-out actions to be executed; call the environment's action mask (e.g.,
self.action_masks()) at the start of apply_action, verify that the bit/entry for
action_index is True/allowed, and if not raise an informative ValueError
(include action_index and action.origin); apply this check before handling
PassType.TERMINATE and before calling
run_qiskit_action/run_tket_action/run_bqskit_action so masked TERM and SDK
actions are rejected rather than executed.
There was a problem hiding this comment.
as above, this should not happen (or do you see a path?)
There was a problem hiding this comment.
@flowerthrower, you're right — in the normal RL training loop the sequence is:
policy → action_masks() → step() → apply_action()
By the time apply_action() is reached, the policy has already used the mask to select only structurally and SDK-valid actions, so an additional guard inside apply_action() would be redundant, for exactly the same reason as the existing guard-free design of step(). I don't see a reachable path in the current codebase where a masked-out action could arrive there. Withdrawing the suggestion.
✏️ Learnings added
Learnt from: flowerthrower
Repo: munich-quantum-toolkit/predictor PR: 680
File: src/mqt/predictor/rl/predictorenv.py:325-360
Timestamp: 2026-06-08T13:25:02.753Z
Learning: In munich-quantum-toolkit/predictor, `PredictorEnv.apply_action()` in `src/mqt/predictor/rl/predictorenv.py` does not re-validate the requested action against `action_masks()` before dispatching to `run_*_action` helpers. This is intentional: `apply_action()` is only ever called from `step()`, which itself is only reached after the RL policy has already filtered actions through `action_masks()`, making an additional guard inside `apply_action()` redundant.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: flowerthrower
Repo: munich-quantum-toolkit/predictor PR: 680
File: src/mqt/predictor/rl/predictorenv.py:140-143
Timestamp: 2026-06-08T13:11:33.062Z
Learning: In munich-quantum-toolkit/predictor, `PredictorEnv.step()` in `src/mqt/predictor/rl/predictorenv.py` does not re-validate the requested action against `self.valid_actions` or `action_masks()` before calling `apply_action()`. This is intentional: step() is only called in a tightly controlled RL training loop after actions have already been filtered by masking, so an additional guard would be redundant.
Learnt from: flowerthrower
Repo: munich-quantum-toolkit/predictor PR: 680
File: src/mqt/predictor/rl/predictorenv.py:140-143
Timestamp: 2026-06-08T13:11:33.062Z
Learning: In munich-quantum-toolkit/predictor, the broad `except Exception` catch (with `# noqa: BLE001`) in `PredictorEnv.step()` in `src/mqt/predictor/rl/predictorenv.py` is intentional. Qiskit, TKET, and BQSKit compilation passes can raise a wide variety of unpredictable exceptions, and catching them all to truncate the episode gracefully is the desired design.
Learnt from: denialhaag
Repo: munich-quantum-toolkit/predictor PR: 572
File: tests/compilation/test_integration_further_SDKs.py:20-20
Timestamp: 2026-01-21T00:18:21.993Z
Learning: In the munich-quantum-toolkit/predictor repository, Ruff is configured with preview mode enabled (`preview = true`). The PLC2701 rule (import from private modules) is active, so `# noqa: PLC2701` directives are necessary when importing from private modules like `pytket._tket.passes`, even if Ruff reports RUF100 warnings suggesting the directive is unused.
Learnt from: flowerthrower
Repo: munich-quantum-toolkit/predictor PR: 680
File: src/mqt/predictor/rl/predictorenv.py:140-143
Timestamp: 2026-06-08T13:11:33.062Z
Learning: In munich-quantum-toolkit/predictor, `PredictorEnv.reset()` in `src/mqt/predictor/rl/predictorenv.py` unconditionally sets `self.layout = None`. This is intentional: reset() is only called at the start of a new rollout from the fully uncompiled training circuit set, so no pre-existing layout should ever be assumed or preserved.
Learnt from: denialhaag
Repo: munich-quantum-toolkit/predictor PR: 572
File: src/mqt/predictor/rl/predictorenv.py:0-0
Timestamp: 2026-01-21T00:17:19.184Z
Learning: In the munich-quantum-toolkit/predictor repository, Ruff is configured with preview = true and the "PL" (pylint) rule category in extend-select. PLC2701 (import-private-name) is a preview rule that flags imports from private modules (names starting with underscore). The `# noqa: PLC2701` directive on the import `from pytket._tket.passes import BasePass as TketBasePass` in src/mqt/predictor/rl/predictorenv.py is necessary and appropriate, even if Ruff reports RUF100 warnings suggesting the directive is unused.
Learnt from: flowerthrower
Repo: munich-quantum-toolkit/predictor PR: 526
File: src/mqt/predictor/rl/predictorenv.py:238-243
Timestamp: 2026-02-09T13:15:50.387Z
Learning: In src/mqt/predictor/rl/predictorenv.py, when reward_function is 'estimated_hellinger_distance', compute the Hellinger-distance reward at every step rather than only at episode termination. Currently, non-terminal steps receive no_effect_penalty, which prevents intermediate shaping signals. Implement delta-based reward shaping by producing or accumulating a per-step shaping signal (analogous to how it's done for expected_fidelity and estimated_success_probability) so that intermediate rewards reflect progress toward the goal. Maintain the existing terminal reward behavior on episode end. Add or adjust tests to verify step-wise rewards for this case and ensure the shaping signal is correctly integrated into the total return.
Learnt from: burgholzer
Repo: munich-quantum-toolkit/predictor PR: 526
File: src/mqt/predictor/rl/predictorenv.py:271-271
Timestamp: 2025-12-25T13:28:19.850Z
Learning: In the munich-quantum-toolkit/predictor repository, Ruff has a broad rule set including FLake8-SLF (slf) in extend-select. For private member access (e.g., self.state._layout = self.layout) in src/mqt/predictor/rl/predictorenv.py, include a per-file noqa directive: # noqa: SLF001. This is appropriate even if Ruff reports RUF100 as unused, to acknowledge intentional private attribute access and to avoid false positives in this specific code path. Apply this directive only to the files where private-member access is intentionally used and where SLF001 is the correct rationale.
Learnt from: linus-hologram
Repo: munich-quantum-toolkit/predictor PR: 641
File: src/mqt/predictor/rl/tracer.py:172-186
Timestamp: 2026-04-13T21:24:06.882Z
Learning: In the munich-quantum-toolkit/predictor repository, Ruff’s flake8-boolean-trap integration (FBT) is not enabled (FBT is not in Ruff’s `extend-select` in `pyproject.toml`), and the specific rule `FBT001` is not active. Therefore, during code reviews for this repo, do not flag boolean-typed positional arguments as `FBT001` Ruff violations unless `FBT001` is re-enabled in `pyproject.toml`.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Patrick Hopf <81010725+flowerthrower@users.noreply.github.com>
…ove-action-pass-imports-and-wrappers
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/compilation/test_predictor_rl.py`:
- Around line 25-26: The import statement for qiskit_actions uses an alias that
matches the module name, which Ruff suggests simplifying per PLR0402. Replace
the import statement `import mqt.predictor.rl.actions.qiskit_actions as
qiskit_actions` with a from-import style: `from mqt.predictor.rl.actions import
qiskit_actions`. This eliminates the redundant alias and improves code clarity.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 73f17e94-e81a-4060-a17a-eb0063efc4fa
📒 Files selected for processing (8)
CHANGELOG.mdsrc/mqt/predictor/rl/actions/__init__.pysrc/mqt/predictor/rl/actions/bqskit_actions.pysrc/mqt/predictor/rl/actions/qiskit_actions.pysrc/mqt/predictor/rl/predictorenv.pytests/compilation/test_helper_rl.pytests/compilation/test_integration_further_SDKs.pytests/compilation/test_predictor_rl.py
| import mqt.predictor.rl.actions as actions_module | ||
| import mqt.predictor.rl.actions.qiskit_actions as qiskit_actions |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Simplify import per Ruff PLR0402.
The alias qiskit_actions is identical to the module name. Ruff suggests using a from import for clarity.
♻️ Proposed simplification
import mqt.predictor.rl.actions as actions_module
-import mqt.predictor.rl.actions.qiskit_actions as qiskit_actions
+from mqt.predictor.rl.actions import qiskit_actions🧰 Tools
🪛 Ruff (0.15.17)
[warning] 26-26: Use from mqt.predictor.rl.actions import qiskit_actions in lieu of alias
Replace with from mqt.predictor.rl.actions import qiskit_actions
(PLR0402)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/compilation/test_predictor_rl.py` around lines 25 - 26, The import
statement for qiskit_actions uses an alias that matches the module name, which
Ruff suggests simplifying per PLR0402. Replace the import statement `import
mqt.predictor.rl.actions.qiskit_actions as qiskit_actions` with a from-import
style: `from mqt.predictor.rl.actions import qiskit_actions`. This eliminates
the redundant alias and improves code clarity.
Source: Linters/SAST tools
Description
Restructures RL actions into SDK-specific modules for Qiskit, TKET, and BQSKit. Also moves the corresponding wrapper logic closer to each SDK action implementation and keeps compatibility exports for existing imports.
This immensely improves modularity and reduces SDK-dependent patches scattered across RL modules.
It also enables much cleaner and proper testing of pass invariants.
actions.pyto SDK-specific action modules.rl/actions/__init__.py.PredictorEnv.parsing.py; conversion/layout helpers now live with the owning SDK.Fixes #668
Fixes #66
Assisted by: GPT5.5 via Codex
Checklist
If PR contains AI-assisted content:
Assisted-by: [Model Name] via [Tool Name]footer.