Repository: Qiskit/qiskit — pull request #9441 (python)
Title: Fix `SolovayKitaev` for roundoff errors in Qiskit gates

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\n\r\nFixes #9437.\r\n\r\n\r\n

Resolved issues:
title: Solovay-Kitaev, ValueError: Input matrix is not orthogonal.
body: ### Environment\r\n\r\n- **Commit hash**:  dc8e232a\r\n- **Python version**: 3.8.7\r\n- **Operating system**: Ubuntu 20.04.5 LTS\r\n- **Qiskit Terra version**: 0.24.0\r\n\r\n### What is happening?\r\n\r\nI'm trying to run the newly added SolovayKitaev (SK) algorithm. For some simple qiskit circuits the SK works, but most of the time it produces the following error:\r\n\r\n```\r\n---------------------------------------------------------------------------\r\nValueError                                Traceback (most recent call last)\r\nCell In[13], line 15\r\n     12 # Time computation\r\n     13 start_time = time.time()\r\n---> 15 discretized = skd(transpiled_circ_qasm)\r\n     17 print("\\t Time of conversion:", (time.time() - start_time) / 60, " min")\r\n\r\nFile ~/.pyenv/versions/circuit_comp_qiskit_dev/lib/python3.8/site-packages/qiskit/transpiler/basepasses.py:121, in BasePass.__call__(self, circuit, property_set)\r\n    118 if isinstance(property_set_, PropertySet):\r\n    119     self.property_set = property_set_\r\n--> 121 result = self.run(circuit_to_dag(circuit))\r\n    123 result_circuit = circuit\r\n    125 if isinstance(property_set, dict):  # this includes (dict, PropertySet)\r\n\r\nFile ~/.pyenv/versions/circuit_comp_qiskit_dev/lib/python3.8/site-packages/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py:158, in SolovayKitaev.run(self, dag)\r\n    155 matrix = node.op.to_matrix()\r\n    157 # call solovay kitaev\r\n--> 158 approximation = self._sk.run(matrix, self.recursion_degree, return_dag=True)\r\n    160 # convert to a dag and replace the gate by the approximation\r\n    161 dag.substitute_node_with_dag(node, approximation)\r\n\r\nFile ~/.pyenv/versions/circuit_comp_qiskit_dev/lib/python3.8/site-packages/qiskit/synthesis/discrete_basis/solovay_kitaev.py:107, in SolovayKitaevDecomposition.run(self, gate_matrix, recursion_degree, return_dag)\r\n    104 global_phase = np.arctan2(np.imag(z), np.real(z))\r\n    106 # get the decompositon as GateSequence type\r\n--> 107 decomposition = self._recurse(gate_matrix_su2, recursion_degree)\r\n    109 # simplify\r\n    110 _remove_identities(decomposition)\r\n\r\nFile ~/.pyenv/versions/circuit_comp_qiskit_dev/lib/python3.8/site-packages/qiskit/synthesis/discrete_basis/solovay_kitaev.py:146, in SolovayKitaevDecomposition._recurse(self, sequence, n)\r\n    142 u_n1 = self._recurse(sequence, n - 1)\r\n    144 v_n, w_n = commutator_decompose(sequence.dot(u_n1.adjoint()).product)\r\n--> 146 v_n1 = self._recurse(v_n, n - 1)\r\n    147 w_n1 = self._recurse(w_n, n - 1)\r\n    148 return v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1)\r\n\r\nFile ~/.pyenv/versions/circuit_comp_qiskit_dev/lib/python3.8/site-packages/qiskit/synthesis/discrete_basis/solovay_kitaev.py:144, in SolovayKitaevDecomposition._recurse(self, sequence, n)\r\n    140     return self.find_basic_approximation(sequence)\r\n    142 u_n1 = self._recurse(sequence, n - 1)\r\n--> 144 v_n, w_n = commutator_decompose(sequence.dot(u_n1.adjoint()).product)\r\n    146 v_n1 = self._recurse(v_n, n - 1)\r\n    147 w_n1 = self._recurse(w_n, n - 1)\r\n\r\nFile ~/.pyenv/versions/circuit_comp_qiskit_dev/lib/python3.8/site-packages/qiskit/synthesis/discrete_basis/commutator_decompose.py:224, in commutator_decompose(u_so3, check_input)\r\n    220     identity = np.identity(3)\r\n    221     if not (\r\n    222         np.allclose(u_so3.dot(u_so3.T), identity) and np.allclose(u_so3.T.dot(u_so3), identity)\r\n    223     ):\r\n--> 224         raise ValueError("Input matrix is not orthogonal.")\r\n    226 angle = _solve_decomposition_angle(u_so3)\r\n    228 # Compute rotation about x-axis with angle 'angle'\r\n\r\nValueError: Input matrix is not orthogonal.\r\n```\r\n\r\n### How can we reproduce the issue?\r\n\r\nSince I can't share the qiskit circuit, I will upload its QASM file which contains about 100 (out of few millions) gates  of the original circuit. This QASM file is enough to replicate the error.\r\n\r\nFirst, I will give the code:\r\n```\r\n# Load the QASM file\r\ntranspiled_circ_qasm = QuantumCircuit.from_qasm_file(filename)\r\n\r\n# Do SK pass\r\ndepth = 4\r\ngate_approx_library = generate_basic_approximations(\r\n    basis_gates=["h", "t", "tdg", "s", "z"], depth=depth\r\n)\r\n\r\nskd = SolovayKitaev(recursion_degree=2, basic_approximations=gate_approx_library)\r\ndiscretized = skd(transpiled_circ_qasm)\r\n\r\n``` \r\n\r\nHere is the partial QASM file generated from  a qiskit circuit:\r\n```\r\nOPENQASM 2.0;\r\ninclude "qelib1.inc";\r\nqreg eigenvalue_ancilla_register[1];\r\nqreg eigenvalue_register[4];\r\nqreg coin_register[3];\r\nqreg coin_ancilla_register[1];\r\nqreg power_register[8];\r\nqreg state_register[8];\r\ncreg classical_register[8];\r\nu3(pi/2,-pi,-pi) eigenvalue_ancilla_register[0];\r\nu3(pi/2,pi/2,-pi) eigenvalue_register[0];\r\nu3(pi/2,pi/2,-pi) eigenvalue_register[1];\r\nu3(pi/2,pi/2,-pi) eigenvalue_register[2];\r\nu3(pi/2,pi/2,-pi) eigenvalue_register[3];\r\nu3(pi/2,pi/2,-pi) coin_register[0];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu3(-pi/4,0,-pi/2) coin_register[0];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu3(pi/4,-pi/16,0) coin_register[0];\r\nu3(0,0,pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu3(0,0,pi/16) coin_register[0];\r\nu3(0,pi/4,pi/4) eigenvalue_register[0];\r\nu3(pi/2,pi/2,-pi) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(-pi/4,0,-pi/2) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(pi/4,-pi/16,0) coin_register[1];\r\nu3(0,0,pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(pi/2,0,-15*pi/16) coin_register[1];\r\ncx coin_register[0],coin_register[1];\r\nu1(-pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu1(pi/4) coin_register[1];\r\ncx coin_register[0],coin_register[1];\r\nu1(pi/4) coin_register[0];\r\nu1(-pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(pi/2,0,-3*pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu1(-pi/4) coin_register[0];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu3(0,0,-pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(0,0,pi/16) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(pi/2,0,15*pi/16) coin_register[1];\r\ncx coin_register[0],coin_register[1];\r\nu1(-pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu1(pi/4) coin_register[1];\r\ncx coin_register[0],coin_register[1];\r\nu1(pi/4) coin_register[0];\r\nu1(-pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(pi/2,0,-3*pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu1(-pi/4) coin_register[0];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu3(0,pi/4,pi/4) eigenvalue_register[0];\r\nu3(pi/2,pi/2,-pi) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(-pi/4,0,-pi/2) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/4,-pi/16,0) coin_register[2];\r\nu3(0,0,pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,-15*pi/16) coin_register[2];\r\ncx coin_register[1],coin_register[2];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu1(pi/4) coin_register[2];\r\ncx coin_register[1],coin_register[2];\r\nu1(pi/4) coin_register[1];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,-3*pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu1(-pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(0,0,-pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(0,0,pi/16) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,15*pi/16) coin_register[2];\r\ncx coin_register[0],coin_register[2];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu1(pi/4) coin_register[2];\r\ncx coin_register[0],coin_register[2];\r\nu1(pi/4) coin_register[0];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,-3*pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu1(-pi/4) coin_register[0];\r\ncx eigenvalue_register[0],coin_register[0];\r\nu3(0,0,pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(0,0,-pi/16) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,-15*pi/16) coin_register[2];\r\ncx coin_register[1],coin_register[2];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu1(pi/4) coin_register[2];\r\ncx coin_register[1],coin_register[2];\r\nu1(pi/4) coin_register[1];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,-3*pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu1(-pi/4) coin_register[1];\r\ncx eigenvalue_register[0],coin_register[1];\r\nu3(0,0,-pi/16) eigenvalue_register[0];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(0,0,pi/16) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,15*pi/16) coin_register[2];\r\ncx coin_register[0],coin_register[2];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu1(pi/4) coin_register[2];\r\ncx coin_register[0],coin_register[2];\r\nu1(pi/4) coin_register[0];\r\nu1(-pi/4) coin_register[2];\r\ncx eigenvalue_register[0],coin_register[2];\r\nu3(pi/2,0,-3*pi/4) coin_register[2];\r\n```\r\n\r\n### What should happen?\r\n\r\nI would like to emphasize that even if I feed the circuit directly into SK (without loading it as QASM), the SK algorithm still produces the error. The original circuit was created exclusively with qiskit's default toolset and it runs on qiskit simulators without problems.\r\n\r\nThe expectation is that the following lines of code should produce a new quantum circuit which is comprised of $S, T, T^*, Z, CNOT$ and $H$ gates. Instead there is an error `ValueError: Input matrix is not orthogonal.`\r\n\r\n```\r\nskd = SolovayKitaev(recursion_degree=2, basic_approximations=gate_approx_library)\r\ndiscretized = skd(transpiled_circ_qasm)\r\n```\r\n\r\n### Any suggestions?\r\n\r\nI can only kindly ask to fix this issue ASAP.

Hints:

Fix patch:
diff --git a/qiskit/synthesis/discrete_basis/commutator_decompose.py b/qiskit/synthesis/discrete_basis/commutator_decompose.py\nindex c50a19dfa35e..10622e76e2c9 100644\n--- a/qiskit/synthesis/discrete_basis/commutator_decompose.py\n+++ b/qiskit/synthesis/discrete_basis/commutator_decompose.py\n@@ -16,6 +16,7 @@\n \n import math\n import numpy as np\n+from qiskit.quantum_info.operators.predicates import is_identity_matrix\n from .gate_sequence import _check_is_so3, GateSequence\n \n \n@@ -217,10 +218,7 @@ def commutator_decompose(\n         # assert that the input matrix is really SO(3)\n         _check_is_so3(u_so3)\n \n-        identity = np.identity(3)\n-        if not (\n-            np.allclose(u_so3.dot(u_so3.T), identity) and np.allclose(u_so3.T.dot(u_so3), identity)\n-        ):\n+        if not is_identity_matrix(u_so3.dot(u_so3.T)):\n             raise ValueError("Input matrix is not orthogonal.")\n \n     angle = _solve_decomposition_angle(u_so3)\ndiff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py\nindex cd470265f348..62ad50582d40 100644\n--- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py\n+++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py\n@@ -86,14 +86,20 @@ def load_basic_approximations(self, data: list | str | dict) -> list[GateSequenc\n         return sequences\n \n     def run(\n-        self, gate_matrix: np.ndarray, recursion_degree: int, return_dag: bool = False\n+        self,\n+        gate_matrix: np.ndarray,\n+        recursion_degree: int,\n+        return_dag: bool = False,\n+        check_input: bool = True,\n     ) -> "QuantumCircuit" | "DAGCircuit":\n         r"""Run the algorithm.\n \n         Args:\n-            gate_matrix: The 2x2 matrix representing the gate. Does not need to be SU(2).\n+            gate_matrix: The 2x2 matrix representing the gate. This matrix has to be SU(2)\n+                up to global phase.\n             recursion_degree: The recursion degree, called :math:`n` in the paper.\n             return_dag: If ``True`` return a :class:`.DAGCircuit`, else a :class:`.QuantumCircuit`.\n+            check_input: If ``True`` check that the input matrix is valid for the decomposition.\n \n         Returns:\n             A one-qubit circuit approximating the ``gate_matrix`` in the specified discrete basis.\n@@ -104,7 +110,7 @@ def run(\n         global_phase = np.arctan2(np.imag(z), np.real(z))\n \n         # get the decompositon as GateSequence type\n-        decomposition = self._recurse(gate_matrix_su2, recursion_degree)\n+        decomposition = self._recurse(gate_matrix_su2, recursion_degree, check_input=check_input)\n \n         # simplify\n         _remove_identities(decomposition)\n@@ -120,18 +126,20 @@ def run(\n \n         return out\n \n-    def _recurse(self, sequence: GateSequence, n: int) -> GateSequence:\n+    def _recurse(self, sequence: GateSequence, n: int, check_input: bool = True) -> GateSequence:\n         """Performs ``n`` iterations of the Solovay-Kitaev algorithm on ``sequence``.\n \n         Args:\n-            sequence: GateSequence to which the Solovay-Kitaev algorithm is applied.\n-            n: number of iterations that the algorithm needs to run.\n+            sequence: ``GateSequence`` to which the Solovay-Kitaev algorithm is applied.\n+            n: The number of iterations that the algorithm needs to run.\n+            check_input: If ``True`` check that the input matrix represented by ``GateSequence``\n+                is valid for the decomposition.\n \n         Returns:\n             GateSequence that approximates ``sequence``.\n \n         Raises:\n-            ValueError: if ``u`` does not represent an SO(3)-matrix.\n+            ValueError: If the matrix in ``GateSequence`` does not represent an SO(3)-matrix.\n         """\n         if sequence.product.shape != (3, 3):\n             raise ValueError("Shape of U must be (3, 3) but is", sequence.shape)\n@@ -139,12 +147,14 @@ def _recurse(self, sequence: GateSequence, n: int) -> GateSequence:\n         if n == 0:\n             return self.find_basic_approximation(sequence)\n \n-        u_n1 = self._recurse(sequence, n - 1)\n+        u_n1 = self._recurse(sequence, n - 1, check_input=check_input)\n \n-        v_n, w_n = commutator_decompose(sequence.dot(u_n1.adjoint()).product)\n+        v_n, w_n = commutator_decompose(\n+            sequence.dot(u_n1.adjoint()).product, check_input=check_input\n+        )\n \n-        v_n1 = self._recurse(v_n, n - 1)\n-        w_n1 = self._recurse(w_n, n - 1)\n+        v_n1 = self._recurse(v_n, n - 1, check_input=check_input)\n+        w_n1 = self._recurse(w_n, n - 1, check_input=check_input)\n         return v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1)\n \n     def find_basic_approximation(self, sequence: GateSequence) -> Gate:\ndiff --git a/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py b/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py\nindex feb40ad53f04..43ae079b9fc2 100644\n--- a/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py\n+++ b/qiskit/transpiler/passes/synthesis/solovay_kitaev_synthesis.py\n@@ -18,6 +18,7 @@\n import numpy as np\n \n from qiskit.converters import circuit_to_dag\n+from qiskit.circuit.gate import Gate\n from qiskit.dagcircuit import DAGCircuit\n from qiskit.synthesis.discrete_basis.solovay_kitaev import SolovayKitaevDecomposition\n from qiskit.synthesis.discrete_basis.generate_basis_approximations import (\n@@ -146,16 +147,21 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:\n             if not node.op.num_qubits == 1:\n                 continue  # ignore all non-single qubit gates\n \n+            # we do not check the input matrix as we know it comes from a Qiskit gate, as this\n+            # we know it will generate a valid SU(2) matrix\n+            check_input = not isinstance(node.op, Gate)\n+\n             if not hasattr(node.op, "to_matrix"):\n                 raise TranspilerError(\n-                    "SolovayKitaev does not support gate without "\n-                    f"to_matrix method: {node.op.name}"\n+                    f"SolovayKitaev does not support gate without to_matrix method: {node.op.name}"\n                 )\n \n             matrix = node.op.to_matrix()\n \n             # call solovay kitaev\n-            approximation = self._sk.run(matrix, self.recursion_degree, return_dag=True)\n+            approximation = self._sk.run(\n+                matrix, self.recursion_degree, return_dag=True, check_input=check_input\n+            )\n \n             # convert to a dag and replace the gate by the approximation\n             dag.substitute_node_with_dag(node, approximation)\n

Test patch:
diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py\nindex 95dfd06763d0..d3d85b8cfb76 100644\n--- a/test/python/transpiler/test_solovay_kitaev.py\n+++ b/test/python/transpiler/test_solovay_kitaev.py\n@@ -202,6 +202,35 @@ def test_approximation_on_qft(self):\n             discretized = skd(transpiled)\n             self.assertLess(_trace_distance(transpiled, discretized), 7)\n \n+    def test_u_gates_work(self):\n+        """Test SK works on Qiskit's UGate.\n+\n+        Regression test of Qiskit/qiskit-terra#9437.\n+        """\n+        circuit = QuantumCircuit(1)\n+        circuit.u(np.pi / 2, -np.pi, -np.pi, 0)\n+        circuit.u(np.pi / 2, np.pi / 2, -np.pi, 0)\n+        circuit.u(-np.pi / 4, 0, -np.pi / 2, 0)\n+        circuit.u(np.pi / 4, -np.pi / 16, 0, 0)\n+        circuit.u(0, 0, np.pi / 16, 0)\n+        circuit.u(0, np.pi / 4, np.pi / 4, 0)\n+        circuit.u(np.pi / 2, 0, -15 * np.pi / 16, 0)\n+        circuit.p(-np.pi / 4, 0)\n+        circuit.p(np.pi / 4, 0)\n+        circuit.u(np.pi / 2, 0, -3 * np.pi / 4, 0)\n+        circuit.u(0, 0, -np.pi / 16, 0)\n+        circuit.u(np.pi / 2, 0, 15 * np.pi / 16, 0)\n+\n+        depth = 4\n+        basis_gates = ["h", "t", "tdg", "s", "z"]\n+        gate_approx_library = generate_basic_approximations(basis_gates=basis_gates, depth=depth)\n+\n+        skd = SolovayKitaev(recursion_degree=2, basic_approximations=gate_approx_library)\n+        discretized = skd(circuit)\n+\n+        included_gates = set(discretized.count_ops().keys())\n+        self.assertEqual(set(basis_gates), included_gates)\n+\n \n @ddt\n class TestGateSequence(QiskitTestCase):\n