{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ef64ecb9",
   "metadata": {},
   "source": [
    "# Usage from Python\n",
    "\n",
    "MQT DDSIM is available for multiple Python versions (>=3.7) from [PyPI](https://pypi.org/project/mqt.ddsim/). \n",
    "Using it as backend for [Qiskit](https://qiskit.org/) additionally requires at least [qiskit-terra](https://pypi.org/project/qiskit-terra/).\n",
    "\n",
    "In a [virtual environment](https://docs.python.org/3/tutorial/venv.html) you can use the following snippet:\n",
    "```\n",
    "$ python3 -m venv .venv\n",
    "$ . .venv/bin/activate\n",
    "(.venv) $ pip install -U pip setuptools wheel\n",
    "(.venv) $ pip install mqt.ddsim jupyter\n",
    "(.venv) $ jupyter notebook\n",
    "\n",
    "```\n",
    "\n",
    "The DDSIMProvider currently has five backends\n",
    "\n",
    "- `QasmSimulator` simulates a circuit and generates the given number of shots\n",
    "- `StatevectorSimulator` simulates the circuit and returns the statevector\n",
    "- `HybridQasmSimulator` simualtes a circuit in parallel using a hybrid Schrodinger-Feynman technique and generates the given number of shots\n",
    "- `HybridStatevectorSimulator` simulates the circuit in parallel using a hybrid Schrodinger-Feynman technique and returns the statevector\n",
    "- `PathQuasmSimulator` simulates a circuit by potential using a different order of multiplying operations and operation/state and generates the requested number of shots\n",
    "- `PathStatevectorSimulator` simulates a circuit by potential using a different order of multiplying operations and operation/state and returns the statevector\n",
    "- `UnitarySimulator` constructs the unitary functionality of a circuit and returns the corresponding unitary matrix"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b806c0fb",
   "metadata": {},
   "source": [
    "## QasmSimulator for Sampling\n",
    "\n",
    "The QasmSimulator-Backend takes a QuantumCircuit object and simulates it using decision diagrams in the underlying C++ implementation.\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47f47e0b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from qiskit import *\n",
    "\n",
    "from mqt import ddsim\n",
    "\n",
    "# Circuit to create a Bell state\n",
    "circ = QuantumCircuit(3)\n",
    "circ.h(0)\n",
    "circ.cx(0, 1)\n",
    "circ.cx(0, 2)\n",
    "circ.measure_all()\n",
    "\n",
    "# Show circuit\n",
    "print(circ.draw(fold=-1))\n",
    "\n",
    "provider = ddsim.DDSIMProvider()\n",
    "\n",
    "# get the QasmSimulator and sample 100000 times\n",
    "backend = provider.get_backend(\"qasm_simulator\")\n",
    "print(f\"Backend version: {backend.configuration().backend_version}\")\n",
    "job = execute(circ, backend, shots=100000)\n",
    "result = job.result()\n",
    "counts = result.get_counts(circ)\n",
    "print(counts)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ede16a7",
   "metadata": {},
   "source": [
    "## StatevectorSimulator for Observing the Statevector\n",
    "\n",
    "The StatevectorSimulator-Backend takes a QuantumCircuit as above but returns the state vector instead of a number of samples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d06285c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# get the StatevectorSimulator and calculate the statevector\n",
    "backend = provider.get_backend(\"statevector_simulator\")\n",
    "print(f\"Backend version: {backend.configuration().backend_version}\")\n",
    "job = execute(circ, backend)\n",
    "result = job.result()\n",
    "statevector = result.get_statevector(circ)\n",
    "print(statevector)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f08043a9",
   "metadata": {},
   "source": [
    "## HybridQasmSimulator for Sampling\n",
    "\n",
    "The HybridQasmSimulator-Backend takes a QuantumCircuit object and uses a hybrid Schrodinger-Feynman technique to simulate the circuit in parallel using decision diagrams.\n",
    "It currently assumes that no non-unitary operations (besides measurements at the end of the circuit) are present in the circuit.\n",
    "Furthermore it always measures all qubits at the end of the circuit in the order they were defined.\n",
    "\n",
    "The backend provides two different modes that can be set using the `mode` option:\n",
    "\n",
    " - `dd`: all computations are conducted on decision diagrams and the requested number of shots are sampled from the final decision diagram\n",
    " - `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.\n",
    "\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "620bcbc5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# get the HybridQasmSimulator and sample 100000 times using the amplitude mode and 4 threads\n",
    "backend = provider.get_backend(\"hybrid_qasm_simulator\")\n",
    "print(f\"Backend version: {backend.configuration().backend_version}\")\n",
    "job = execute(circ, backend, shots=100000, mode=\"amplitude\", nthreads=4)\n",
    "result = job.result()\n",
    "counts = result.get_counts(circ)\n",
    "print(counts)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48f4ce30",
   "metadata": {},
   "source": [
    "## HybridStatevectorSimulator for Observing the Statevector\n",
    "\n",
    "The HybridStatevectorSimulator-Backend provides the same options as the HybridQasmSimulator-Backend, but returns the final statevector as a result.\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c54a42fe",
   "metadata": {},
   "outputs": [],
   "source": [
    "# get the HybridStatevectorSimulator and calculate the statevector using the amplitude mode and 4 threads\n",
    "backend = provider.get_backend(\"hybrid_statevector_simulator\")\n",
    "print(f\"Backend version: {backend.configuration().backend_version}\")\n",
    "job = execute(circ, backend, mode=\"amplitude\", nthreads=4)\n",
    "result = job.result()\n",
    "statevector = result.get_statevector(circ)\n",
    "print(statevector)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1dbb7dbe",
   "metadata": {},
   "source": [
    "## PathQasmSimulator for Sampling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5d19cd6",
   "metadata": {},
   "outputs": [],
   "source": [
    "backend = provider.get_backend(\"path_sim_qasm_simulator\")\n",
    "print(f\"Backend version: {backend.configuration().backend_version}\")\n",
    "job = execute(circ, backend, shots=100000)  # uses the sequential strategy b\n",
    "result = job.result()\n",
    "counts = result.get_counts(circ)\n",
    "print(counts)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c72242ec",
   "metadata": {},
   "source": [
    "## UnitarySimulator for Constructing Functional Representations\n",
    "\n",
    "The UnitarySimulator-Backend takes a quantum circuit and constructs the corresponding unitary matrix using decision diagrams.\n",
    "\n",
    "The backend provides two different modes that can be set using the `mode` option:\n",
    "\n",
    " - `sequential`: construct the functionality in a sequential fashion multiplying all operations from left to right\n",
    " - `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"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1870ae6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# get the UnitarySimulator and calculate the unitary functionality using the recursive mode\n",
    "backend = provider.get_backend(\"unitary_simulator\")\n",
    "print(f\"Backend version: {backend.configuration().backend_version}\")\n",
    "job = execute(circ.remove_final_measurements(inplace=False), backend, mode=\"recursive\")\n",
    "result = job.result()\n",
    "unitary = result.get_unitary(circ)\n",
    "print(unitary)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
