In a stabilizer circuit that doesn't allow classically-controlled Cliffords feedback, every outcome that is possible is equally likely. Also, when a measurement is random, it's always 50/50 random. So really you're just trying to figure out whether the result is possible at all and, if it is, how many measurements have random results.
You can use the tableau simulator to do this. Iterate over the circuit's instructions, simulating them one by one. When you encounter a measurement, use stim.TableauSimulator.peek_z
to determine if the result is random and then use stim.TableauSimulator.postselect_z
to force the correct measurement result (if possible).
If all postselections succeed, the result is possible and has probability 2**-num_measurements_that_were_random
.
A few caveats:
- This assumes the circuit contains no noise operations.
- You need to handle every kind of measurement that appears in the circuit (
M
, MY
, MRX
, etc).
- If the circuit contains resets, you need to put in more work, because they are collapsing operations which affect
peek_z
's result but without telling you how. Even if you make the effects visible by replacing each R
with MR
you have the problem that you don't know what the desired measurement results for those operations is. What you can do is instead replace each R
with a SWAP
that swaps the target qubit for a fresh unused ancilla qubit. This avoids a collapse being simulated by the simulator, so the expectations from peek_z
remain purely a function of the previous measurements.
Here's the version that works if no resets are present:
from typing import List
import stim
def outcome_probability(circuit: stim.Circuit, desired_results: List[bool]) -> float:
# Note: assumes no noise in circuit
# Note: assumes all measurements are single-qubit Z basis measuremet ('M').
assert len(desired_results) == circuit.num_measurements
random_results = 0
s = stim.TableauSimulator()
iter_desired = iter(desired_results)
for instruction in circuit.flattened():
if instruction.name == 'M':
for t in instruction.targets_copy():
expectaction = s.peek_z(t.value)
desired = next(iter_desired)
if expectaction == 0:
random_results += 1
elif expectaction != (-1 if desired else +1):
return 0 # Impossible!
s.postselect_z(t.value, desired_value=desired)
else:
assert instruction.name not in [
"R", "RX", "RY",
"MX", "MY", "MR", "MRX", "MRY",
"MPP", "X_ERROR", "Y_ERROR", "Z_ERROR",
"DEPOLARIZE1", "DEPOLARIZE2", "MPP", "E",
"PAULI_CHANNEL_1", "PAULI_CHANNEL_2"]
c = stim.Circuit()
c.append(instruction)
s.do_circuit(c)
return 2**-random_results # Possible.
c = stim.Circuit("""
H 0 2
CX 0 1
CX 2 1
M 0 1 2
""")
print(outcome_probability(c, desired_results=[False, False, True]))
# 0
print(outcome_probability(c, desired_results=[True, False, True]))
# 0.25
```