Skip to content

claude identified bugs/validations - #71

Closed
qci-amos wants to merge 4 commits into
mainfrom
bug_fixes
Closed

claude identified bugs/validations#71
qci-amos wants to merge 4 commits into
mainfrom
bug_fixes

Conversation

@qci-amos

@qci-amos qci-amos commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Client-side fixes for findings from the QCDL alpha test report (2026-08-18). Each
of these was reported as something dwave-gate accepts silently and the service
either rejects one round trip later or, worse, executes into a plausible-looking
wrong answer.

Closes 11 findings: QCDL-03, 04, 05, 06, 07, 10, 12, 13, 14, 25, 26.
Partially addresses QCDL-23: adds the missing sxdg gate.

Nothing here touches the service, the transpiler, or dwave-cloud-client, so the
report's two critical findings (QCDL-16 register-driven rotations, QCDL-09 mixed
register arithmetic) are still open. See Follow-ups.

What changed

@qcdl decorator validation — QCDL-03 / 04 / 05

qcdl_circuit.py

  • num_qubits is validated when the decorator is built: < 1, non-integers, and
    a bare @qcdl (used without parentheses, so the function itself arrives as
    num_qubits) all raise QCDLUserError. Previously @qcdl(0) and @qcdl(-1)
    built an empty program.
  • Qubits are injected by keyword, so a parameter that is not named q<N>
    silently receives nothing. Any required parameter left unbound now raises a
    TypeError that keeps Python's own missing 1 required positional argument: 'alpha' wording and then explains the rule and what was actually supplied.
    This was the single most confusing failure in the report.
  • If num_qubits generates a qubit the signature has no parameter for, that
    qubit was previously dropped from the program in silence. It now raises,
    naming the dropped qubit. Environment mode is exempt, since an environment
    legitimately supplies its whole set.

Gate argument validation — QCDL-06 / 07

operations.py

A _validate_qubit_args decorator on all 34 operations checks the qubit
arguments before the statement is recorded:

  • A non-qubit argument raises QCDLUserError: h() parameter 'qubit' must be a qubit, not int (5); … instead of surfacing later as
    AttributeError: 'int' object has no attribute 'procedure'.
  • Two-qubit gates reject a repeated qubit: cx() needs distinct qubits, but parameters 'control_qubit' and 'target_qubit' are both q0. cx(q0, q0)
    previously built cleanly and failed only at submission.

Whether the qubits must be distinct is derived from the signature rather than
configured per gate: each named qubit parameter is a separate role in the
operation, whereas a *qubits parameter is a set, so initialize(q0, q1, q0)
and barrier(q0, q0) remain legal.

A wrong number of arguments still reports against the real signature, and
functools.wraps keeps signatures and docstrings intact for sphinx.

Duplicate register declarations — QCDL-10

components.py, registers.py

The compiler keeps the first allocation of a register name, so re-declaring
one was a silent no-op that discarded the second initial value.
Procedure.register_memory_allocation now tracks names per module per procedure
and reports the collision. The same name on different qubits, and in a nested
procedure, is still fine.

Re-declaring deliberately is allowed via the existing alias=True /
ignore_reallocation=True arguments — but not together with an explicit
initial_value, because that value is information the caller stated explicitly
and the compiler would throw away:

already allocated fresh name
plain redeclaration raise
ignore_reallocation=True, no value allowed allowed
ignore_reallocation=True, explicit value raise allowed
alias=True, no value allowed allowed
alias=True, explicit value raise raise

alias=True raises even for a fresh name: an alias never emits an allocation, so
its initial value is dead on arrival. String aliases (alias="other") are left
alone — those do emit an allocation carrying the value, so it is the compiler's
call.

To tell an omitted initial value from an explicit 0, Register and
FixedPointRegister now default initial_value to None and resolve it per
dtype (0 / 0.0). Array needed no change: its initial_value is a required
positional, so array contents always count as explicit.

one_to_all argument types — QCDL-14

components.py

QCDLModule.one_to_all(destinations, …) accepted only a Scope, so passing
scope.qcdl_modules — as the guide does — failed with an internal-sounding
AttributeError: 'list' object has no attribute 'qcdl_modules'. It now accepts a
Scope, a single QCDLModule, or a sequence of either, deduplicated and in
order. scope_id is carried only by a real Scope, since a bare sequence has no
identity. Anything else gets a message naming what was expected.

Result API — QCDL-12 / 13

results.py

  • get_counts() and get_memory() now default their register to
    get_measurements_register(), so a qubit that was never measured is left out
    rather than padded into the bitstring as _. A three-qubit circuit measuring
    two qubits used to report '1_'; it now reports '1'. The old behaviour is
    still reachable by passing register=get_default_register(...) explicitly, and
    the all-qubits register remains the fallback when nothing was measured at all.
  • YieldHandling.apply() no longer assumes string keys. Feeding it
    count_measurements(key_format=None) output raised
    TypeError: argument of type 'numpy.int64' is not a container; integer keys
    cannot hold an erasure, so they are now treated as splat-free. The
    all-erasures fallback had the same assumption one line down.
  • apply({}) raises ValueError instead of leaking StopIteration, which
    silently terminates any generator it is raised in. An empty counts dict is a
    natural output of get_counts(post_select=True).

Namespace and missing operations — QCDL-25 / 26 / 23

operations.py

  • Added __all__, so from dwave.gate.qcdl.operations import * brings in the
    operations and AngleType only, rather than also binding np, Any,
    Sequence, TypeAlias and the implementations module. Dropped the unused
    Sequence import.
  • Added reset(qubit). It was taught in the guide as q0.reset(), which worked
    only through __getattr__, so it had no importable function, signature or
    docstring. The operation emits a byte-identical statement to q0.reset(),
    asserted by a test, so it is an alias for the documented behaviour rather than
    a variant.
  • Added sxdg(qubit), one of the three Qiskit-parity gaps in QCDL-23. It emits
    the op name directly, matching sdg / tdg, since sxdg is a real
    QuantumCircuit method and the transpiler is Qiskit-based.

Documentation

docs/workflow.rst

  • The reset examples now use the importable reset operation. One of them also
    imported initialize and never used it.
  • detect_erasure_example(q) declared a parameter named q, which can never
    receive a qubit. Renamed to q0. It would have raised under the new QCDL-03
    check; it survived because the example is defined and never called.
  • The all_to_all example (and the matching docstring in components.py)
    re-declared a register name to get a single-qubit view of shared memory. That
    is exactly what alias=True is for, so both now use it. Semantics are
    unchanged — the aliased register still writes to q0 only, without mirroring.

Behaviour changes reviewers should know about

Several things that previously succeeded now raise. All of them were reported as
defects, but they are visible:

  1. @qcdl(0), @qcdl(-1), @qcdl(2.5), bare @qcdlQCDLUserError.
  2. @qcdl(N) with a signature that does not name all N qubits → QCDLUserError.
  3. A two-qubit gate given the same qubit twice → QCDLUserError.
  4. A non-qubit passed to a gate → QCDLUserError, not AttributeError.
  5. A duplicate register name → QCDLUserError.
  6. alias=True or ignore_reallocation=True together with an initial_value
    QCDLUserError.
  7. Register(None, name="r") used to raise; None now means "use the default".
  8. get_counts() / get_memory() bitstrings may be narrower than before, since
    unmeasured qubits are dropped.
  9. from dwave.gate.qcdl.operations import * no longer binds np,
    implementations, or the typing helpers.

Testing

pytest438 passing, up from 300 on main. New coverage in
test_qcdl_circuit.py, test_operations.py, test_registers.py,
test_scope.py and test_results.py for every fix above, including the
happy paths each new check could plausibly break.

make -C docs doctest — 102 testcode blocks execute. Two pre-existing
failures in implementations.py docstrings (they use Register without
importing it) are untouched and pass only because an earlier block in the same
document imports it.

One thing that is not verified: sxdg is checked for the QCDL it emits, but
not against a live solver's operation whitelist. It needs one integration smoke
test. If the service rejects it, the fallback is emitting rx(-π/2) the way
sy / sydg emit ry(±π/2), at the cost of a global phase.

The qcdl_alpha_tests suite is deliberately untouched. Six of its
xfail(strict=True) cases now XPASS — QCDL-03, 04, 06, 10, 12, 13 — which is the
report's own signal that a finding is fixed, and eleven tests that pinned the old
behaviour now fail on purpose. That repo and the report need a follow-up pass.

Follow-ups

Not in this PR, roughly in the order I would do them:

  • QCDL-09 (critical, silent wrong result) — reject mixed
    Register / FixedPointRegister arithmetic. OpsMixin._broadcasted_op is the
    single chokepoint; it needs RegisterExpression to carry a dtype so the check
    propagates through nested expressions. That would also fix the related latent
    bug where a float-valued expression exposes &, |, ^ and >>.
  • QCDL-16 (critical, silent wrong result) — the root cause is a unit mismatch
    in the service transpiler, but dwave-gate can stop the silent failure by
    refusing register-valued angles on any gate the transpiler rewrites (only rz
    and p are verified correct). Needs a decision on raise vs. warn.
  • QCDL-11 / 22 — export Result and YieldHandling from dwave.gate (the
    guide already cross-references dwave.gate.Result in five places, all broken)
    and add a documented constructor for
    Result.model_validate(orjson.loads(future.answer_data.read())).
  • QCDL-20 — the Simulator Configuration table is wrong on three of four rows
    and still carries a .. todo::.
  • QCDL-02 — an attribute typo puts a live QCDLStatementBridge in the
    payload, failing at encode time rather than at the statement.
  • QCDL-15 / 18 / 19 / 08 / 23 — documentation corrections in this repo.
  • QCDL-01 — validate instruction names with an explicit escape hatch. Needs
    the list of ops the compiler accepts, so it needs coordination.
  • QCDL-17 / 21 and the environment note are the transpiler and
    dwave-cloud-client respectively, not this repo.

Also noticed while working here, not from the report:

  • initialize() and barrier() with no arguments raise
    IndexError: tuple index out of range.
  • Most @qcdl examples in the guide define a circuit and never call it, so
    make docs doctest never executes the body. That is how the q-instead-of-q0
    parameter above survived; adding the call line to each example would catch the
    next one.
  • reno is a dev dependency but there is no releasenotes/ directory, so this
    PR adds no release note.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.02513% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.36%. Comparing base (206a2d6) to head (157ac9f).

Files with missing lines Patch % Lines
tests/test_qcdl_circuit.py 93.10% 6 Missing ⚠️
dwave/gate/qcdl/qcdl_circuit.py 90.69% 4 Missing ⚠️
dwave/gate/qcdl/components.py 96.42% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #71      +/-   ##
==========================================
+ Coverage   90.18%   91.36%   +1.17%     
==========================================
  Files          31       31              
  Lines        5317     5869     +552     
==========================================
+ Hits         4795     5362     +567     
+ Misses        522      507      -15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

1 participant