User guide#

This guide serves as an introduction to the main features of Cirq on IQM. You are encouraged to run the demonstrated code snippets and check the output yourself.

Installation#

The recommended way is to install the distribution package cirq-iqm directly from the Python Package Index (PyPI):

$ pip install cirq-iqm

After installation Cirq on IQM can be imported in your Python code as follows:

from iqm import cirq_iqm

IQM’s quantum devices#

Cirq on IQM provides descriptions of IQM’s quantum architectures using the IQMDevice class, which is a subclass of cirq.devices.Device and implements general functionality relevant to all IQM devices. The native gates and connectivity of the architecture are available in the IQMDeviceMetadata object returned by the IQMDevice.metadata property. It is possible to use the IQMDevice class directly, but certain devices with predefined metadata are also available as subclasses of IQMDevice. As an example, let us import the class Adonis, which describes IQM’s five-qubit architecture, and view some of its properties contained in its metadata property:

from iqm.cirq_iqm import Adonis

adonis = Adonis()

print(adonis.metadata.qubit_set)
print(adonis.metadata.gateset)
print(adonis.metadata.nx_graph)

IQM devices use cirq.NamedQubit to represent their qubits. The names of the qubits consist of a prefix followed by a numeric index, so we have qubit names like QB1, QB2, etc. Note that we use 1-based indexing. You can get the list of the qubits in a particular device by accessing the qubits attribute of a corresponding IQMDevice instance:

print(adonis.qubits)

Constructing circuits#

There are two main ways of constructing cirq.Circuit instances for IQM devices:

  1. Create a Circuit instance using arbitrary qubit names and types.

  2. Create a Circuit from an OpenQASM 2.0 program. The qubit names are determined by the OpenQASM qreg names, appended with zero-based indices.

Below we give an example of each method.

Method 1#

Construct a circuit and use arbitrary qubits:

import cirq

q1, q2 = cirq.NamedQubit('Alice'), cirq.NamedQubit('Bob')
circuit = cirq.Circuit()
circuit.append(cirq.X(q1))
circuit.append(cirq.H(q2))
circuit.append(cirq.CNOT(q1, q2))
circuit.append(cirq.measure(q1, q2, key='m'))
print(circuit)

This will result in the circuit

Alice: ───X───@───M('m')───
              │   │
Bob: ─────H───X───M────────

Method 2#

You can read an OpenQASM 2.0 program from a file (or a string), e.g.

OPENQASM 2.0;
include "qelib1.inc";

qreg q[2];
creg m[2];

x q[0];
h q[1];
cx q[0], q[1];
measure q -> m;

and convert it into a cirq.Circuit object using circuit_from_qasm().

from iqm import cirq_iqm

with open('circuit.qasm', 'r') as f:
    qasm_circuit = cirq_iqm.circuit_from_qasm(f.read())
print(qasm_circuit)
q_0: ───X───@───M('m_0')───
            │
q_1: ───H───X───M('m_1')───

circuit_from_qasm() uses the OpenQASM 2.0 parser in cirq.contrib.qasm_import.

After a circuit has been constructed, it can be decomposed and routed against a particular IQMDevice.

Decomposition#

The method IQMDevice.decompose_circuit() accepts a cirq.Circuit object as an argument and returns the decomposed circuit containing only native operations for the corresponding device:

decomposed_circuit = adonis.decompose_circuit(circuit)
print(decomposed_circuit)
Alice: ───X────────────────────@───────────M('m')───
                               │           │
Bob: ─────Y^0.5───X───Y^-0.5───@───Y^0.5───M────────

The Hadamard and CNOT gates are not native to Adonis, so they were decomposed to X, Y and CZ gates which are.

Routing#

Routing means transforming a circuit such that it acts on the device qubits, and respects the device connectivity. The method IQMDevice.route_circuit() accepts a cirq.Circuit object as an argument, and returns the circuit routed against the device, acting on the device qubits instead of the arbitrary qubits we had originally.

routed_circuit_1, initial_mapping, final_mapping = adonis.route_circuit(decomposed_circuit)
print(routed_circuit_1)
QB3: ───X────────────────────@───────────M('m')───
                             │           │
QB4: ───Y^0.5───X───Y^-0.5───@───Y^0.5───M────────

Along with the routed circuit route_circuit() returns the initial_mapping and final_mapping. The initial_mapping is either the mapping from circuit to device qubits as provided by an cirq.AbstractInitialMapper or a mapping that is initialized from the device graph. The final_mapping is the mapping from physical qubits before inserting SWAP gates to the physical qubits after the routing is complete

As mentioned above, you may also provide the initial mapping from the logical qubits in the circuit to the physical qubits on the device yourself, by using the keyword argument initial_mapper. It serves as the starting point of the routing:

initial_mapper = cirq.HardCodedInitialMapper({q1: adonis.qubits[2], q2: adonis.qubits[0]})
routed_circuit_2, _, _ = adonis.route_circuit(
    decomposed_circuit,
    initial_mapper=initial_mapper,
)
print(routed_circuit_2)
QB1: ───Y^0.5───X───Y^-0.5───@───Y^0.5───────M────────
                             │               │
QB3: ───X────────────────────@───────────────M('m')───

Under the hood, route_circuit() leverages the routing provided by cirq.RouteCQC. It works on single- and two-qubit gates, and measurement operations of arbitrary size. If you have gates involving more than two qubits you need to decompose them before routing. Since routing may add some SWAP gates to the circuit, you will need to decompose the circuit again after the routing, unless SWAP is a native gate for the target device.

Optimization#

Yet another important topic is circuit optimization. In addition to the optimizers available in Cirq you can also benefit from Cirq on IQM’s optimizers module which contains some optimization tools geared towards IQM devices. The function optimizers.simplify_circuit() is a convenience method encapsulating a particular sequence of optimizations. Let us try it out on our decomposed and routed circuit above:

from iqm.cirq_iqm.optimizers import simplify_circuit

simplified_circuit = simplify_circuit(routed_circuit_1)
print(simplified_circuit)
QB3: ───PhX(1)───@───────────────────M('m')───
                 │                   │
QB4: ────────────@───PhX(-0.5)^0.5───M────────

Note

The funtion simplify_circuit() is not associated with any IQM device, so its result may contain non-native gates for a particular device. In the example above we don’t have them, however it is generally a good idea to run decomposition once again after the simplification.

Running on a real quantum computer#

Note

At the moment IQM does not provide a quantum computing service open to the general public. Please contact our sales team to set up your access to an IQM quantum computer.

Cirq contains various simulators which you can use to simulate the circuits constructed above. In this subsection we demonstrate how to run them on an IQM quantum computer.

Cirq on IQM implements IQMSampler, a subclass of cirq.work.Sampler, which is used to execute quantum circuits. Once you have access to an IQM server you can create an IQMSampler instance and use its run() method to send a circuit for execution and retrieve the results:

from iqm.cirq_iqm.iqm_sampler import IQMSampler

sampler = IQMSampler(iqm_server_url)
result = sampler.run(routed_circuit_1, repetitions=10)
print(result.measurements['m'])

Note that the code snippet above assumes that you have set the variable iqm_server_url to the URL of the IQM server. Additionally, you can pass IQM backend specific options to the IQMSampler class. The below table summarises the currently available options:

Name

Type

Example value

Description

calibration_set_id

str

“f7d9642e-b0ca-4f2d-af2a-30195bd7a76d”

Indicates the calibration set to use. Defaults to None, which means the IQM server will use the best available calibration set automatically.

max_circuit_duration_over_t2

float

1.0

Set server-side circuit disqualification threshold. If any circuit in a job is estimated to take longer than the shortest T2 time of any qubit used in the circuit multiplied by this value, the server will reject the job. Setting this value to 0.0 will disable circuit duration check. The default value None means the server default value will be used.

heralding_mode

HeraldingMode

“zeros”

Heralding mode to use during execution. The default value is “none”, “zeros” enables heralding.

For example if you would like to use a particular calibration set, you can provide it as follows:

sampler = IQMSampler(iqm_server_url, calibration_set_id="f7d9642e-b0ca-4f2d-af2a-30195bd7a76d")

The same applies for heralding_mode and max_circuit_duration_over_t2. The sampler will by default use an IQMDevice created based on architecture data obtained from the server, which is then available in the IQMSampler.device property. Alternatively, the device can be specified directly with the device argument.

If the IQM server you are connecting to requires authentication, you will also have to use Cortex CLI to retrieve and automatically refresh access tokens, then set the IQM_TOKENS_FILE environment variable to use those tokens. See Cortex CLI’s documentation for details. Alternatively, you can authenticate yourself using the IQM_AUTH_SERVER, IQM_AUTH_USERNAME and IQM_AUTH_PASSWORD environment variables, or pass them as arguments to the constructor of IQMProvider, but this approach is less secure and considered deprecated.

When executing a circuit that uses something other than the device qubits, you need to route it first, as explained in the Routing section above.

Multiple circuits can be submitted to the IQM quantum computer at once using the run_iqm_batch() method of IQMSampler. This is often faster than executing the circuits individually. Circuits submitted in a batch are still executed sequentially.

circuit_list = []

circuit_list.append(routed_circuit_1)
circuit_list.append(routed_circuit_2)

results = sampler.run_iqm_batch(circuit_list, repetitions=10)

for result in results:
     print(result.histogram(key="m"))

More examples#

More examples are available in the examples directory of the Cirq on IQM repository.

How to develop and contribute#

Cirq on IQM is an open source Python project. You can contribute by creating GitHub issues to report bugs or request new features, or by opening a pull request to submit your own improvements to the codebase.

To start developing the project, clone the GitHub repository and install it in editable mode with all the extras:

$ git clone git@github.com:iqm-finland/cirq-on-iqm.git
$ cd cirq-on-iqm
$ pip install -e ".[dev,docs,testing]"

Build and view the docs:

$ tox -e docs
$ firefox build/sphinx/html/index.html

Run the tests:

$ tox

Tagging and releasing#

After implementing changes to Cirq on IQM one usually wants to release a new version. This means that after the changes are merged to the main branch -

  1. the repository should have an updated CHANGELOG with information about the new changes,

  2. the latest commit should be tagged with the new version number,

  3. and a release should be created based on that tag.

The last two steps are automated, so one needs to worry only about properly updating the CHANGELOG. It should be done along with the pull request which is introducing the main changes. The new version must be added on top of all existing versions and the title must be “Version MAJOR.MINOR”, where MAJOR.MINOR represents the new version number. Please take a look at already existing versions and format the rest of your new CHANGELOG section similarly. Once the pull request is merged into main, a new tag and a release will be created automatically based on the latest version definition in the CHANGELOG.