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:
Create a
Circuit
instance using arbitrary qubit names and types.Create a
Circuit
from an OpenQASM 2.0 program. The qubit names are determined by the OpenQASMqreg
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.
To ensure that the transpiler is restricted to a specific subset of qubits, you can provide a list
of qubits in the qubit_subset
argument such that ancillary qubits will not be added during
routing. This is particularly useful when running Quantum Volume benchmarks.
IQM Star architecture#
Devices that have the IQM Star architecture (e.g. IQM Deneb) support MOVE gates that are used to move quantum states between qubits and computational resonators. For these devices a final MOVE gate insertion step must be performed, which introduces the computational resonators to the circuit, and routes the two-qubit gates through them using MOVEs.
Under the hood, this uses the transpile_insert_moves()
function of the
iqm_client
library. This method is exposed through transpile_insert_moves_into_circuit()
which
can also be used by advanced users to transpile circuits that have already some MOVE gates in them,
or to remove existing MOVE gates from a circuit so the circuit can be reused on a device that does
not support them.
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 provides IQMSampler
, a subclass of cirq.work.Sampler
, which is used
to execute quantum circuits and decompose/route them for the architecture of the quantum computer.
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
# circuit = ...
sampler = IQMSampler(iqm_server_url)
decomposed_circuit = sampler.device.decompose_circuit(circuit)
routed_circuit, _, _ = sampler.device.route_circuit(decomposed_circuit)
result = sampler.run(routed_circuit, 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 |
---|---|---|---|
|
“f7d9642e-b0ca-4f2d-af2a-30195bd7a76d” |
Indicates the calibration set to use. Defaults to |
|
|
see below |
Contains various options that affect the compilation of the quantum circuit into an instruction schedule. |
The CircuitCompilationOptions
class contains the following attributes (in addition to some
advanced options described in the API documentation):
Name |
Type |
Example value |
Description |
---|---|---|---|
|
|
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 |
|
“zeros” |
Heralding mode to use during execution. The default value is “none”, “zeros” enables all-zeros heralding where the circuit qubits are measured before the circuit begins, and the server post-selects and returns only those shots where the heralding measurement yields zeros for all the qubits. |
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 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. The architecture
data depends on the calibration set used by the sampler, so one should usually use different sampler
instances for different calibration sets. Alternatively, the device can be specified directly with
the device
argument, but this is not recommended when running on a real quantum computer.
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.
Authentication#
If the IQM server you are connecting to requires authentication, you may use
Cortex CLI to retrieve and automatically refresh access tokens,
then set the IQM_TOKENS_FILE
environment variable, as instructed, to point to the tokens file.
See Cortex CLI’s documentation for details.
Alternatively, you may authenticate yourself using the IQM_AUTH_SERVER
,
IQM_AUTH_USERNAME
and IQM_AUTH_PASSWORD
environment variables, or pass them as
arguments to IQMSampler
, but this approach is less secure and
considered deprecated.
Finally, if you are using IQM Resonance
, authentication is handled differently.
Use the IQM_TOKEN
environment variable to provide the API Token obtained
from the server dashboard.
Batch execution#
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"))
Inspecting the final circuits before submitting them for execution#
It is possible to inspect the final circuits that would be submitted for execution before actually submitting them,
which can be useful for debugging purposes. This can be done using IQMSampler.create_run_request()
, which returns
a RunRequest
containing the circuits and other data. The method accepts the same
parameters as IQMSampler.run()
and IQMSampler.run_iqm_batch()
, and creates the run request in the same
way as those functions.
# inspect the run_request without submitting it for execution
run_request = sampler.create_run_request(routed_circuit_1, repetitions=10)
print(run_request)
# the following calls submit exactly the same run request for execution on the server
sampler.run(routed_circuit_1, repetitions=10)
sampler._client.submit_run_request(run_request)
It is also possible to print a run request when it is actually submitted by setting the environment variable
IQM_CLIENT_DEBUG=1
.
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 -
the repository should have an updated CHANGELOG with information about the new changes,
the latest commit should be tagged with the new version number,
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.