Usage from Python#

MQT DDSIM is available for multiple Python versions (>=3.8) from PyPI. See the Installation guide for more information on how to install MQT DDSIM.

Standalone Usage#

The classical simulation methods available in MQT DDSIM can be used in a standalone fashion. To this end, DDSIM currently offers four different kinds of simulators:

The CircuitSimulator#

The standard, Schrödinger-style simulator. Takes a circuit and sequentially simulates it using decision diagrams by successively applying the operations in the circuit. Can be used to obtain the full statevector of the circuit or to sample from the circuit.

[1]:
from qiskit import QuantumCircuit
from mqt.ddsim import CircuitSimulator

# A simple circuit that creates a Bell state
circ = QuantumCircuit(2)
circ.h(0)
circ.cx(0, 1)

# Create the simulator
sim = CircuitSimulator(circ)

# Simulate the circuit and sample 1024 shots from the resulting state
result = sim.simulate(shots=1024)
print(result)
{'00': 502, '11': 522}

Obtaining the full statevector is also possible:

[2]:
sv = sim.get_vector()
print(sv)
[(0.7071067811865476+0j), 0j, 0j, (0.7071067811865476+0j)]

If you want to inspect the final decision diagram, you can get a Graphviz representation of it. For that, make sure that you have Graphviz installed and that the graphviz Python package is available. A simple pip install graphviz should do the trick. Then, you can call the export_dd_to_graphviz_str method on the simulator to obtain a Graphviz representation of the decision diagram. The following shows the default configuration options for the export.

[3]:
import graphviz

dot = sim.export_dd_to_graphviz_str(colored=True, edge_labels=False, classic=False, memory=False, format_as_polar=True)

graphviz.Source(source=dot)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/backend/execute.py:79, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     78         kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
---> 79     proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
     80 else:

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/backend/execute.py:99, in _run_input_lines(cmd, input_lines, kwargs)
     98 def _run_input_lines(cmd, input_lines, *, kwargs):
---> 99     popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
    101     stdin_write = popen.stdin.write

File ~/.asdf/installs/python/3.11.4/lib/python3.11/subprocess.py:1026, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File ~/.asdf/installs/python/3.11.4/lib/python3.11/subprocess.py:1950, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1949         err_msg = os.strerror(errno_num)
-> 1950     raise child_exception_type(errno_num, err_msg, err_filename)
   1951 raise child_exception_type(err_msg)

FileNotFoundError: [Errno 2] No such file or directory: PosixPath('dot')

The above exception was the direct cause of the following exception:

ExecutableNotFound                        Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/IPython/core/formatters.py:974, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    971     method = get_real_method(obj, self.print_method)
    973     if method is not None:
--> 974         return method(include=include, exclude=exclude)
    975     return None
    976 else:

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in JupyterIntegration._repr_mimebundle_(self, include, exclude, **_)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in <dictcomp>(.0)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/jupyter_integration.py:112, in JupyterIntegration._repr_image_svg_xml(self)
    110 def _repr_image_svg_xml(self) -> str:
    111     """Return the rendered graph as SVG string."""
--> 112     return self.pipe(format='svg', encoding=SVG_ENCODING)

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/piping.py:104, in Pipe.pipe(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
     55 def pipe(self,
     56          format: typing.Optional[str] = None,
     57          renderer: typing.Optional[str] = None,
   (...)
     61          engine: typing.Optional[str] = None,
     62          encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
     63     """Return the source piped through the Graphviz layout command.
     64
     65     Args:
   (...)
    102         '<?xml version='
    103     """
--> 104     return self._pipe_legacy(format,
    105                              renderer=renderer,
    106                              formatter=formatter,
    107                              neato_no_op=neato_no_op,
    108                              quiet=quiet,
    109                              engine=engine,
    110                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/_tools.py:171, in deprecate_positional_args.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    162     wanted = ', '.join(f'{name}={value!r}'
    163                        for name, value in deprecated.items())
    164     warnings.warn(f'The signature of {func.__name__} will be reduced'
    165                   f' to {supported_number} positional args'
    166                   f' {list(supported)}: pass {wanted}'
    167                   ' as keyword arg(s)',
    168                   stacklevel=stacklevel,
    169                   category=category)
--> 171 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/piping.py:121, in Pipe._pipe_legacy(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    112 @_tools.deprecate_positional_args(supported_number=2)
    113 def _pipe_legacy(self,
    114                  format: typing.Optional[str] = None,
   (...)
    119                  engine: typing.Optional[str] = None,
    120                  encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
--> 121     return self._pipe_future(format,
    122                              renderer=renderer,
    123                              formatter=formatter,
    124                              neato_no_op=neato_no_op,
    125                              quiet=quiet,
    126                              engine=engine,
    127                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/piping.py:149, in Pipe._pipe_future(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    146 if encoding is not None:
    147     if codecs.lookup(encoding) is codecs.lookup(self.encoding):
    148         # common case: both stdin and stdout need the same encoding
--> 149         return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
    150     try:
    151         raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/backend/piping.py:212, in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, neato_no_op, quiet)
    206 cmd = dot_command.command(engine, format,
    207                           renderer=renderer,
    208                           formatter=formatter,
    209                           neato_no_op=neato_no_op)
    210 kwargs = {'input_lines': input_lines, 'encoding': encoding}
--> 212 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
    213 return proc.stdout

File ~/checkouts/readthedocs.org/user_builds/ddsim/envs/v1.20.1/lib/python3.11/site-packages/graphviz/backend/execute.py:84, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     82 except OSError as e:
     83     if e.errno == errno.ENOENT:
---> 84         raise ExecutableNotFound(cmd) from e
     85     raise
     87 if not quiet and proc.stderr:

ExecutableNotFound: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
[3]:
<graphviz.sources.Source at 0x7fe9bbb4d490>

The UnitarySimulator#

To come soon.

The HybridCircuitSimulator#

To come soon.

The PathSimulator#

To come soon.

Usage as Qiskit Backends#

The DDSIMProvider currently has seven backends

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

  • StatevectorSimulator simulates the circuit and returns the statevector

  • HybridQasmSimulator simulates a circuit in parallel using a hybrid Schrödinger-Feynman technique and generates the given number of shots

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

  • PathQasmSimulator 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.

[4]:
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
{'000': 49998, '111': 50002}

StatevectorSimulator for Observing the Statevector#

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

[5]:
# 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
[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.

[6]:
# 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
{'000': 49741, '111': 50259}

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.

[7]:
# 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
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#

[8]:
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
{'000': 49753, '111': 50247}

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

[9]:
# 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
[[ 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]]