View on QuantumAI | Run in Google Colab | View source on GitHub | Download notebook |
try:
import cirq
except ImportError:
print("installing cirq...")
!pip install --quiet cirq
import cirq
print("installed cirq.")
While some quantum algorithms can be defined entirely at the quantum level, there are many others (notably including teleportation and error correction) which rely on classical measurement results from one part of the algorithm to control operations in a later section.
To represent this, Cirq provides the ClassicallyControlledOperation
. Following the pattern of controlled operations, a classically-controlled version of any Operation
can be constructed by calling its with_classical_controls
method with the control condition(s).
Basic conditions
In the example below, H
will only be applied to q1
if the previous measurement "a" returns a 1. More generally, providing some string "cond"
to with_classical_controls
creates a cirq.ClassicallyControlledOperation
with a cirq.KeyCondition
whose key is "cond"
. A KeyCondition
will only trigger, and apply the operation it controls, if a preceding measurement with the same key measured one or more qubits in the \(|1\rangle\) state.
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
cirq.H(q0),
cirq.measure(q0, key='a'),
cirq.H(q1).with_classical_controls('a'),
cirq.measure(q1, key='b'),
)
print(circuit)
print(cirq.Simulator().run(circuit, repetitions=1000).histogram(key='b'))
0: ───H───M──────────────── ║ 1: ───────╫───H───M('b')─── ║ ║ a: ═══════@═══^════════════ Counter({0: 750, 1: 250})
The results from running the circuit on the simulator match expectation. H
applied to qubit q0
means that qubit will be \(|1\rangle\) half of the time on average. When H
is then applied to qubit q1
, (half of the time), q1
will measure \(|1\rangle\) a quarter of the time and \(|0\rangle\) three-quarters of the time.
Using just these conditions, we can construct the quantum teleportation circuit:
# Teleports `_message` from Alice to Bob.
alice = cirq.NamedQubit('alice')
bob = cirq.NamedQubit('bob')
message = cirq.NamedQubit('_message')
message_circuit = cirq.Circuit(
# Create the message.
cirq.X(message) ** 0.371,
cirq.Y(message) ** 0.882,
)
teleport_circuit = cirq.Circuit(
# Create Bell state to be shared between Alice and Bob.
cirq.H(alice),
cirq.CNOT(alice, bob),
# Prepare message circuit
message_circuit,
# Bell measurement of the message and Alice's entangled qubit.
cirq.CNOT(message, alice),
cirq.H(message),
cirq.measure(message, key='M'),
cirq.measure(alice, key='A'),
# Uses the two classical bits from the Bell measurement to recover the
# original quantum message on Bob's entangled qubit.
cirq.X(bob).with_classical_controls('A'),
cirq.Z(bob).with_classical_controls('M'),
)
print(circuit)
# Simulate the message and teleport circuits for Bloch vectors to compare
# the state of the teleported qubit before and after teleportation.
sim = cirq.Simulator()
message_bloch_vector = cirq.bloch_vector_from_state_vector(
sim.simulate(message_circuit).final_state_vector, index=0
)
teleport_bloch_vector = cirq.bloch_vector_from_state_vector(
sim.simulate(teleport_circuit).final_state_vector, index=2
)
print(f"Message Qubit State: {message_bloch_vector}")
print(f"Teleported Bob's Qubit state: {teleport_bloch_vector}")
0: ───H───M──────────────── ║ 1: ───────╫───H───M('b')─── ║ ║ a: ═══════@═══^════════════ Message Qubit State: [ 0.14283165 -0.91899776 -0.36748084] Teleported Bob's Qubit state: [ 0.14283162 -0.91899776 -0.3674809 ]
This example separately simulated the message qubit after its construction, and Bob's qubit after teleportation of the message. The fact that the Bloch vectors of each respective qubit are the same indicate that the circuit successfully teleported the message qubit's state onto Bob's qubit.
Sympy conditions
Cirq also supports more complex control conditions: providing some sympy
expression "expr"
to with_classical_controls
creates a ClassicallyControlledOperation
with a SympyCondition
. That condition will only trigger if "expr"
evaluates to a "truthy" value (bool(expr) == True
), and uses measurement results to resolve any variables in the expression.
In this example, X
will only be applied to q2
if a == b
; in other words, \(|q_0q_1\rangle\) must be either \(|00\rangle\) or \(|11\rangle\). This is verifiable with the simulated result data, where the c
measurement key for qubit q2
is always 1
when a
and b
are 00
or 11
, and 0
otherwise.
import sympy
q0, q1, q2 = cirq.LineQubit.range(3)
a, b = sympy.symbols('a b')
sympy_cond = sympy.Eq(a, b)
circuit = cirq.Circuit(
cirq.H.on_each(q0, q1),
cirq.measure(q0, key='a'),
cirq.measure(q1, key='b'),
cirq.X(q2).with_classical_controls(sympy_cond),
cirq.measure(q2, key='c'),
)
print(circuit)
results = cirq.Simulator(seed=2).run(circuit, repetitions=8)
print(results.data)
┌──┐ 0: ───H────M───────────────────────────────────────── ║ 1: ───H────╫M──────────────────────────────────────── ║║ 2: ────────╫╫────X(conditions=[Eq(a, b)])───M('c')─── ║║ ║ a: ════════@╬════^═══════════════════════════════════ ║ ║ b: ═════════@════^═══════════════════════════════════ └──┘ a b c 0 0 0 1 1 0 0 1 2 0 1 0 3 0 1 0 4 0 1 0 5 1 1 1 6 1 0 0 7 0 0 1
Combining conditions
Multiple conditions of either type can be specified to with_classical_controls
, in which case the resulting ClassicallyControlledOperation
will only trigger if all conditions trigger. Similarly, calling with_classical_controls
on an existing ClassicallyControlledOperation
will require all new and pre-existing conditions to trigger for the operation to trigger.
q0, q1, q2, q3, q4 = cirq.LineQubit.range(5)
a = sympy.symbols('a')
sympy_cond = sympy.Eq(a, 0)
circuit = cirq.Circuit(
cirq.H.on_each(q0, q1, q2),
cirq.measure(q0, q1, key='a'),
cirq.measure(q2, key='b'),
cirq.X(q3).with_classical_controls('b', sympy_cond),
cirq.X(q4).with_classical_controls('b').with_classical_controls(sympy_cond),
cirq.measure(q3, key='c'),
cirq.measure(q4, key='d'),
)
print(circuit)
results = cirq.Simulator(seed=1).run(circuit, repetitions=8)
print(results.data)
┌──┐ ┌──────────────────────────────────────────────────────┐ 0: ───H────M───────────────────────────────────────────────────────────────────────── ║ 1: ───H────M───────────────────────────────────────────────────────────────────────── ║ 2: ───H────╫M──────────────────────────────────────────────────────────────────────── ║║ 3: ────────╫╫─────X(conditions=[b, Eq(a, 0)])───────────────────────────────M('c')─── ║║ ║ 4: ────────╫╫─────╫──────────────────────────X(conditions=[Eq(a, 0), b])────M('d')─── ║║ ║ ║ a: ════════@╬═════^══════════════════════════^═══════════════════════════════════════ ║ ║ ║ b: ═════════@═════^══════════════════════════^═══════════════════════════════════════ └──┘ └──────────────────────────────────────────────────────┘ a b c d 0 1 1 0 0 1 0 0 0 0 2 1 1 0 0 3 0 1 1 1 4 1 1 0 0 5 3 1 0 0 6 3 1 0 0 7 0 1 1 1
First, remember that the value of a measurement key for multiple qubits will be an integer representative of the bit string of those qubits' measurements. You can see this in the data for a
, the measurement key for both q0
and q1
, which has values in the range [0, 3]
. The sympy condition Eq(a, 0)
will then only trigger when both of those qubits individually measure 0
.
This means that X(q3).with_classical_controls('b', sympy_cond)
only triggers when b
's qubit q2
measures 1
and a = 0
is true (q0
and q1
measure 0
). This is consistent with the simulated results, for both c
(q3
's key) and d
(q4
's key).
Finally, the fact that c
and d
are always identical serves as a reminder that chaining multiple calls of with_classical_controls()
together is equivalent to calling it once with multiple arguments.
Variable scope
When used with cirq.CircuitOperation
, classically controlled operations will be resolved using local repetition IDs, if any. This is the only way to create a non-global variable scope within a circuit. A simple example of this is shown below, where the controls inside and outside a subcircuit rely on measurements in their respective scopes:
q0 = cirq.LineQubit(0)
subcircuit = cirq.FrozenCircuit(cirq.measure(q0, key='a'), cirq.X(q0).with_classical_controls('a'))
circuit = cirq.Circuit(
cirq.measure(q0, key='a'),
cirq.CircuitOperation(subcircuit, repetitions=2),
cirq.X(q0).with_classical_controls('a'),
)
print("Original Circuit")
print(circuit)
print("Circuit with nested circuit unrolled.")
print(cirq.CircuitOperation(cirq.FrozenCircuit(circuit)).mapped_circuit(deep=True))
Original Circuit [ 0: ───M───X─── ] 0: ───M───[ ║ ║ ]────────────X─── ║ [ a: ═══@═══^═══ ](loops=2) ║ ║ ║ a: ═══@═════════════════════════════════^═══ Circuit with nested circuit unrolled. 0: ─────M───M───X───M───X───X─── ║ ║ ║ ║ ║ ║ 0:a: ═══╬═══@═══^═══╬═══╬═══╬═══ ║ ║ ║ ║ 1:a: ═══╬═══════════@═══^═══╬═══ ║ ║ a: ═════@═══════════════════^═══
The measurement key a
is present both in the outer circuit and the FrozenCircuit
nested within it, but these two keys are different due to their different scopes. After unrolling the inner circuit twice, these inner a
s get prefixed by the repetition number and becomes new, separate measurement keys, 0:a
and 1:a
, that don't interact with each other or the original a
.
More complex scoping behavior is described in the classically controlled operation tests.
Using with transformers
Cirq transformers are aware of classical control and will avoid changes which move a control before its corresponding measurement. Additionally, for some simple cases the defer_measurements
transformer can convert a classically-controlled circuit into a purely-quantum circuit:
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.measure(q0, key='a'), cirq.X(q1).with_classical_controls('a'), cirq.measure(q1, key='b')
)
deferred = cirq.defer_measurements(circuit)
print("Original circuit:")
print(circuit)
print("Measurement deferred:")
print(deferred)
Original circuit: 0: ───M──────────────── ║ 1: ───╫───X───M('b')─── ║ ║ a: ═══@═══^════════════ Measurement deferred: 0: ───────────────────@──────────────── │ 1: ───────────────────┼───X───M('b')─── │ │ M('a[0]', q=q(0)): ───X───@───M('a')───
Compatibility
The Cirq built-in simulators provide support for classical control, but caution should be exercised when exporting these circuits to other environments. ClassicallyControlledOperation
is fundamentally different from other operations in that it requires access to the measurement results, and simulators or hardware that do not explicitly support this will not be able to run ClassicallyControlledOperation
s.