Usage from Python#
MQT DDSIM is available for multiple Python versions (>=3.8) 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 shotsStatevectorSimulator
simulates the circuit and returns the statevectorHybridQasmSimulator
simualtes a circuit in parallel using a hybrid Schrodinger-Feynman technique and generates the given number of shotsHybridStatevectorSimulator
simulates the circuit in parallel using a hybrid Schrodinger-Feynman technique and returns the statevectorPathQuasmSimulator
simulates a circuit by potential using a different order of multiplying operations and operation/state and generates the requested number of shotsPathStatevectorSimulator
simulates a circuit by potential using a different order of multiplying operations and operation/state and returns the statevectorUnitarySimulator
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.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.20.1.dev4+g62a50dc
{'000': 49909, '111': 50091}
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.backend_version}")
job = execute(circ, backend)
result = job.result()
statevector = result.get_statevector(circ)
print(statevector)
Backend version: 1.20.1.dev4+g62a50dc
[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 diagramamplitude
: 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.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.20.1.dev4+g62a50dc
{'000': 50311, '111': 49689}
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.backend_version}")
job = execute(circ, backend, mode="amplitude", nthreads=4)
result = job.result()
statevector = result.get_statevector(circ)
print(statevector)
Backend version: 1.20.1.dev4+g62a50dc
Statevector can only be shown if shots == 0 when using the amplitude hybrid simulation mode.
[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.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.20.1.dev4+g62a50dc
{'000': 49911, '111': 50089}
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 rightrecursive
: 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.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.20.1.dev4+g62a50dc
[[ 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]]