Skip to content

Fix #34: reject gates and repeated measurements on measured qubits while building - #92

Open
corgab wants to merge 3 commits into
mainfrom
fix/34-measured-qubit-guards
Open

Fix #34: reject gates and repeated measurements on measured qubits while building#92
corgab wants to merge 3 commits into
mainfrom
fix/34-measured-qubit-guards

Conversation

@corgab

@corgab corgab commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Braket's Circuit rejects a measure() that lists the same qubit twice (cannot repeat qubit(s) 0 in the same measurement) and any instruction applied to a qubit that was already measured (cannot apply instruction to measured qubits). CircuitBuilder only checked that indices were in range, so ->measure([0, 0]) and ->measure(0)->h(0) built fine in PHP and failed one subprocess later as a generic QuantumExecutionException::fromPythonError(), instead of the InvalidCircuitException every other shape mistake produces.

Plan

Track the measured qubits in the builder and check every pushed gate against them, so both mistakes fail at build time with a specific message, including for fragments added with append() and definitions rebuilt with fromArray(), which also go through the same check. Respect the two existing contracts: append() drops the fragment's measurements, and qubits() may grow the circuit after a measure-all.

Changes

  • CircuitBuilder: a $measuredQubits set plus a $measuredAll flag, and a private assertMeasurementOrder(Gate, string $name, array $indices) called from push() (with the name and indices resolved once for both the range check and this one) and, for non-measurement gates only, from the append() loop. An explicit measurement records its targets; a measure-all sets the flag, so qubits added by a later qubits() call count as measured too, matching how common.py expands targets: null against the final qubit count. A repeated target within one call throws repeatedMeasurementTarget(); a target already measured, a second measure-all, or a gate touching a measured qubit throws qubitAlreadyMeasured().
  • InvalidCircuitException::repeatedMeasurementTarget(int $qubit) and qubitAlreadyMeasured(string $gate, int $qubit): messages name the qubit and the gate, and say to move the measurement to the end.
  • README: one paragraph in the circuit section stating that measurement is final, what throws, and that a fragment's own measurements do not count because append() drops them.

No config change, no new dependencies, no change for circuits that already ran on Braket (they could not contain either pattern).

Tests

CircuitBuilderTest: duplicate target in one measure(); measuring again (same target, inside a wider measurement, measure-all afterwards); a single-qubit gate, and a two-qubit gate through its control or its target, on a measured qubit; any gate after a measure-all; gates on unmeasured qubits still allowed; an appended fragment touching a measured qubit; appending a fragment that ends in measure() and then measuring the parent still works; a qubit added after a measure-all is measured too; a second measurement after a measure-all; a fromArray() definition acting on a measured qubit. All the rejecting cases built without error on main.

Local: pint passed, pest 805 tests passed.

Closes #34

…building

Braket refuses a measure() that lists the same qubit twice and any
instruction applied to a qubit that was already measured, but the builder
only checked that indices were in range, so both mistakes surfaced from
the Python subprocess as a generic execution error. CircuitBuilder now
tracks the measured qubits (every qubit for a measure-all) and throws
InvalidCircuitException at build time, including for appended fragments
and definitions rebuilt with fromArray(), with messages naming the gate
and the qubit.

Closes #34
…guard

append() drops a fragment's measurements by contract, so they must not
mark the parent's qubits as measured; only the fragment's gates are checked
against the parent's own measurements. A measure-all now sets a flag that
covers qubits added by a later qubits() call, matching how the Python side
expands it against the final qubit count, and push() resolves the gate
name and indices once for both checks.
PHPStan reads Gate::qubitIndices() as array<int>, so the guard's parameter
uses the same shape instead of list<int>.
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.

[circuit] Duplicate measure() targets and post-measurement gates only fail inside the Python subprocess

1 participant