Usage from Python

MQT DDSIM is available for multiple Python versions (>=3.7) from PyPI. Using it as backend for Qiskit additionally requires at least qiskit-terra.

In a virtual environment you can use the following snippet:

$ python3 -m venv .venv
$ . .venv/bin/activate
(.venv) $ pip install -U pip setuptools wheel
(.venv) $ pip install mqt.ddsim jupyter
(.venv) $ jupyter notebook

The DDSIMProvider currently has five backends

  • QasmSimulator simulates a circuit and generates the given number of shots

  • StatevectorSimulator simulates the circuit and returns the statevector

  • HybridQasmSimulator simualtes a circuit in parallel using a hybrid Schrodinger-Feynman technique and generates the given number of shots

  • HybridStatevectorSimulator simulates the circuit in parallel using a hybrid Schrodinger-Feynman technique and returns the statevector

  • PathQuasmSimulator simulates a circuit by potential using a different order of multiplying operations and operation/state and generates the requested number of shots

  • PathStatevectorSimulator simulates a circuit by potential using a different order of multiplying operations and operation/state and returns the statevector

  • UnitarySimulator constructs the unitary functionality of a circuit and returns the corresponding unitary matrix

QasmSimulator for Sampling

The QasmSimulator-Backend takes a QuantumCircuit object and simulates it using decision diagrams in the underlying C++ implementation. For circuits with no non-unitary operations (except for measurements at the end of the circuit) the simulation is only done once and the samples subsequently drawn from the decision diagram, resulting in fast runtime.

[1]:
from qiskit import *

from mqt import ddsim

# Circuit to create a Bell state
circ = QuantumCircuit(3)
circ.h(0)
circ.cx(0, 1)
circ.cx(0, 2)
circ.measure_all()

# Show circuit
print(circ.draw(fold=-1))

provider = ddsim.DDSIMProvider()

# get the QasmSimulator and sample 100000 times
backend = provider.get_backend("qasm_simulator")
print(f"Backend version: {backend.configuration().backend_version}")
job = execute(circ, backend, shots=100000)
result = job.result()
counts = result.get_counts(circ)
print(counts)
        ┌───┐           ░ ┌─┐
   q_0: ┤ H ├──■────■───░─┤M├──────
        └───┘┌─┴─┐  │   ░ └╥┘┌─┐
   q_1: ─────┤ X ├──┼───░──╫─┤M├───
             └───┘┌─┴─┐ ░  ║ └╥┘┌─┐
   q_2: ──────────┤ X ├─░──╫──╫─┤M├
                  └───┘ ░  ║  ║ └╥┘
meas: 3/═══════════════════╩══╩══╩═
                           0  1  2
Backend version: 1.17.1
{'000': 50286, '111': 49714}

StatevectorSimulator for Observing the Statevector

The StatevectorSimulator-Backend takes a QuantumCircuit as above but returns the state vector instead of a number of samples.

[2]:
# get the StatevectorSimulator and calculate the statevector
backend = provider.get_backend("statevector_simulator")
print(f"Backend version: {backend.configuration().backend_version}")
job = execute(circ, backend)
result = job.result()
statevector = result.get_statevector(circ)
print(statevector)
Backend version: 1.17.1
[0.70710678+0.j 0.        +0.j 0.        +0.j 0.        +0.j
 0.        +0.j 0.        +0.j 0.        +0.j 0.70710678+0.j]

HybridQasmSimulator for Sampling

The HybridQasmSimulator-Backend takes a QuantumCircuit object and uses a hybrid Schrodinger-Feynman technique to simulate the circuit in parallel using decision diagrams. It currently assumes that no non-unitary operations (besides measurements at the end of the circuit) are present in the circuit. Furthermore it always measures all qubits at the end of the circuit in the order they were defined.

The backend provides two different modes that can be set using the mode option:

  • dd: all computations are conducted on decision diagrams and the requested number of shots are sampled from the final decision diagram

  • amplitude: all individual paths in the hybrid simulation scheme are simulated using decision diagrams, while subsequent computations (addition of all results) is conducted using arrays. This requires more memory but can lead to significantly better runtime performance in many cases. The requested shots are sampled from the final statevector array.

The number of threads to use can be set using the nthreads option. Note that the number of threads may be reduced when using the amplitude mode in order to fit the computation in the available memory.

[3]:
# get the HybridQasmSimulator and sample 100000 times using the amplitude mode and 4 threads
backend = provider.get_backend("hybrid_qasm_simulator")
print(f"Backend version: {backend.configuration().backend_version}")
job = execute(circ, backend, shots=100000, mode="amplitude", nthreads=4)
result = job.result()
counts = result.get_counts(circ)
print(counts)
Backend version: 1.17.1
{'000': 50032, '111': 49968}

HybridStatevectorSimulator for Observing the Statevector

The HybridStatevectorSimulator-Backend provides the same options as the HybridQasmSimulator-Backend, but returns the final statevector as a result. Note that shots has to be set to 0 when using the amplitude mode as the statevector array is modified in-place for sampling and, hence, the state vector is no longer available afterwards.

[4]:
# get the HybridStatevectorSimulator and calculate the statevector using the amplitude mode and 4 threads
backend = provider.get_backend("hybrid_statevector_simulator")
print(f"Backend version: {backend.configuration().backend_version}")
job = execute(circ, backend, mode="amplitude", nthreads=4)
result = job.result()
statevector = result.get_statevector(circ)
print(statevector)
Backend version: 1.17.1
[0.70710678+0.j 0.        +0.j 0.        +0.j 0.        +0.j
 0.        +0.j 0.        +0.j 0.        +0.j 0.70710678+0.j]

PathQasmSimulator for Sampling

[5]:
backend = provider.get_backend("path_sim_qasm_simulator")
print(f"Backend version: {backend.configuration().backend_version}")
job = execute(circ, backend, shots=100000)  # uses the sequential strategy b
result = job.result()
counts = result.get_counts(circ)
print(counts)
Backend version: 1.17.1
{'000': 49741, '111': 50259}

UnitarySimulator for Constructing Functional Representations

The UnitarySimulator-Backend takes a quantum circuit and constructs the corresponding unitary matrix using decision diagrams.

The backend provides two different modes that can be set using the mode option:

  • sequential: construct the functionality in a sequential fashion multiplying all operations from left to right

  • recursive: construct the functionality recursively by grouping operations in a tree-like fashion. This might require a little more memory, but significantly less runtime in many cases

[6]:
# get the UnitarySimulator and calculate the unitary functionality using the recursive mode
backend = provider.get_backend("unitary_simulator")
print(f"Backend version: {backend.configuration().backend_version}")
job = execute(circ.remove_final_measurements(inplace=False), backend, mode="recursive")
result = job.result()
unitary = result.get_unitary(circ)
print(unitary)
Backend version: 1.17.1
[[ 0.70710678+0.j  0.70710678+0.j  0.        +0.j  0.        +0.j
   0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j]
 [ 0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
   0.        +0.j  0.        +0.j  0.70710678+0.j -0.70710678+0.j]
 [ 0.        +0.j  0.        +0.j  0.70710678+0.j  0.70710678+0.j
   0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j]
 [ 0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
   0.70710678+0.j -0.70710678+0.j  0.        +0.j  0.        +0.j]
 [ 0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
   0.70710678+0.j  0.70710678+0.j  0.        +0.j  0.        +0.j]
 [ 0.        +0.j  0.        +0.j  0.70710678+0.j -0.70710678+0.j
   0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j]
 [ 0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j
   0.        +0.j  0.        +0.j  0.70710678+0.j  0.70710678+0.j]
 [ 0.70710678+0.j -0.70710678+0.j  0.        +0.j  0.        +0.j
   0.        +0.j  0.        +0.j  0.        +0.j  0.        +0.j]]