Skip to content

Fix box duration validation to track delays per qubit timeline#330

Open
ryanhill1 wants to merge 4 commits into
mainfrom
fix/box-delay-per-qubit
Open

Fix box duration validation to track delays per qubit timeline#330
ryanhill1 wants to merge 4 commits into
mainfrom
fix/box-delay-per-qubit

Conversation

@ryanhill1

@ryanhill1 ryanhill1 commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #329.

Box duration validation summed the durations of all delay statements inside a box into a single accumulator, regardless of which qubits they target. Delays on disjoint qubits execute in parallel, so this rejected valid programs whose per-qubit timelines fit comfortably within the declared duration — while accepting the same physical schedule written as a single broadcast delay:

box[300ns] { delay[200ns] q[0]; delay[200ns] q[1]; }  // rejected: "400.0ns > 300.0ns"
box[300ns] { delay[200ns] q; }                        // accepted (identical schedule)

Changes

  • Per-qubit accounting (visitor.py): the flat _total_delay_duration_in_box counter is replaced with a stack of per-box frames mapping (register, index) → accumulated delay on that qubit's timeline. A broadcast delay (no qubit args) now contributes to every qubit in scope, fixing the broadcast/per-qubit inconsistency. Validation compares the busiest single qubit timeline against the box duration.
  • Nested boxes: when an inner box closes, it contributes its declared duration (or its per-qubit delay totals, if undeclared) to the enclosing box's timelines. Previously the global accumulator was reset to zero when an inner box closed, so the outer box silently lost all inner delay accounting.
  • Clearer error: the validation message now names the offending qubit, e.g. Total delay duration value '350.0ns' on qubit 'q[0]' should be less than 'box[300.0ns]' duration.
  • Helper (analyzer.py): extracted Qasm3Analyzer.extract_qubit_key() from extract_duplicate_qubit() so both share the qubit-identity logic (handles physical $n qubits and indexed identifiers).

Semantics

  • box[300ns] { delay[200ns] q[0]; delay[200ns] q[1]; }valid (each timeline is 200ns)
  • box[300ns] { delay[200ns] q[0]; delay[200ns] q[0]; } → still invalid (q[0]'s timeline is 400ns)
  • box[300ns] { delay[200ns] q; } → still valid (each qubit's timeline is 200ns)
  • box[500ns] { box[400ns] { delay[100ns] q[0]; } delay[150ns] q[0]; } → now invalid (400 + 150 = 550ns on q[0]; previously the inner box's time was dropped entirely)

Testing

Test-driven: the four new/updated cases were written first and fail against the previous implementation (5 failed, 15 passed on tests/qasm3/test_box.py before the fix). With the fix:

  • tests/qasm3/test_box.py: 21 passed (new: parallel-disjoint-delays regression, broadcast/per-qubit consistency, same-qubit sequential overflow, nested-box propagation; updated: the two error-message expectations now include the qubit)
  • Full suite: 679 passed, 3 skipped (pre-existing skips)
  • black, isort, and pylint clean on changed files

Summary by CodeRabbit

  • Bug Fixes
    • Corrected box duration validation to track delay time independently for each qubit.
    • Parallel delays on separate qubits are now accepted when each timeline fits within the declared duration.
    • Nested box durations are properly included in enclosing box validation.
    • Validation errors now identify the specific qubit exceeding the allowed duration.
  • Tests
    • Added coverage for parallel, broadcast, sequential, and nested box-delay scheduling scenarios.
  • Documentation
    • Updated the unreleased changelog with the corrected box-duration behavior.

Box duration validation summed all delay durations inside a box into a
single accumulator regardless of target qubit, rejecting valid programs
whose parallel per-qubit timelines fit the declared duration. The same
schedule written as one broadcast delay was accepted, so semantically
identical programs validated differently.

Delays are now accumulated per qubit key on a stack of per-box frames:
the box only needs to fit the busiest single qubit timeline, sequential
delays on one qubit still accumulate and reject correctly, and a nested
box contributes its declared duration to the enclosing box's timelines
(previously the accumulator was reset when an inner box closed, losing
all inner delay accounting). The validation error now names the
offending qubit.

Fixes #329
@ryanhill1
ryanhill1 requested a review from TheGupta2012 as a code owner July 20, 2026 22:13
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d86274d5-f361-4173-933d-8ae958a83c35

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Box duration validation now accumulates delays per qubit, accounts for nested boxes, reports offending qubits, and adds regression coverage. Duplicate-qubit detection uses a shared identity-key helper for physical and indexed qubits.

Changes

Box timing validation

Layer / File(s) Summary
Per-qubit box delay accounting
src/pyqasm/visitor.py
Box scopes track delay totals per qubit, validate the maximum timeline against the declared duration, and propagate nested-box durations outward.
Box timing regression coverage
tests/qasm3/test_box.py, CHANGELOG.md
Tests cover parallel, broadcast, sequential, and nested delays, while the changelog documents the corrected validation behavior.

Qubit identity extraction

Layer / File(s) Summary
Shared qubit identity helper
src/pyqasm/analyzer.py
Duplicate-qubit detection uses a shared helper to derive identity keys for physical and indexed qubits.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: thegupta2012

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement per-qubit delay tracking, broadcast parity, nested-box propagation, and qubit-specific errors required by #329.
Out of Scope Changes check ✅ Passed No clearly unrelated changes are evident; the analyzer refactor and changelog entry are ancillary to the box-validation fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: box duration validation now tracks delays per qubit timeline.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/box-delay-per-qubit

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 `@src/pyqasm/analyzer.py`:
- Around line 266-270: Update extract_duplicate_qubit with the return annotation
tuple[str, int] | None, and annotate its qubit_set local variable as
set[tuple[str, int]]. Keep the existing duplicate-detection behavior unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 821d1321-ef4f-4f62-b42c-b9a0e5905d88

📥 Commits

Reviewing files that changed from the base of the PR and between 8d5f8af and 06baeb4.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/pyqasm/analyzer.py
  • src/pyqasm/visitor.py
  • tests/qasm3/test_box.py

Comment thread src/pyqasm/analyzer.py
Comment on lines +266 to 270
qubit_key = Qasm3Analyzer.extract_qubit_key(qubit)
if qubit_key in qubit_set:
return qubit_key
qubit_set.add(qubit_key)
return None

Copy link
Copy Markdown

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, but extract_duplicate_qubit still has no return annotation and qubit_set is untyped. Add -> tuple[str, int] | None and set[tuple[str, int]] so mypy can validate this refactored path.

🤖 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/pyqasm/analyzer.py` around lines 266 - 270, Update
extract_duplicate_qubit with the return annotation tuple[str, int] | None, and
annotate its qubit_set local variable as set[tuple[str, int]]. Keep the existing
duplicate-detection behavior unchanged.

Source: Coding guidelines

Comment thread src/pyqasm/visitor.py
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])

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.

Why not just keep the (name) of the register with the largest duration in the box as the key rather than the (name,index) pair? Eg. -

box[350ns] { delay[200ns] q[0]; delay[400ns] q[1]; delay[300] q[0]; delay[600] q_new[0]; }

would only require { 'q' : 500, 'q_new' : 600 } to determine whether the operations fit in the box duration. We can probably just make a simple dict rather than list of dict.

Do you see a use case for the per-qubit durations in any later processing stages?

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.

The per-qubit keys are load-bearing — the 500 in your example is q[0]'s sequential total (200 + 300), i.e. the max over per-qubit sums. A register-keyed dict has nowhere to hold q[0] = 200 while q[1] = 400 arrives, so at the third delay there's no way to know which running total the 300 belongs to. Both collapse strategies lose it:

  • sum into 'q' → 200+400+300 = 900 (that's the bug this PR fixes)
  • max into 'q' → max(200, 400, 300) = 400, never 500

The max variant also regresses a test in this PR:

box[300ns] { delay[200ns] q[0]; delay[200ns] q[1]; delay[150ns] q[0]; }

q[0]'s timeline is 350ns and must be rejected; register-max gives 200ns and silently accepts it — a false accept, worse than the false reject we started with.

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 list[dict]: that's a stack, not parallel data — one frame per open box, so a nested box doesn't clobber its parent's timelines. Depth is bounded by box nesting. Folding depth into a single dict's key would be strictly worse.

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.

@TheGupta2012 TheGupta2012 left a comment

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.

Thanks for fixing this @ryanhill1 ! Gave one comment regarding the data structure being used, but the idea is correct

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Box duration validation sums delays across qubits instead of per qubit timeline

3 participants