diff --git a/README.md b/README.md index a59f1ac..c433c20 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,8 @@ $result = Quantum::circuit() Appending a fragment that requires more qubits than the circuit has throws an `InvalidCircuitException`. +Measurement is final, as on Braket: listing a qubit twice in one `measure()` call, measuring a qubit a second time, or applying any gate to a qubit after it was measured throws an `InvalidCircuitException` while the circuit is being built, before any Python process is spawned. Put `measure()` last, or measure only the qubits you are done with. A fragment's own measurements do not count, since `append()` drops them: only the parent's measurements constrain what follows. + ### Adding a Gate Gate knowledge lives in a single metadata layer on each side of the bridge: the `GateType` / `GateShape` enums in `src/Circuit/` (PHP) and the `GATE_PARAMS` table in `bin/python/common.py` (Python). Adding a gate touches exactly five places: diff --git a/src/Circuit/CircuitBuilder.php b/src/Circuit/CircuitBuilder.php index 0d7f565..7860a3a 100644 --- a/src/Circuit/CircuitBuilder.php +++ b/src/Circuit/CircuitBuilder.php @@ -40,6 +40,12 @@ class CircuitBuilder private bool $hasMeasurement = false; + /** @var array Qubits already measured; Braket rejects any later instruction on them. */ + private array $measuredQubits = []; + + /** A measure-all was pushed: every qubit, including ones added later, counts as measured. */ + private bool $measuredAll = false; + private int $shots = 1000; final public function __construct( @@ -407,7 +413,10 @@ public function append(self|callable $fragment): static // can be shared directly without a toArray()/Gate::fromArray() round // trip that would re-serialize and re-validate every gate. foreach ($fragment->gates as $gate) { + // The fragment's measurements are dropped below, so only its gates + // are checked against what the parent has already measured. if (! $gate->isMeasurement()) { + $this->assertMeasurementOrder($gate, strtoupper($gate->type), $gate->qubitIndices()); $this->gates[] = $gate; } } @@ -635,7 +644,11 @@ public function validate(): static */ private function push(Gate $gate): static { - $this->validateTargets(strtoupper($gate->type), ...$gate->qubitIndices()); + $name = strtoupper($gate->type); + $indices = $gate->qubitIndices(); + + $this->validateTargets($name, ...$indices); + $this->assertMeasurementOrder($gate, $name, $indices); $this->gates[] = $gate; @@ -659,4 +672,60 @@ private function validateTargets(string $gate, int ...$qubits): void } } } + + /** + * Enforce strict measurement ordering during circuit construction: + * a qubit can be measured once, and nothing may act on it afterwards. + * + * Explicit measurements record their targets; a measure-all marks the + * whole circuit, including qubits added by a later qubits() call, since + * the driver expands it against the final qubit count. + * + * @param array $indices The gate's qubit indices, already range-checked. + * + * @throws InvalidCircuitException + */ + private function assertMeasurementOrder(Gate $gate, string $name, array $indices): void + { + if (! $gate->isMeasurement()) { + foreach ($indices as $qubit) { + if ($this->isMeasured($qubit)) { + throw InvalidCircuitException::qubitAlreadyMeasured($name, $qubit); + } + } + + return; + } + + if ($indices === []) { + if ($this->measuredAll || $this->measuredQubits !== []) { + throw InvalidCircuitException::qubitAlreadyMeasured($name, array_key_first($this->measuredQubits) ?? 0); + } + + $this->measuredAll = true; + + return; + } + + $seen = []; + + foreach ($indices as $qubit) { + if (isset($seen[$qubit])) { + throw InvalidCircuitException::repeatedMeasurementTarget($qubit); + } + + if ($this->isMeasured($qubit)) { + throw InvalidCircuitException::qubitAlreadyMeasured($name, $qubit); + } + + $seen[$qubit] = true; + } + + $this->measuredQubits += $seen; + } + + private function isMeasured(int $qubit): bool + { + return $this->measuredAll || isset($this->measuredQubits[$qubit]); + } } diff --git a/src/Exceptions/InvalidCircuitException.php b/src/Exceptions/InvalidCircuitException.php index ed110ef..077d63f 100644 --- a/src/Exceptions/InvalidCircuitException.php +++ b/src/Exceptions/InvalidCircuitException.php @@ -75,6 +75,26 @@ public static function appendedCircuitTooLarge(int $fragmentQubits, int $qubits) ); } + /** + * Create an exception for a measure() call that lists the same qubit twice. + */ + public static function repeatedMeasurementTarget(int $qubit): self + { + return new self( + "Qubit {$qubit} is listed more than once in the same measure() call; each qubit can be measured once." + ); + } + + /** + * Create an exception for an instruction applied to a qubit that was already measured. + */ + public static function qubitAlreadyMeasured(string $gate, int $qubit): self + { + return new self( + "Cannot apply {$gate} to qubit {$qubit}: it has already been measured. Quantum circuits cannot apply gates or measurements to a measured qubit, so move the measurement to the end of the circuit." + ); + } + /** * Create an exception for a measurement operation with an empty target list. */ diff --git a/tests/Unit/Circuit/CircuitBuilderTest.php b/tests/Unit/Circuit/CircuitBuilderTest.php index 6ac6459..58bacd2 100644 --- a/tests/Unit/Circuit/CircuitBuilderTest.php +++ b/tests/Unit/Circuit/CircuitBuilderTest.php @@ -340,6 +340,86 @@ expect($builder->qubitCount())->toBe(2); }); +it('rejects a measure() call that lists the same qubit twice', function () use (&$builder): void { + expect(fn () => $builder->qubits(2)->h(0)->measure([0, 0])) + ->toThrow(InvalidCircuitException::class, 'listed more than once'); +}); + +it('rejects measuring a qubit a second time', function (callable $second) use (&$builder): void { + $builder->qubits(2)->h(0)->measure(0); + + expect(fn () => $second($builder)) + ->toThrow(InvalidCircuitException::class, 'already been measured'); +})->with([ + 'explicit target again' => [fn (CircuitBuilder $b) => $b->measure(0)], + 'inside a wider measurement' => [fn (CircuitBuilder $b) => $b->measure([1, 0])], + 'measure all afterwards' => [fn (CircuitBuilder $b) => $b->measure()], +]); + +it('rejects a gate applied to a qubit after it was measured', function (callable $gate, string $name) use (&$builder): void { + $builder->qubits(2)->h(0)->measure(0); + + expect(fn () => $gate($builder)) + ->toThrow(InvalidCircuitException::class, "Cannot apply {$name} to qubit 0"); +})->with([ + 'single-qubit gate' => [fn (CircuitBuilder $b) => $b->h(0), 'H'], + 'two-qubit gate through its control' => [fn (CircuitBuilder $b) => $b->cnot(0, 1), 'CNOT'], + 'two-qubit gate through its target' => [fn (CircuitBuilder $b) => $b->cnot(1, 0), 'CNOT'], +]); + +it('rejects any gate after a measure-all', function () use (&$builder): void { + $builder->qubits(2)->h(0)->measure(); + + expect(fn () => $builder->x(1))->toThrow(InvalidCircuitException::class, 'already been measured'); +}); + +it('still allows gates on qubits that were not measured', function () use (&$builder): void { + $builder->qubits(3)->h(0)->measure(0)->h(1)->cnot(1, 2)->measure([1, 2]); + + expect($builder->toArray()['gates'])->toHaveCount(5); +}); + +it('rejects an appended fragment that touches a measured qubit', function () use (&$builder, &$device): void { + $builder->qubits(2)->h(0)->measure(0); + $fragment = (new CircuitBuilder($device))->qubits(2)->x(0); + + expect(fn () => $builder->append($fragment))->toThrow(InvalidCircuitException::class, 'already been measured'); +}); + +it('ignores the measurements of an appended fragment, which append() drops', function () use (&$builder, &$device): void { + $bell = (new CircuitBuilder($device))->qubits(2)->h(0)->cnot(0, 1)->measure(); + + $builder->qubits(2)->append($bell)->x(0)->measure(); + + expect($builder->toArray()['gates'])->toHaveCount(4); +}); + +it('treats qubits added after a measure-all as measured too', function () use (&$builder): void { + $builder->qubits(2)->h(0)->measure()->qubits(3); + + expect(fn () => $builder->h(2))->toThrow(InvalidCircuitException::class, 'already been measured'); +}); + +it('rejects a second measurement after a measure-all', function () use (&$builder): void { + $builder->qubits(2)->measure(); + + expect(fn () => $builder->measure(1))->toThrow(InvalidCircuitException::class, 'already been measured'); +}); + +it('fromArray rejects a definition that acts on a measured qubit', function () use (&$device): void { + $definition = [ + 'qubits' => 1, + 'gates' => [ + ['type' => 'measure', 'targets' => [0]], + ['type' => 'h', 'target' => 0], + ], + 'shots' => 10, + ]; + + expect(fn () => CircuitBuilder::fromArray($definition, $device)) + ->toThrow(InvalidCircuitException::class, 'already been measured'); +}); + it('measure with an empty array throws', function () use (&$builder): void { expect(fn () => $builder->qubits(2)->measure([])) ->toThrow(InvalidCircuitException::class);