Classical control

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: 772, 1: 228})

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.14283168 -0.91899776 -0.3674809 ]
Teleported Bob's Qubit state: [ 0.14283162 -0.9189975  -0.36748075]

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 as 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', 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 ClassicallyControlledOperations.