Repository: Qiskit/qiskit — pull request #13121 (python)
Title: Fix the matrix representation of `CUGate` in Rust

<!--\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\nFixed #13118.\r\n\r\n### Details and comments\r\n\r\nThe matrix representation of `CUGate` in Rust accidentally swapped two arguments (`phi` and `lam`). This didn't fail the tests, because there all parameters are set to the same value. The tests are therefore also updated to vary the parameters values within a gate.\r\n

Resolved issues:
title: Transpiler changes circuit semantics
body: ### Environment\n\n- **Qiskit version**: 1.3.0\r\n- **Python version**: 3.10.12\r\n- **Operating system**: Ubuntu 22.04 LTS on Windows 11 WSL\r\n\n\n### What is happening?\n\n[Ilan](https://github.com/IlanIwumbwe) and I have found a possible miscompilation caused by the transpiler.\r\n\r\nThe resulting state-vector of the circuit after putting it through `transpile()` changes abruptly after optimization level has been set to above 2, thereby also changing the states of the circuit upon measurement.\n\n### How can we reproduce the issue?\n\n```py\r\nfrom qiskit import QuantumCircuit\r\nfrom qiskit.transpiler import generate_preset_pass_manager\r\nfrom qiskit.quantum_info import Statevector\r\nfrom numpy import vdot\r\n\r\ndef simulate_circuit(qc : QuantumCircuit):\r\n    state = Statevector.from_int(0, 2**qc.num_qubits)\r\n    state = state.evolve(qc)\r\n    return state\r\n\r\ndef compare_statevectors(qc : QuantumCircuit):\r\n    # Need to set the seed for consistent transpiler pass results across optimisation levels\r\n    options = {'seed_transpiler': 1235}\r\n    sv0 = simulate_circuit(qc)\r\n    # Code for checking different levels of optimization levels and if they are the same\r\n    for i in range(4):\r\n        pass_manager = generate_preset_pass_manager(optimization_level=i, **options)\r\n        transpiled_circuit = pass_manager.run(qc)\r\n        sv = simulate_circuit(transpiled_circuit)\r\n        # Uncomment to see statevector\r\n        # print(sv) \r\n        if(sv0 == sv):\r\n            print("Level ", i, " passed")\r\n        else:\r\n            print("Failed level ", i, ". Dot product is: " , abs(vdot(sv0, sv)), "\\n")\r\n\r\nsubcirc2 = QuantumCircuit(2)\r\nsubcirc2.z(0)\r\nsubcirc2.h(0)\r\nsubcirc2.z(0)\r\nsubcirc2.s(1)\r\nsubcirc2 = subcirc2.to_gate().control(1)\r\n\r\nmain_circ = QuantumCircuit(4)\r\nmain_circ.h(0)\r\nmain_circ.append(subcirc2,[0, 2, 3])\r\nmain_circ.append(subcirc2,[0, 2, 3])\r\nmain_circ.append(subcirc2,[0, 2, 3])\r\nmain_circ.h(2)\r\n\r\ncompare_statevectors(main_circ)\r\n```\r\n\r\nRunning the above code will reveal that the state-vectors are different when the optimization level is above 2. The results will also be the same if the circuit was instead run on `AerBackend`, with optimization levels 2 and 3 producing very different measured states than levels 0 and 1.\r\n\r\n\r\nThis is done on the latest version of qiskit (1.3.0), which we have compiled from source to get the latest fixes. However, it seems like this issue is also present in version 1.2.0. \n\n### What should happen?\n\nThe circuit should not have a different state-vector after putting it through the transpiler.\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs\nindex a38783ef7e7f..6b04b8512fb0 100644\n--- a/crates/circuit/src/gate_matrix.rs\n+++ b/crates/circuit/src/gate_matrix.rs\n@@ -387,12 +387,12 @@ pub fn cu_gate(theta: f64, phi: f64, lam: f64, gamma: f64) -> GateArray2Q {\n             C_ZERO,\n             c64(0., gamma).exp() * cos_theta,\n             C_ZERO,\n-            c64(0., gamma + phi).exp() * (-1.) * sin_theta,\n+            c64(0., gamma + lam).exp() * (-1.) * sin_theta,\n         ],\n         [C_ZERO, C_ZERO, C_ONE, C_ZERO],\n         [\n             C_ZERO,\n-            c64(0., gamma + lam).exp() * sin_theta,\n+            c64(0., gamma + phi).exp() * sin_theta,\n             C_ZERO,\n             c64(0., gamma + phi + lam).exp() * cos_theta,\n         ],\ndiff --git a/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml b/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml\nnew file mode 100644\nindex 000000000000..406d6052a1df\n--- /dev/null\n+++ b/releasenotes/notes/fix-cu-rust-6464b6893ecca1b3.yaml\n@@ -0,0 +1,9 @@\n+---\n+fixes:\n+  - |\n+    Fixed the definition of the :class:`.CUGate` matrix in Rust-space. \n+    While this was not noticable while handling the :class:`.CUGate` purely on \n+    Python side, this had knock-on effects when transpiler passes were using the \n+    Rust representation, such as could happen in :class:`.Consolidate2qBlocks`.\n+    Fixed `#13118 <https://github.com/Qiskit/qiskit/issues/13118>`__.\n+\n

Test patch:
diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py\nindex b6ec28f82e8d..db4f1441bea3 100644\n--- a/test/python/circuit/test_rust_equivalence.py\n+++ b/test/python/circuit/test_rust_equivalence.py\n@@ -66,7 +66,7 @@ def test_definitions(self):\n                 continue\n \n             with self.subTest(name=name):\n-                params = [pi] * standard_gate._num_params()\n+                params = [0.1 * (i + 1) for i in range(standard_gate._num_params())]\n                 py_def = gate_class.base_class(*params).definition\n                 rs_def = standard_gate._get_definition(params)\n                 if py_def is None:\n@@ -141,7 +141,7 @@ def test_matrix(self):\n                 continue\n \n             with self.subTest(name=name):\n-                params = [0.1] * standard_gate._num_params()\n+                params = [0.1 * (i + 1) for i in range(standard_gate._num_params())]\n                 py_def = gate_class.base_class(*params).to_matrix()\n                 rs_def = standard_gate._to_matrix(params)\n                 np.testing.assert_allclose(rs_def, py_def)\n