Skip to content

Commit

Permalink
📝 Update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
TeWas committed Oct 27, 2024
1 parent 668589e commit 380d0aa
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 6 deletions.
248 changes: 248 additions & 0 deletions docs/source/ApproximateEquivalence.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "77a131e6",
"metadata": {},
"source": [
"# Approximate Equivalence Checking\n",
"\n",
"## Approximate Equivalence vs. Exact Equivalence\n",
"\n",
"Two circuits are considered exactly equivalent if their matrix representations, `U` and `V`, are identical. Thus, the problem of exact equivalence checking reduces to verifying whether $UV^\\dagger = I$, where `I` is the identity matrix.\n",
"\n",
"In practice, however, it is often sufficient to determine whether two circuits are equivalent up to a certain percentage, while permitting an error tolerance. For instance, when optimizing circuits, we may not require exact equivalence, but rather a circuit that is sufficiently close to the original, while being optimal with respect to a specific metric, such as minimizing gate counts.\n",
"\n",
"Here, approximate equivalence is quantified by the process distance between two circuits based on the Hilbert-Schmidt inner product, with n being the number of qubits: $$\\Delta(U, V) = 1 - |\\frac{Tr(U V^\\dagger)}{2^n}|$$ Specifically, we compute the magnitude of the normalized trace of $UV^\\dagger$ and assess its proximity to 1. Two circuits are considered approximately equivalent if their process distance is below a specified error threshold, $\\epsilon$.\n",
"\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "b9be796e",
"metadata": {},
"source": [
"\n",
"## Checking Approximate Equivalence of Quantum Circuits\n",
"\n",
"To enable approximate equivalence checking, set the `check_approximate_equivalence` option to `True` in the configuration parameters. When activated, the equivalence checker will return `equivalent` if the process distance is below a specified threshold. Otherwise, it will return `not-equivalent`. The error threshold is controlled via the `approximate_checking_threshold` parameter, with a default value of `1e-8`. Moreover, it is recommended to disable the simulation checker by setting `run_simulation_checker` to `False`.\n",
"\n",
"The following is a summary of the behaviour of each type of equivalence checker when the `checkApproximateEquivalence` option is set to `True`.\n",
"\n",
"1. **Construction Equivalence Checker:** The construction checker supports approximate equivalence checking by using decision diagrams to compute $UV^\\dagger$. It then calculates the normalized trace and compares the process distance to the defined error threshold. \n",
"\n",
"1. **Alternating Equivalence Checker:** The alternating checker likewise computes the composition of one circuit with the inverse of the other. Again, when checking for approximate equivalence, it suffices to verify that the normalized trace is sufficiently close to 1. \n",
"\n",
"1. **Simulation Equivalence Checker:** Approximate equivalence checking is not directly supported by the Simulation checker. Although numerical instabilities can be mitigated through the configuration parameter `simulation.fidelityThreshold`, this parameter does not serve as an approximate equivalence threshold. For instance, consider $UV^\\dagger$ representing a 10-qubit multi-controlled X-gate with the first 9 qubits as control and the last qubit as the target. In this setup, the process distance would meet an error threshold of $1e^-2$. However, since the Simulation checker evaluates both circuits using random input states, the fidelity drops to zero whenever one of the two computational basis states, $|1111111110\\rangle$ or $|1111111111\\rangle$, is selected. In such cases, regardless of any threshold setting, the checker reports non-equivalence.\n",
"\n",
"1. **ZX-Calculus Equivalence Checker:** Approximate equivalence checking is not yet supported in the ZX-calculus checker, which is not a problem for the equivalence checking workflow, given that the ZX-calculus checker cannot demonstrate non-equivalence of circuits due to its incompleteness. \n",
"Therefore, it will simply output 'No Information' for circuits that are approximately but not totally equivalent.\n",
"\n",
"1. **Hybrid Schrödinger-Feynman (HSF) Equivalence Checker:** By default, the HSF checker is disabled. It can only be used for approximate equivalence checking. To enable it, the `check_approximate_equivalence` option must also be activated. The HSF checker computes the process distance by dividing the circuit corresponding to $UV^\\dagger$ horizontally into two independent halves: a lower part and an upper part. This is achieved by decomposing controlled gates, acting across both halves, according to the Schmidt decomposition.\n",
"By leveraging key trace equalities - specifically,\n",
"\n",
" • $tr[L⊗U]=tr[L]⋅tr[U]$\n",
"\n",
" • $tr[P_1+P_2]=tr[P_1]+tr[P_2]$\n",
"\n",
" we can treat the lower and upper circuit parts, as well as the summands from the Schmidt decomposition, independently. This enables parallel trace computation, allowing to check the equivalence of larger, yet shallow circuits.\n",
" \n",
" Note: The following configurations are not currently supported, as they require the explicit computation of the Schmidt decomposition for the gates being cut (decisions):\n",
"\n",
" • Multiple targets spread across the cut through the circuit\n",
" • Multiple controls in the control part of the gate being cut \n",
" \n",
" Moreover, despite parallelization, the method is best suited for shallow circuits, as the number of paths to be computed grows exponentially with the number of decisions. "
]
},
{
"cell_type": "markdown",
"id": "977071d9",
"metadata": {},
"source": [
"## Using QCEC to Check for Approximate Equivalence\n",
"\n",
"Consider the following three-qubit circuit representing the Toffoli gate."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "03b73b17",
"metadata": {},
"outputs": [],
"source": [
"from qiskit import QuantumCircuit\n",
"\n",
"qc_lhs = QuantumCircuit(3)\n",
"qc_lhs.mcx([0, 1], 2)\n",
"qc_lhs.measure_all()\n",
"qc_lhs.draw(output=\"mpl\", style=\"iqp\")"
]
},
{
"cell_type": "markdown",
"id": "10fff8c7-f36c-422e-8deb-796cc7637e45",
"metadata": {},
"source": [
"Additionally, consider the following simple circuit, which represents the Identity."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d167869e-e082-4a42-9eab-112931f5c697",
"metadata": {},
"outputs": [],
"source": [
"qc_rhs = QuantumCircuit(3)\n",
"qc_rhs.id(0)\n",
"qc_rhs.id(1)\n",
"qc_rhs.id(2)\n",
"qc_rhs.measure_all()\n",
"qc_rhs.draw(output=\"mpl\", style=\"iqp\")"
]
},
{
"cell_type": "markdown",
"id": "3b85338e",
"metadata": {},
"source": [
"We now aim to compute the process distance between these circuits. Since the second circuit represents the Identity, it follows that $UV^\\dagger$ simplifies to the Toffoli gate matrix. \n",
"\n",
"$$\n",
"\\text{Toffoli} = \\begin{bmatrix}\n",
"1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\\n",
"0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\\\\n",
"0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\\\\n",
"0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\\\\n",
"0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\\\\n",
"0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\\\\n",
"0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\\\\n",
"0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\\\\n",
"\\end{bmatrix}\n",
"$$\n",
"\n",
"The normalized trace of this matrix is `0.75`. \n",
"\n",
"Consequently, the process distance $\\Delta(U, V) = 1 - |\\frac{Tr(U V^\\dagger)}{2^n}|$ is equal to `0.25`.\n",
"\n",
"If we set the equivalence tolerance to `0.3`, the checker should output `equivalent`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "36df44b7",
"metadata": {},
"outputs": [],
"source": [
"from mqt.qcec import Configuration, verify\n",
"\n",
"config = Configuration()\n",
"config.execution.run_simulation_checker = False\n",
"config.functionality.check_approximate_equivalence = True\n",
"config.functionality.approximate_checking_threshold = 0.3\n",
"verify(qc_lhs, qc_rhs, configuration=config)"
]
},
{
"cell_type": "markdown",
"id": "f2c4104d",
"metadata": {},
"source": [
"However, with an error tolerance below `0.25`, the checker should return `not_equivalent`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "da9fa188",
"metadata": {},
"outputs": [],
"source": [
"config = Configuration()\n",
"config.execution.run_simulation_checker = False\n",
"config.functionality.check_approximate_equivalence = True\n",
"config.functionality.approximate_checking_threshold = 0.2\n",
"verify(qc_lhs, qc_rhs, configuration=config)"
]
},
{
"cell_type": "markdown",
"id": "5d7f9c62",
"metadata": {},
"source": [
"To use the HSF checker, it must be explicitly enabled in the configuration. Below, we will explicitly use the HSF checker and disable the other checkers."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "494ef477",
"metadata": {},
"outputs": [],
"source": [
"config.execution.run_simulation_checker = False\n",
"config.execution.run_alternating_checker = False\n",
"config.execution.run_construction_checker = False\n",
"config.execution.run_zx_checker = False\n",
"config.execution.run_hsf_checker = True\n",
"config.functionality.check_approximate_equivalence = False\n",
"config.functionality.approximate_checking_threshold = 0.3\n",
"verify(qc_lhs, qc_rhs, configuration=config)"
]
},
{
"cell_type": "markdown",
"id": "b327ab4b",
"metadata": {},
"source": [
"However, we also need to explicitly enable the `check_approximate_equivalence` option."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "679291b7",
"metadata": {},
"outputs": [],
"source": [
"config.functionality.check_approximate_equivalence = True\n",
"config.functionality.approximate_checking_threshold = 0.3\n",
"verify(qc_lhs, qc_rhs, configuration=config)"
]
},
{
"cell_type": "markdown",
"id": "6af54b68",
"metadata": {},
"source": [
"Source: The concept of approximate equivalence used here is inspired by [Approximate Equivalence Checking of Noisy Quantum Circuits](https://arxiv.org/abs/2103.11595) by Xin Hong, Mingsheng Ying, Yuan Feng, Xiangzhen Zhou, Sanjiang Li, and [QUEST: systematically approximating Quantum circuits for higher output fidelity](https://dl.acm.org/doi/10.1145/3503222.3507739) by Tirthak Patel, Ed Younis, Costin Iancu, Wibe de Jong, and Devesh Tiwari."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "venv",
"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
}
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ We appreciate any feedback and contributions to the project. If you want to cont
CompilationFlowVerification
ParameterizedCircuits
PartialEquivalence
ApproximateEquivalence
Publications

.. toctree::
Expand Down
12 changes: 6 additions & 6 deletions include/checker/dd/HybridSchrodingerFeynmanChecker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ namespace ec {
/**
* @brief Approximate Equivalence Checking with the
* HybridSchrodingerFeynmanChecker This checker divides a circuit horizontally
* into two halves: a lower part and an upper part. This is achieved by cutting
* controlled gates that cross the middle line according to the Schmidt
* decomposition. By leveraging key trace equalities - specifically,
* into two halves: a lower part and an upper part. This is achieved by
* decomposing controlled gates, acting across both halves, according to the
* Schmidt decomposition. By leveraging key trace equalities - specifically,
*
* tr[L ⊗ U] = tr[L] ⋅ tr[U]
*
Expand All @@ -39,7 +39,7 @@ namespace ec {
* computation, allowing to check the equivalence of larger, yet shallow
* circuits.
* @note Only suitable for shallow circuits with a maximum number of 63
* controlled gates crossing the middle line (decisions).
* controlled gates acting on both circuit parts (decisions).
*/
class HybridSchrodingerFeynmanChecker final
: public DDEquivalenceChecker<qc::MatrixDD, dd::DDPackageConfig> {
Expand Down Expand Up @@ -81,7 +81,7 @@ class HybridSchrodingerFeynmanChecker final
* @brief Get # of decisions for given split_qubit, so that lower slice: q0 <
* i < qubit; upper slice: qubit <= i < nqubits
* @details The number of decisions is determined by the number of controlled
* gates that cross the middle line.
* gates that operate across both halves.
* @param qc
* @return std::size_t
*/
Expand Down Expand Up @@ -110,7 +110,7 @@ class HybridSchrodingerFeynmanChecker final
* into a sum of circuits, each consisting of only single-qubit gates. By
* recursively applying this decomposition to all decisions, we generate a
* total of 2^decisions circuits, which do not contain controlled operations
* crossing the middle line. This enables independent investigation of the
* acting on both halves. This enables independent investigation of the
* lower and upper circuit parts.
*
* This function computes the trace for the i-th summand, where the index 'i'
Expand Down

0 comments on commit 380d0aa

Please sign in to comment.