-
Notifications
You must be signed in to change notification settings - Fork 25
Fix box duration validation to track delays per qubit timeline #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ae1bd0
dbc5ada
06baeb4
773fd94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,7 +138,11 @@ def __init__( # pylint: disable=too-many-arguments | |
| self._in_generic_gate_op_scope: int = 0 | ||
| self._qubit_register_offsets: OrderedDict = OrderedDict() | ||
| self._qubit_register_max_offset = 0 | ||
| self._total_delay_duration_in_box = 0 | ||
| # Stack of per-box delay accounting frames. Each frame maps a qubit | ||
| # key (name, index) to the summed delay durations on that qubit's | ||
| # timeline within the box. Delays on disjoint qubits run in parallel, | ||
| # so box durations are validated against the per-qubit maximum. | ||
| self._box_delay_frames: list[dict[tuple[str, int], float]] = [] | ||
| self._in_extern_function: bool = False | ||
| self._openpulse_qubit_map: dict[str, set[str]] = {} | ||
| self._total_pulse_qubits: int = 0 | ||
|
|
@@ -2872,9 +2876,6 @@ def _visit_delay_statement( | |
| duration_val, unit=self._resolve_duration_unit(_delay_time_var) | ||
| ) | ||
|
|
||
| if self._scope_manager.in_box_scope(): | ||
| self._total_delay_duration_in_box += duration_val | ||
|
|
||
| if statement.qubits is not None: | ||
| _is_delay_frame = False | ||
| for qubit in statement.qubits: | ||
|
|
@@ -2901,6 +2902,21 @@ def _visit_delay_statement( | |
| span=statement.span, | ||
| ) | ||
|
|
||
| if self._box_delay_frames and duration_val: | ||
| # Record this delay on each targeted qubit's timeline. A delay | ||
| # with no qubit args applies to every qubit in scope. | ||
| if delay_qubit_bits: | ||
| delay_keys = [Qasm3Analyzer.extract_qubit_key(bit) for bit in delay_qubit_bits] | ||
| else: | ||
| delay_keys = [ | ||
| (reg_name, reg_id) | ||
| for reg_name, reg_size in self._global_qreg_size_map.items() | ||
| for reg_id in range(reg_size) | ||
| ] | ||
| box_delay_frame = self._box_delay_frames[-1] | ||
| for key in delay_keys: | ||
| box_delay_frame[key] = box_delay_frame.get(key, 0) + duration_val | ||
|
|
||
| if self._check_only: | ||
| return [] | ||
|
|
||
|
|
@@ -2944,6 +2960,7 @@ def _visit_box_statement(self, statement: qasm3_ast.Box) -> list[qasm3_ast.State | |
| self._scope_manager.push_scope({}) | ||
| self._scope_manager.increment_scope_level() | ||
| self._scope_manager.push_context(Context.BOX) | ||
| self._box_delay_frames.append({}) | ||
|
|
||
| if statement.body: | ||
| statements.extend( | ||
|
|
@@ -2960,19 +2977,30 @@ def _visit_box_statement(self, statement: qasm3_ast.Box) -> list[qasm3_ast.State | |
| self._scope_manager.decrement_scope_level() | ||
| self._scope_manager.pop_scope() | ||
|
|
||
| if ( | ||
| _box_time_var | ||
| and box_duration_val | ||
| and self._total_delay_duration_in_box > box_duration_val | ||
| ): | ||
| time_unit = self._resolve_duration_unit(_box_time_var).name | ||
| raise_qasm3_error( | ||
| f"Total delay duration value '{self._total_delay_duration_in_box}{time_unit}' " | ||
| f"should be less than 'box[{box_duration_val}{time_unit}]' duration.", | ||
| error_node=statement, | ||
| span=statement.span, | ||
| ) | ||
| self._total_delay_duration_in_box = 0 | ||
| delay_frame = self._box_delay_frames.pop() | ||
| if _box_time_var and box_duration_val and delay_frame: | ||
| # Delays on different qubits run in parallel, so the box only | ||
| # needs to fit the busiest single qubit timeline. | ||
| max_qubit_key = max(delay_frame, key=lambda k: delay_frame[k]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just keep the
would only require Do you see a use case for the per-qubit durations in any later processing stages?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The per-qubit keys are load-bearing — the
The max variant also regresses a test in this PR: box[300ns] { delay[200ns] q[0]; delay[200ns] q[1]; delay[150ns] q[0]; }
The register-level max is fine as an output, just not as an accumulator — and that's what the code already does: it accumulates per qubit, then takes the max at box close for the comparison and the error message. On On later stages: I'd argue it's required for correctness here regardless of future use. That said, per-qubit timelines are the natural input to anything scheduling-related later (stretch resolution, inferring an undeclared box duration), so I don't expect this to be throwaway state.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I was considering that the value is the max of the per-qubit sum of a register but I get that it might be a little complex to handle nested boxes that way |
||
| max_delay_duration = delay_frame[max_qubit_key] | ||
| if max_delay_duration > box_duration_val: | ||
| time_unit = self._resolve_duration_unit(_box_time_var).name | ||
| qubit_name, qubit_id = max_qubit_key | ||
| raise_qasm3_error( | ||
| f"Total delay duration value '{max_delay_duration}{time_unit}' " | ||
| f"on qubit '{qubit_name}[{qubit_id}]' should be less than " | ||
| f"'box[{box_duration_val}{time_unit}]' duration.", | ||
| error_node=statement, | ||
| span=statement.span, | ||
| ) | ||
| if self._box_delay_frames and delay_frame: | ||
| # Nested box: contribute this box's time to the enclosing box's | ||
| # per-qubit timelines. If the box declares a duration, that is | ||
| # the time it occupies; otherwise use each qubit's delay total. | ||
| parent_frame = self._box_delay_frames[-1] | ||
| for key, qubit_total in delay_frame.items(): | ||
| contribution = box_duration_val if box_duration_val else qubit_total | ||
| parent_frame[key] = parent_frame.get(key, 0) + contribution | ||
|
|
||
| if self._check_only: | ||
| return [] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add complete type annotations to duplicate detection.
The changed method now depends on a typed
(str, int)key, butextract_duplicate_qubitstill has no return annotation andqubit_setis untyped. Add-> tuple[str, int] | Noneandset[tuple[str, int]]so mypy can validate this refactored path.🤖 Prompt for AI Agents
Source: Coding guidelines