Quickstart

The central interface for working with quantum circuits in the Munich Quantum Toolkit is the QuantumComputation class. It represents quantum circuits as a sequential list of operations. Operations can be directly applied to the QuantumComputation:

 1from mqt.core import QuantumComputation
 2
 3# Build a `QuantumComputation` representing a Bell-state preparation circuit.
 4nqubits = 2
 5qc = QuantumComputation(nqubits)
 6
 7qc.h(0)  # Apply Hadamard gate on qubit 0
 8qc.cx(0, 1)  # Apply a CNOT (controlled X-Gate) with control on qubit 0 and target on qubit 1
 9
10# Get Circuit in OpenQASM 3.0 format
11print(qc.qasm3_str())
// i 0 1
// o 0 1
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
h q[0];
cx q[0], q[1];

The circuit class provides a lot of flexibility as every unitary gate can be declared as a controlled gate:

 1from mqt.core.operations import Control
 2
 3nqubits = 2
 4qc = QuantumComputation(nqubits)
 5
 6# Controlled Hadamard Gate
 7qc.ch(0, 1)
 8
 9# Negatively controlled S-gate: S-Gate on target is performed if control is in |0> state.
10qc.cs(Control(0, Control.Type.Neg), 1)
11
12print(qc.qasm3_str())
// i 0 1
// o 0 1
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
ch q[0], q[1];
negctrl @ p(pi/2) q[0], q[1];

Providing a set of Control objects allows declaring any (unitary) gate as a multi-controlled gate:

 1nqubits = 3
 2qc = QuantumComputation(nqubits)
 3
 4# Toffoli gate in mqt-core:
 5qc.mcx({0, 1}, 2)
 6
 7# Control type can be individually declared
 8qc.mcs({Control(0, Control.Type.Neg), Control(1, Control.Type.Pos)}, 2)
 9
10print(qc.qasm3_str())
// i 0 1 2
// o 0 1 2
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
ccx q[0], q[1], q[2];
negctrl @ ctrl @ p(pi/2) q[0], q[1], q[2];

Layout Information

A QuantumComputation also contains information about the mapping of algorithmic (or logical/virtual/circuit) qubits to and from device (or physical) qubits. These are contained in the initial_layout and output_permutation members which are instances of the Permutation class. If no layout is given the trivial layout is assumed.

When printing the OpenQASM representation of the QuantumComputation the input and output permutations are given as comments in the first two lines of the QASM string. The format is:

// i Q_0, Q_1, ..., Q_n … algorithmic qubit \(i\) is mapped to device qubit \(Q_i\).

// o Q_0, Q_1, ..., Q_n … the value of algorithmic qubit \(i\) (assumed to be stored in classical bit \(c[i]\)) is measured at device qubit \(Q_i\).

 1nqubits = 3
 2qc = QuantumComputation(nqubits)
 3qc.initial_layout[2] = 0
 4qc.initial_layout[0] = 1
 5qc.initial_layout[1] = 2
 6
 7qc.output_permutation[2] = 0
 8qc.output_permutation[0] = 1
 9qc.output_permutation[1] = 2
10
11
12print(qc.qasm3_str())
// i 2 0 1
// o 2 0 1
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;

The layout information can also be automatically determined from measurements using the initialize_io_mapping() method:

 1nqubits = 3
 2qc = QuantumComputation(nqubits, nqubits)  # 3 qubits, 3 classical bits
 3
 4qc.h(0)
 5qc.x(1)
 6qc.s(2)
 7qc.measure(1, 0)  # c[0] is measured at qubit 1
 8qc.measure(2, 1)  # c[1] is measured at qubit 2
 9qc.measure(0, 2)  # c[2] is measured at qubit 0
10qc.initialize_io_mapping()  # determine permutation from measurement
11
12print(qc.qasm3_str())
// i 0 1 2
// o 1 2 0
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
x q[1];
s q[2];
c[0] = measure q[1];
c[1] = measure q[2];
c[2] = measure q[0];

Visualizing Circuits

Circuits can be printed in a human-readable format:

 1from mqt.core import QuantumComputation
 2
 3nqubits = 2
 4qc = QuantumComputation(nqubits, 1)
 5
 6qc.h(0)
 7qc.cx(0, 1)
 8qc.measure(1, 0)
 9
10print(qc)
i:   0   1
1:   h   |
2:   c   x
3:   |   0
o:   0   1

Operations

The operations in a QuantumComputation object are of type Operation. Every type of operation in mqt-core is derived from this class. Operations can also be explicitly constructed. Each Operation has a type in the form of an OpType.

StandardOperation

A StandardOperation is used to represent basic unitary gates. These can also be declared with arbitrary targets and controls.

 1from math import pi
 2
 3from mqt.core.operations import OpType, StandardOperation
 4
 5nqubits = 3
 6
 7# u3 gate on qubit 0 in a 3-qubit circuit
 8u_gate = StandardOperation(target=0, params=[pi / 4, pi, -pi / 2], op_type=OpType.u)
 9
10# controlled x-rotation
11crx = StandardOperation(control=Control(0), target=1, params=[pi], op_type=OpType.rx)
12
13# multi-controlled x-gate
14mcx = StandardOperation(controls={Control(0), Control(1)}, target=2, op_type=OpType.x)
15
16# add operations to a quantum computation
17qc = QuantumComputation(nqubits)
18qc.append(u_gate)
19qc.append(crx)
20qc.append(mcx)
21
22print(qc)
i:   0   1   2
1:   u   |   |  p: (0.785398) (3.14159) (-1.5708) 
2:   c  rx   |  p: (3.14159) 
3:   c   c   x
o:   0   1   2

NonUnitaryOperation

A NonUnitaryOperation is used to represent operations involving measurements or resets.

 1from mqt.core.operations import NonUnitaryOperation
 2
 3nqubits = 2
 4qc = QuantumComputation(nqubits, nqubits)
 5qc.h(0)
 6
 7# measure qubit 0 on classical bit 0
 8meas_0 = NonUnitaryOperation(target=0, classic=0)
 9
10# reset all qubits
11reset = NonUnitaryOperation(targets=[0, 1], op_type=OpType.reset)
12
13qc.append(meas_0)
14qc.append(reset)
15
16print(qc.qasm3_str())
// i 0 1
// o 0 1
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
c[0] = measure q[0];
reset q;

SymbolicOperation

A SymbolicOperation can represent all gates of a StandardOperation but the gate parameters can be symbolic. Symbolic expressions are represented in MQT using the Expression type, which represent linear combinations of symbolic Term objects over some set of Variable objects.

 1from mqt.core.operations import SymbolicOperation
 2from mqt.core.symbolic import Expression, Term, Variable
 3
 4nqubits = 1
 5
 6x = Variable("x")
 7y = Variable("y")
 8sym = Expression([Term(x, 2), Term(y, 3)])
 9print(sym)
10
11sym += 1
12print(sym)
13
14# Create symbolic gate
15u1_symb = SymbolicOperation(target=0, params=[sym], op_type=OpType.p)
16
17# Mixed symbolic and instantiated parameters
18u2_symb = SymbolicOperation(target=0, params=[sym, 2.0], op_type=OpType.u2)
2*x + 3*y + 0
2*x + 3*y + 1

CompoundOperation

A CompoundOperation bundles multiple Operation objects together.

 1from mqt.core.operations import CompoundOperation
 2
 3nqubits = 2
 4comp_op = CompoundOperation()
 5
 6# create bell pair circuit
 7comp_op.append(StandardOperation(0, op_type=OpType.h))
 8comp_op.append(StandardOperation(target=0, control=Control(1), op_type=OpType.x))
 9
10qc = QuantumComputation(nqubits)
11qc.append(comp_op)
12
13print(qc)
i:   0   1
1:--------
 :   h   |
 :   x   c
 ---------
o:   0   1

Circuits can be conveniently turned into operations which allows to create nested circuits:

 1from mqt.core import QuantumComputation
 2
 3nqubits = 2
 4comp = QuantumComputation(nqubits)
 5comp.h(0)
 6comp.cx(0, 1)
 7
 8qc = QuantumComputation(nqubits)
 9qc.append(comp.to_operation())
10
11print(qc)
i:   0   1
1:--------
 :   h   |
 :   c   x
 ---------
o:   0   1

Interfacing with other SDKs

Since a QuantumComputation can be imported from and exported to an OpenQASM 3.0 (or OpenQASM 2.0) string, any library that can work with OpenQASM is easy to use in conjunction with the QuantumComputation class.

In addition, mqt-core can import Qiskit QuantumCircuit objects directly.

 1from qiskit import QuantumCircuit
 2
 3from mqt.core.plugins.qiskit import qiskit_to_mqt
 4
 5# GHZ circuit in qiskit
 6qiskit_qc = QuantumCircuit(3)
 7qiskit_qc.h(0)
 8qiskit_qc.cx(0, 1)
 9qiskit_qc.cx(0, 2)
10
11qiskit_qc.draw(output="mpl", style="iqp")
_images/8f6ac57715cb3fa1b41e01790d9c771a598596fedb7ed4117d7c7605296f78df.svg
1mqt_qc = qiskit_to_mqt(qiskit_qc)
2print(mqt_qc)
circuit-158
i:   0   1   2
1:   h   |   |
2:   c   x   |
3:   c   |   x
o:   0   1   2