MQT Qudits Tutorial ๐ŸŒŒยถ

Discover a New Dimension in Quantum Computing. Embark on a journey with MQT Qudits, a framework for Mixed-Dimensional Quantum Computing.

Delve into the realm of mixed-dimensional quantum computing with NeQSTโ€”a project funded by the European Union and developed developed as part of the Munich Quantum Toolkit (MQT) by the Chair for Design Automation at the Technical University of Munich. Our team is focused on creating design automation methods and software for quantum computing. The following tutorial will guide you through the initial tools and contributions we have made to advance Quantum Information Processing for Science and Technology.

Installation Steps:ยถ

(.venv) $ pip install mqt.qudits

For those seeking hands-on customization, simply clone the corresponding repository and perform a local installation.

$ git clone https://github.com/cda-tum/mqt-qudits.git
$ cd mqt-qudits
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ pip install -ve .
+++

```{note}
This requires a C++17 compiler, a minimum CMake version of 3.19, and Python 3.8+.

User Inputs ๐Ÿ’ปยถ

1import numpy as np
2
3from mqt.qudits.quantum_circuit import QuantumCircuit

๐Ÿš€ New QASM Extension:ยถ

Dive into a language meticulously designed to express quantum algorithms and circuits. MQT Qudits extends the OpenQASM 2.0 grammar, effortlessly adapting to mixed-dimensional registers. In the following, a DITQASM program is explicitly written, although several methods for importing programs from files are present in the library.

 1qasm = """
 2    DITQASM 2.0;
 3
 4    qreg field [7][5,5,5,5,5,5,5];
 5    qreg matter [2];
 6
 7    creg meas_matter[7];
 8    creg meas_fields[3];
 9
10    h matter[0] ctl field[0] field[1] [0,0];
11    cx field[2], matter[0];
12    cx field[2], matter[1];
13    rxy (0, 1, pi, pi/2) field[3];
14
15    measure q[0] -> meas[0];
16    measure q[1] -> meas[1];
17    measure q[2] -> meas[2];
18    """

A new feature is the control syntax:

_operation_   __ctl__  _quditline_  [list of qudit control levels]

We can import the DITQASM program and construct a quantum circuit.

1circuit = QuantumCircuit()
2circuit.from_qasm(qasm)
3
4print(f"Number of operations: {len(circuit.instructions)}")
5print(f"Number of qudits in the circuit: {circuit.num_qudits}")
6print(f"Dimensions: {circuit.dimensions}")
Number of operations: 4
Number of qudits in the circuit: 9
Dimensions: [5, 5, 5, 5, 5, 5, 5, 2, 2]

๐Ÿ Python Interfaceยถ

Constructing and manipulating quantum programs becomes a breeze with Python. You have the flexibility to:

  1. Initialize Quantum Circuits: Start by creating your quantum circuits effortlessly.

  2. Create Quantum Registers: Build dedicated quantum registers tailored to your needs.

  3. Compose Circuits: Seamlessly bring together your quantum registers, forming a unified and powerful circuit.

  4. Apply Operations: Easily apply a variety of qudit operations, without worrying about the right representation.

Letโ€™s construct a quantum circuit from scratch, with the python interface.

 1from mqt.qudits.quantum_circuit import QuantumRegister
 2
 3circuit = QuantumCircuit()
 4
 5field_reg = QuantumRegister("fields", 1, [7])
 6matter_reg = QuantumRegister("matter", 1, [2])
 7
 8circuit.append(field_reg)
 9circuit.append(matter_reg)
10
11print(f"Number of operations: {len(circuit.instructions)}")
12print(f"Number of qudits in the circuit: {circuit.num_qudits}")
13print(f"Gate set: {circuit.gate_set}")
Number of operations: 0
Number of qudits in the circuit: 2
csum
cu_one
cu_two
cu_multi
cx
gellmann
h
ls
ms
pm
r
rh
randu
rz
virtrz
s
x
z
Gate set: None

No operations were inserted yet, letโ€™s take a look at how operations can be applied!

The size of every line is detected automatically and the right operations are applied to the right qudits

1circuit.h(field_reg[0])
2circuit.csum([field_reg[0], matter_reg[0]])
3
4print(f"Number of operations: {len(circuit.instructions)}")
5print(f"Number of qudits in the circuit: {circuit.num_qudits}")
Number of operations: 2
Number of qudits in the circuit: 2

It is possible to export the code as well and share your program in a QASM file.

1print(circuit.to_qasm())
DITQASM 2.0;
qreg fields [1][7];
qreg matter [1][2];
creg meas[2];
h fields[0];
csum fields[0], matter[0];
measure fields[0] -> meas[0];
measure matter[0] -> meas[1];

Letโ€™s save the circuit to a file

1file = circuit.save_to_file("my_circuit", ".")

and load it back

1circuit.load_from_file(file)
2
3print("Program:", circuit.to_qasm(), sep="\n")
4print("Dimensions: ", circuit.dimensions)
Program:
DITQASM 2.0;
qreg fields [1][7];
qreg matter [1][2];
creg meas[2];
h fields[0];
csum fields[0], matter[0];
measure fields[0] -> meas[0];
measure matter[0] -> meas[1];

Dimensions:  [7, 2]

Custom gates can be added to the circuit as well.

1n = 5
2random_matrix = np.random.randn(n, n) + 1j * np.random.randn(n, n)
3
4Q, R = np.linalg.qr(random_matrix)
5
6unitary_matrix = Q
7cu = circuit.cu_one(field_reg[0], unitary_matrix)

Gates follow the order:

  • target qudit/s : list or single number

  • parameters list with order lower level, upper level, control level, theta, phi

  • control data

A simple qudit gate can be added as follows:

1r = circuit.r(field_reg[0], [0, 1, np.pi / 5, np.pi / 7])

Operations can also be controlled by other qudits, as shown below:

1from mqt.qudits.quantum_circuit.gate import ControlData
2
3r_c1 = circuit.r(field_reg[0], [0, 1, np.pi / 5, np.pi / 7], ControlData([matter_reg[0]], [1]))

or as

1r_c2 = circuit.r(field_reg[0], [0, 1, np.pi / 5, np.pi / 7]).control([matter_reg[0]], [1])

The representation of the matrix corresponding to a gate is dynamic:

  • 0: no identities

  • 1: identities in between long-range gates are introduced

  • 2: full circuit unitary

1print(f"Gate matrix for {r._name}:", r.to_matrix(0), sep="\n")
Gate matrix for R7:
[[ 0.95105652+0.j         -0.13407745-0.27841469j  0.        +0.j
   0.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j        ]
 [ 0.13407745-0.27841469j  0.95105652+0.j          0.        +0.j
   0.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j        ]
 [ 0.        +0.j          0.        +0.j          1.        +0.j
   0.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j        ]
 [ 0.        +0.j          0.        +0.j          0.        +0.j
   1.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j        ]
 [ 0.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j          1.        +0.j          0.        +0.j
   0.        +0.j        ]
 [ 0.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j          0.        +0.j          1.        +0.j
   0.        +0.j        ]
 [ 0.        +0.j          0.        +0.j          0.        +0.j
   0.        +0.j          0.        +0.j          0.        +0.j
   1.        +0.j        ]]

The inverse of any gate can easily be obtained.

1rd = r.dag()
2print(f"Inverse gate matrix for {r._name}:", rd.to_matrix(0), sep="\n")
Inverse gate matrix for R7_dag:
[[ 0.95105652-0.j          0.13407745+0.27841469j  0.        -0.j
   0.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j        ]
 [-0.13407745+0.27841469j  0.95105652-0.j          0.        -0.j
   0.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j        ]
 [ 0.        -0.j          0.        -0.j          1.        -0.j
   0.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j        ]
 [ 0.        -0.j          0.        -0.j          0.        -0.j
   1.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j        ]
 [ 0.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j          1.        -0.j          0.        -0.j
   0.        -0.j        ]
 [ 0.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j          0.        -0.j          1.        -0.j
   0.        -0.j        ]
 [ 0.        -0.j          0.        -0.j          0.        -0.j
   0.        -0.j          0.        -0.j          0.        -0.j
   1.        -0.j        ]]

The control information can be accessed as well.

1r_c1.control_info
{'target': 0,
 'dimensions_slice': 7,
 'params': [0, 1, 0.6283185307179586, 0.4487989505128276],
 'controls': ControlData(indices=[1], ctrl_states=[1])}

Two- and multi-qudit gates follow the rule:

  • two : target_qudits first is control, second is target

  • multi: all are controls, except last one is target

1r_c1.reference_lines
[1, 0]

Simulation ๐Ÿš€ยถ

After crafting your quantum circuit with precision, take it for a spin using two distinct engines, each flaunting its unique set of data structures.

  • External Tensor-Network Simulator: Delve into the quantum realm with a robust external tensor-network simulator. Can simulate all the gate-set.

  • MiSiM (C++-Powered): Unleash the power of decision-diagram-based simulation with MiSiM, seamlessly interfaced with Python for a fluid and efficient experience. ๐ŸŒ๐Ÿ’กCan only simulate the machine following machine gate set:

    • csum

    • cx

    • h

    • rxy

    • rz

    • rh

    • virtrz

    • s

    • x

    • z

Basic Simulationยถ

 1circuit = QuantumCircuit()
 2
 3field_reg = QuantumRegister("fields", 1, [3])
 4matter_reg = QuantumRegister("matter", 1, [3])
 5
 6circuit.append(field_reg)
 7circuit.append(matter_reg)
 8
 9h = circuit.h(field_reg[0])
10csum = circuit.csum([field_reg[0], matter_reg[0]])
11
12print(f"Number of operations: {len(circuit.instructions)}")
13print(f"Number of qudits in the circuit: {circuit.num_qudits}")
Number of operations: 2
Number of qudits in the circuit: 2
1from mqt.qudits.simulation import MQTQuditProvider
2
3provider = MQTQuditProvider()
4provider.backends("sim")
['tnsim', 'misim']
 1from mqt.qudits.visualisation import plot_counts, plot_state
 2
 3backend = provider.get_backend("tnsim")
 4
 5job = backend.run(circuit)
 6result = job.result()
 7
 8state_vector = result.get_state_vector()
 9
10plot_state(state_vector, circuit)
_images/43dd1f44c70964e59239e93527b69f82566229c05a5732d864195d96ebf2c341.svg
1backend = provider.get_backend("misim")
2
3job = backend.run(circuit)
4result = job.result()
5
6state_vector = result.get_state_vector()
7
8plot_state(state_vector, circuit)
_images/2d9ed8cde8d24b9dc319adce20e283c685883e456cf7618408bd96cad2d8fc0f.svg

Extending Engines with Noise Model and Properties for FakeBackendยถ

Enhance your quantum simulation experience by extending the engines with a noise model and incorporating various properties. By combining a noise model and carefully tuned properties, you can craft a FakeBackend that closely emulates the performance of the best quantum machines in experimental laboratories. This allows for more realistic and insightful quantum simulations.

Experiment, iterate, and simulate quantum circuits with the sophistication of real-world conditions, all within the controlled environment of your simulation. ๐Ÿ› ๏ธ๐Ÿ”ฌ

 1from mqt.qudits.simulation.noise_tools.noise import Noise, NoiseModel
 2
 3# Depolarizing quantum errors
 4local_error = Noise(probability_depolarizing=0.001, probability_dephasing=0.001)
 5local_error_rz = Noise(probability_depolarizing=0.03, probability_dephasing=0.03)
 6
 7entangling_error = Noise(probability_depolarizing=0.1, probability_dephasing=0.001)
 8entangling_error_extra = Noise(probability_depolarizing=0.1, probability_dephasing=0.1)
 9
10entangling_error_on_target = Noise(probability_depolarizing=0.1, probability_dephasing=0.0)
11entangling_error_on_control = Noise(probability_depolarizing=0.01, probability_dephasing=0.0)
12
13# Add errors to noise_tools model
14
15noise_model = NoiseModel()  # We know that the architecture is only two qudits
16# Very noisy gate
17noise_model.add_all_qudit_quantum_error(local_error, ["csum"])
18noise_model.add_recurrent_quantum_error_locally(local_error, ["csum"], [0])
19# Entangling gates
20noise_model.add_nonlocal_quantum_error(entangling_error, ["cx", "ls", "ms"])
21noise_model.add_nonlocal_quantum_error_on_target(entangling_error_on_target, ["cx", "ls", "ms"])
22noise_model.add_nonlocal_quantum_error_on_control(entangling_error_on_control, ["csum", "cx", "ls", "ms"])
23# Super noisy Entangling gates
24noise_model.add_nonlocal_quantum_error(entangling_error_extra, ["csum"])
25# Local Gates
26noise_model.add_quantum_error_locally(local_error, ["h", "rxy", "s", "x", "z"])
27noise_model.add_quantum_error_locally(local_error_rz, ["rz", "virtrz"])
28
29print(noise_model.quantum_errors)
{'csum': {'all': Noise(probability_depolarizing=0.001, probability_dephasing=0.001), (0,): Noise(probability_depolarizing=0.001, probability_dephasing=0.001), 'control': Noise(probability_depolarizing=0.01, probability_dephasing=0.0), 'nonlocal': Noise(probability_depolarizing=0.1, probability_dephasing=0.1)}, 'cx': {'nonlocal': Noise(probability_depolarizing=0.1, probability_dephasing=0.001), 'target': Noise(probability_depolarizing=0.1, probability_dephasing=0.0), 'control': Noise(probability_depolarizing=0.01, probability_dephasing=0.0)}, 'ls': {'nonlocal': Noise(probability_depolarizing=0.1, probability_dephasing=0.001), 'target': Noise(probability_depolarizing=0.1, probability_dephasing=0.0), 'control': Noise(probability_depolarizing=0.01, probability_dephasing=0.0)}, 'ms': {'nonlocal': Noise(probability_depolarizing=0.1, probability_dephasing=0.001), 'target': Noise(probability_depolarizing=0.1, probability_dephasing=0.0), 'control': Noise(probability_depolarizing=0.01, probability_dephasing=0.0)}, 'h': {'local': Noise(probability_depolarizing=0.001, probability_dephasing=0.001)}, 'rxy': {'local': Noise(probability_depolarizing=0.001, probability_dephasing=0.001)}, 's': {'local': Noise(probability_depolarizing=0.001, probability_dephasing=0.001)}, 'x': {'local': Noise(probability_depolarizing=0.001, probability_dephasing=0.001)}, 'z': {'local': Noise(probability_depolarizing=0.001, probability_dephasing=0.001)}, 'rz': {'local': Noise(probability_depolarizing=0.03, probability_dephasing=0.03)}, 'virtrz': {'local': Noise(probability_depolarizing=0.03, probability_dephasing=0.03)}}

We can set the noise model for the simulation, but also set several other flags:

  • shots: number of shots for the stochastic simulation

  • memory: flag for saving shots (True/False)

  • full_state_memory: save the full noisy states

  • file_path: file path of the h5 database storing the data

  • file_name: name of the file

1backend = provider.get_backend("tnsim")
2
3job = backend.run(circuit, noise_model=noise_model)
4
5result = job.result()
6counts = result.get_counts()
7
8plot_counts(counts, circuit)
_images/1d32a3bfc27eac276a432521f3df0cd97324d7e6f526e49f2543fde058e27d40.svg
{'00': 332,
 '01': 0,
 '02': 3,
 '10': 4,
 '11': 329,
 '12': 0,
 '20': 0,
 '21': 7,
 '22': 325}

You can also invoke a fake backend and retrieve a few relevant properties, that are already embedded in them

1provider = MQTQuditProvider()
2provider.backends("fake")
['faketraps2trits', 'faketraps2six']
1backend_ion = provider.get_backend("faketraps2trits", shots=1000)
1import matplotlib.pyplot as plt
2import networkx as nx
3
4mapping = backend_ion.energy_level_graphs
5
6pos = nx.circular_layout(mapping[0])
7nx.draw(mapping[0], pos, with_labels=True, node_size=2000, node_color="lightblue", font_size=12, font_weight="bold")
8plt.show()
_images/4956c0f7c137fb66fece8a828734bbf3f594e75a26b405ae5a6eba8bf35818d8.svg
1job = backend_ion.run(circuit)
2result = job.result()
3counts = result.get_counts()
4
5plot_counts(counts, circuit)
_images/46f4ddcef987ed1b78be20eeff5926827f61f208b787a5ed24f0b30545617020.svg
{'00': 328,
 '01': 0,
 '02': 1,
 '10': 2,
 '11': 322,
 '12': 0,
 '20': 0,
 '21': 2,
 '22': 345}

Compilation โš™๏ธยถ

Tailor your quantum compilation process to achieve optimal performance and emulate the intricacies of experimental setups.

Compiler Customization with Modern Passesยถ

  1. Optimization Strategies: Implement specific optimization strategies based on your quantum algorithmโ€™s characteristics. Fine-tune compilation for better resource utilization and reduced gate counts.

  2. Gate Decomposition: Customize gate decomposition techniques to match the capabilities of experimental quantum hardware. Aligning with the native gate set enhances the efficiency of your compiled circuits.

Experimental-Inspired Compilationยถ

Emulate the features of the best experimental laboratories in your compilation process. Leverage modern compiler passes to customize optimization, gate decomposition, and noise-aware strategies, creating compiled circuits that closely resemble the challenges and advantages of cutting-edge quantum hardware.

Customize, compile, and push the boundaries of quantum algorithms with a tailored approach to quantum compilation. ๐Ÿ› ๏ธ๐Ÿ”ง๐Ÿš€

1from mqt.qudits.compiler import QuditCompiler
1qudit_compiler = QuditCompiler()
2
3passes = ["PhyLocQRPass"]
1compiled_circuit_qr = qudit_compiler.compile(backend_ion, circuit, passes)
2
3print(f"Number of operations: {len(compiled_circuit_qr.instructions)}")
4print(f"Number of qudits in the circuit: {compiled_circuit_qr.num_qudits}")
Number of operations: 10
Number of qudits in the circuit: 2
1job = backend_ion.run(compiled_circuit_qr)
2
3result = job.result()
4counts = result.get_counts()
5
6plot_counts(counts, compiled_circuit_qr)
_images/8b43ce16001d45d06a77cf78b3b80efe9b00d2ef41c4e371954ce57b81a91ca2.svg
{'00': 326,
 '01': 0,
 '02': 4,
 '10': 0,
 '11': 326,
 '12': 0,
 '20': 0,
 '21': 2,
 '22': 342}
1passes = ["PhyLocAdaPass", "ZPropagationPass", "ZRemovalPass"]
2
3compiled_circuit_ada = qudit_compiler.compile(backend_ion, circuit, passes)
4
5print(f"Number of operations: {len(compiled_circuit_ada.instructions)}")
6print(f"Number of qudits in the circuit: {compiled_circuit_ada.num_qudits}")
Number of operations: 5
Number of qudits in the circuit: 2
1job = backend_ion.run(compiled_circuit_ada)
2
3result = job.result()
4counts = result.get_counts()
5
6plot_counts(counts, compiled_circuit_ada)
_images/86d32944e9dca0474a15e673a85d37642142b8a9e237d54c39ae436bcee9649d.svg
{'00': 321,
 '01': 0,
 '02': 4,
 '10': 2,
 '11': 328,
 '12': 0,
 '20': 0,
 '21': 3,
 '22': 342}
1from mqt.qudits.visualisation import draw_qudit_local
2
3draw_qudit_local(compiled_circuit_ada)
|0>-----[R01(1.57,-2.09)]----[R02(1.23,-2.62)]----[R02(3.14,-2.62)]----[R01(1.57,-0.52)]----MG-----=||
|0>-----MG----MG----MG----MG----MG-----=||