Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
71 changes: 70 additions & 1 deletion src/Circuit/CircuitBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ class CircuitBuilder

private bool $hasMeasurement = false;

/** @var array<int, true> 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(
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;

Expand All @@ -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<int> $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]);
}
}
20 changes: 20 additions & 0 deletions src/Exceptions/InvalidCircuitException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
80 changes: 80 additions & 0 deletions tests/Unit/Circuit/CircuitBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down