Quantum Virtual Engine

View on QuantumAI Run in Google Colab View source on GitHub Download notebook

Cirq provides the Quantum Virtual Machine, which consists of two components:

  • The Quantum Virtual Engine: A class that implements the same interface as cirq_google.Engine, allowing you to simulate circuits with the same software interface that the real hardware uses.
  • Realistic noise models that mimic the behavior of real quantum hardware.

This tutorial covers the former of the two components, the Quantum Virtual Engine, and how to run circuits on existing and custom virtual processor models.

Setup

try:
    import cirq
except ImportError:
    print("installing cirq...")
    !pip install --quiet cirq
    print("installed cirq.")
    import cirq

import cirq_google
import sympy

Communication with real quantum hardware in Cirq is done through the cirq_google.Engine class. Each Engine can contain multiple quantum processors, and the Engine class provides functions to run circuits and manage jobs sent to those processors. The Virtual Engine in Cirq is an instance of the class cirq_google.SimulatedLocalEngine that runs circuits on the built-in Cirq Simulator instead of on hardware, but uses the same interface as Engine. This is useful for testing your circuit and code pipeline before running on actual hardware, and can be used as a substitute when the real hardware is not available.

The interface implemented by both cirq_google.Engine and cirq_google.SimulatedLocalEngine is called cirq_google.AbstractEngine, and defines the various functions and types involved with using either option. When writing functions of your own, this interface enables you to seamlessly support simulated and real-hardware versions of the Engine interface.

Instantiate a virtual Engine

The easiest way to create a cirq_google.SimulatedLocalEngine is to make one from one or more processor templates. Example processor device specifications can be found in the devices/specifications folder of cirq_google in the Cirq Github repository. These device specifications closely match previous versions of Google quantum hardware, and can serve as templates for processors in a SimulatedLocalEngine. When Google hardware becomes publicly available again in the future, it will have device specifications like these that differ in details, but not in format.

You can create a cirq_google.SimulatedLocalEngine that includes these example device specifications using cirq_google.engine.create_noiseless_virtual_engine_from_latest_templates(). For example:

engine = cirq_google.engine.create_noiseless_virtual_engine_from_latest_templates()

You can then use this Engine object to perform operations as if it included real hardware. However, all interactions will be local and mocked with these example processors. Program execution will be done by the cirq Simulator.

For instance, you can list the processors and their device layouts, which are the same as those specified in the devices/specification folder:

for proc in engine.list_processors():
    print(proc.processor_id)
    print('-----------------')
    print(proc.get_device())
    print('\n\n\n')
rainbow
-----------------
                  (3, 2)
                  │
                  │
         (4, 1)───(4, 2)───(4, 3)
         │        │        │
         │        │        │
(5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)
         │        │        │        │
         │        │        │        │
         (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)
                  │        │        │        │
                  │        │        │        │
                  (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
                           │        │        │
                           │        │        │
                           (8, 3)───(8, 4)───(8, 5)
                                    │
                                    │
                                    (9, 4)




weber
-----------------
                                             (0, 5)───(0, 6)
                                             │        │
                                             │        │
                                    (1, 4)───(1, 5)───(1, 6)───(1, 7)
                                    │        │        │        │
                                    │        │        │        │
                                    (2, 4)───(2, 5)───(2, 6)───(2, 7)───(2, 8)
                                    │        │        │        │        │
                                    │        │        │        │        │
                  (3, 2)───(3, 3)───(3, 4)───(3, 5)───(3, 6)───(3, 7)───(3, 8)───(3, 9)
                  │        │        │        │        │        │        │        │
                  │        │        │        │        │        │        │        │
         (4, 1)───(4, 2)───(4, 3)───(4, 4)───(4, 5)───(4, 6)───(4, 7)───(4, 8)───(4, 9)
         │        │        │        │        │        │        │        │
         │        │        │        │        │        │        │        │
(5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)───(5, 5)───(5, 6)───(5, 7)───(5, 8)
         │        │        │        │        │        │        │
         │        │        │        │        │        │        │
         (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)───(6, 6)───(6, 7)
                  │        │        │        │        │
                  │        │        │        │        │
                  (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
                           │        │        │
                           │        │        │
                           (8, 3)───(8, 4)───(8, 5)
                                    │
                                    │
                                    (9, 4)

Run circuits and sweeps

After creating the SimulatedLocalEngine, you can use any function that you might use with a normal Engine that has real quantum processors in it. Most importantly, this includes the ability to run circuits.

# Choose one of the (simulated) processors to run on.
weber = engine.get_processor('weber')
sampler = weber.get_sampler()

# Run a simple circuit for ten repetitions
result = sampler.run(cirq.Circuit(cirq.measure(cirq.GridQubit(7, 2))), repetitions=10)
print(result)
q(7, 2)=0000000000

Note that, even though this is a simulated processor device, there are still device constraints that must be met by a circit in order for it to be executed. For example, the gates and qubits used by the circuit must be supported by the device:

# Weber does not have a (7, 1) qubit.
try:
    sampler.run(cirq.Circuit(cirq.measure(cirq.GridQubit(7, 1))), repetitions=10)
except ValueError as e:
    print(e)

# Weber does not support the H gate on qubit (7, 2).
try:
    sampler.run(
        cirq.Circuit(cirq.H(cirq.GridQubit(7, 2)), cirq.measure(cirq.GridQubit(7, 2))),
        repetitions=10,
    )
except ValueError as e:
    print(e)
Qubit not on device: cirq.GridQubit(7, 1).
Cannot serialize op cirq.H(cirq.GridQubit(7, 2)) of type <class 'cirq.ops.common_gates.HPowGate'>

You can also run Parameter Sweeps with the run_sweep function, which returns a cirq.Job-type object instead of a Result. This way, jobs can be prepared and run asynchronously. When running a parameter sweep over many parameter options, or with particularly large circuits, it can be useful to set the job running and return for the results later, with the ability to check job execution status in between.

qubit = cirq.GridQubit(7, 2)
circuit = cirq.Circuit(cirq.X(qubit) ** sympy.Symbol('t'), cirq.measure(qubit, key='m'))
job = weber.run_sweep(circuit, params=cirq.Linspace('t', 0, 2, 20), repetitions=1000)
print(f'job is type {type(job)}')
print(f'job has id {job.id()} and status {job.execution_status()}')
print('')

print('Now executing results!')
results = job.results()
print('')
print(f'job has id {job.id()} and status {job.execution_status()}')

print('')
print('Results:')
for result in results:
    print(result.histogram(key='m'))
job is type <class 'cirq_google.engine.simulated_local_job.SimulatedLocalJob'>
job has id projects/fake_project/processors/weber/job/8 and status 1

Now executing results!

job has id projects/fake_project/processors/weber/job/8 and status 5

Results:
Counter({0: 1000})
Counter({0: 973, 1: 27})
Counter({0: 885, 1: 115})
Counter({0: 777, 1: 223})
Counter({0: 613, 1: 387})
Counter({1: 569, 0: 431})
Counter({1: 709, 0: 291})
Counter({1: 840, 0: 160})
Counter({1: 948, 0: 52})
Counter({1: 991, 0: 9})
Counter({1: 990, 0: 10})
Counter({1: 942, 0: 58})
Counter({1: 843, 0: 157})
Counter({1: 691, 0: 309})
Counter({1: 548, 0: 452})
Counter({0: 614, 1: 386})
Counter({0: 782, 1: 218})
Counter({0: 889, 1: 111})
Counter({0: 976, 1: 24})
Counter({0: 1000})

Reservations and scheduling

Other functions are available to Engine classes that are part of using the Engine as a service. These include reservations, scheduling, downtime, and others. Thes functions are also available with the virtual processors, though all of them will generally succeed since there are no other users using the virtual service.

print(f'Next expected downtime: {weber.expected_down_time()}')
print(f'Next expected recovery: {weber.expected_recovery_time()}')

# Creating two example reservations
import datetime

now = datetime.datetime.now()
hour = datetime.timedelta(hours=1)
try:
    weber.create_reservation(start_time=now, end_time=now + hour)
    weber.create_reservation(
        start_time=now + 2 * hour,
        end_time=now + 3 * hour,
        whitelisted_users=['mysterious_fake_user@nonexistentwebsite.domain'],
    )
except ValueError as e:
    # If you re-run this cell, it will note that you already have a reservation
    print('Cannot reserve time, did you already reserve it?  Error:')
    print(e)

print('')
print('Reservations:')
print('---------------')
print(f'{weber.list_reservations()}')
Next expected downtime: None
Next expected recovery: None

Reservations:
---------------
[name: "projects/fake_project/processors/weber/reservation/9"
start_time {
  seconds: 1684404825
}
end_time {
  seconds: 1684408425
}
, name: "projects/fake_project/processors/weber/reservation/10"
start_time {
  seconds: 1684412025
}
end_time {
  seconds: 1684415625
}
whitelisted_users: "mysterious_fake_user@nonexistentwebsite.domain"
]

The processor also comes with a stock calibration metric report. By default, all of the error values are zero.

print('Calibrations:')
print('---------------')
calibration = weber.list_calibrations()[0]
print(f"Calibration metrics: \n    {list(calibration.keys())}")
# Example calibration data
for metric in ["single_qubit_p00_error", "two_qubit_sycamore_gate_xeb_average_error_per_cycle"]:
    print(metric)
    data = calibration[metric]
    # Only print the first couple qubits/qubit pairs
    for key in list(data.keys())[:3]:
        print(f'   {key}: {data[key]}')
Calibrations:
---------------
Calibration metrics: 
    ['single_qubit_p00_error', 'single_qubit_p11_error', 'single_qubit_readout_separation_error', 'parallel_p00_error', 'parallel_p11_error', 'single_qubit_rb_average_error_per_gate', 'single_qubit_rb_incoherent_error_per_gate', 'single_qubit_rb_pauli_error_per_gate', 'two_qubit_sycamore_gate_xeb_average_error_per_cycle', 'two_qubit_sycamore_gate_xeb_pauli_error_per_cycle', 'two_qubit_sycamore_gate_xeb_incoherent_error_per_cycle', 'two_qubit_sqrt_iswap_gate_xeb_average_error_per_cycle', 'two_qubit_sqrt_iswap_gate_xeb_pauli_error_per_cycle', 'two_qubit_sqrt_iswap_gate_xeb_incoherent_error_per_cycle', 'two_qubit_parallel_sycamore_gate_xeb_average_error_per_cycle', 'two_qubit_parallel_sycamore_gate_xeb_pauli_error_per_cycle', 'two_qubit_parallel_sycamore_gate_xeb_incoherent_error_per_cycle', 'two_qubit_parallel_sqrt_iswap_gate_xeb_average_error_per_cycle', 'two_qubit_parallel_sqrt_iswap_gate_xeb_pauli_error_per_cycle', 'two_qubit_parallel_sqrt_iswap_gate_xeb_incoherent_error_per_cycle', 'single_qubit_idle_t1_micros']
single_qubit_p00_error
   (cirq.GridQubit(5, 7),): [0.0]
   (cirq.GridQubit(9, 4),): [0.0]
   (cirq.GridQubit(6, 4),): [0.0]
two_qubit_sycamore_gate_xeb_average_error_per_cycle
   (cirq.GridQubit(0, 5), cirq.GridQubit(0, 6)): [0.0]
   (cirq.GridQubit(0, 5), cirq.GridQubit(1, 5)): [0.0]
   (cirq.GridQubit(0, 6), cirq.GridQubit(1, 6)): [0.0]

Create a custom processor from a device

You can also create processors to mimic other devices as needed. Each of these classes is customizable and can be modified to suit your simulation needs.

You can create processors from existing devices, like cirq_google.Sycamore, with cirq_google.engine.create_noiseless_virtual_engine_from_device:

sycamore_engine = cirq_google.engine.create_noiseless_virtual_engine_from_device(
    'sycamore', cirq_google.Sycamore
)

# Note that the previous function creates an engine with just one processor
print([proc.processor_id for proc in sycamore_engine.list_processors()])
print(sycamore_engine.get_processor('sycamore').get_device())
['sycamore']
                                             (0, 5)───(0, 6)
                                             │        │
                                             │        │
                                    (1, 4)───(1, 5)───(1, 6)───(1, 7)
                                    │        │        │        │
                                    │        │        │        │
                           (2, 3)───(2, 4)───(2, 5)───(2, 6)───(2, 7)───(2, 8)
                           │        │        │        │        │        │
                           │        │        │        │        │        │
                  (3, 2)───(3, 3)───(3, 4)───(3, 5)───(3, 6)───(3, 7)───(3, 8)───(3, 9)
                  │        │        │        │        │        │        │        │
                  │        │        │        │        │        │        │        │
         (4, 1)───(4, 2)───(4, 3)───(4, 4)───(4, 5)───(4, 6)───(4, 7)───(4, 8)───(4, 9)
         │        │        │        │        │        │        │        │
         │        │        │        │        │        │        │        │
(5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)───(5, 5)───(5, 6)───(5, 7)───(5, 8)
         │        │        │        │        │        │        │
         │        │        │        │        │        │        │
         (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)───(6, 6)───(6, 7)
                  │        │        │        │        │
                  │        │        │        │        │
                  (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
                           │        │        │
                           │        │        │
                           (8, 3)───(8, 4)───(8, 5)
                                    │
                                    │
                                    (9, 4)

Create a custom processor from a specification

You can also create virtual engines from device specifications written in the Protocol Buffer structured-data file format. This allows for detailed custom device creation, in the case where you want to see how a slightly modified existing device, or a completely new device, would work in Cirq.

The previous specification files mentioned in the devices/specifications in the Cirq repository are already in this file format. The details of this format are subject to change as Cirq is updated, but it is designed to be human-readable. If you want to work with a very custom device, the best place to start is by inspecting one of these files, but be aware that the format may change without notice.

import importlib
from cirq_google.devices import specifications

# Get the processor identifier and file location from MOST_RECENT_TEMPLATES.
processor_id, template_name = next(
    iter(cirq_google.engine.virtual_engine_factory.MOST_RECENT_TEMPLATES.items())
)
# Read the protobuf template.
device_str = importlib.resources.read_text(specifications, template_name)
# Print just the first 10 lines of the very long protobuf specification.
print(f'Processor: {processor_id}')
print('\n'.join(device_str.splitlines()[:10]))
print('...')
Processor: rainbow
valid_qubits: "3_2"
valid_qubits: "4_1"
valid_qubits: "4_2"
valid_qubits: "4_3"
valid_qubits: "5_0"
valid_qubits: "5_1"
valid_qubits: "5_2"
valid_qubits: "5_3"
valid_qubits: "5_4"
valid_qubits: "6_1"
...

In order to use this specification protobuf file string, parse it with google.protobuf.text_format and create the SimulatedLocalEngine with cirq_google.engine.create_noiseless_virtual_engine_from_proto.

import google.protobuf.text_format as text_format

# Import the spec.
device_spec = cirq_google.api.v2.device_pb2.DeviceSpecification()
text_format.Parse(device_str, device_spec)
four_engine = cirq_google.engine.create_noiseless_virtual_engine_from_proto(
    processor_id, device_spec
)
# Prepare a sampler.
print([proc.processor_id for proc in four_engine.list_processors()])
processor = four_engine.get_processor(processor_id)
print(processor.get_device())
sampler = processor.get_sampler()

q1_1 = cirq.GridQubit(1, 1)
q1_2 = cirq.GridQubit(1, 2)
q2_1 = cirq.GridQubit(2, 1)
# Run a circuit with one each of Z, CZ, Measure, and CircuitOperation.
circuit = cirq.Circuit(
    cirq.CircuitOperation(cirq.FrozenCircuit(cirq.Z(q2_1), cirq.CZ(q1_1, q1_2))),
    cirq.measure(q1_1),
    cirq.measure(q2_1),
)
print('results', '\n')
try:
    print(sampler.run(circuit))
except ValueError as e:
    print(e)
['rainbow']
                  (3, 2)
                  │
                  │
         (4, 1)───(4, 2)───(4, 3)
         │        │        │
         │        │        │
(5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)
         │        │        │        │
         │        │        │        │
         (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)
                  │        │        │        │
                  │        │        │        │
                  (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
                           │        │        │
                           │        │        │
                           (8, 3)───(8, 4)───(8, 5)
                                    │
                                    │
                                    (9, 4)
results 

Qubit not on device: cirq.GridQubit(1, 1).

Summary

Cirq provides the cirq.SimulatedLocalEngine. which allows you to run circuits on the Cirq Simulator through the same interface as the cirq.Engine object, which is used for running on real quantum hardware. This is useful both as a preparation step before running on real quantum hardware, and as a substitute when real hardware is unavailable.

As presented in this page, the virtual Engine is completely noiseless. In order to learn about using the virtual Engine with noise models, including realistic noise models which closely mimic actual hardware, see the Quantum Virtual Machine page.