Repository: NVIDIA/cuda-quantum
Pull request #2864: Fix #2863.
Author: login: schweitzpgi
Created: 2025-05-05T21:20:05Z | Merged: 2025-05-06T21:42:14Z

Fix bugs with the u3<adj> operator.

<!--
Thanks for helping us improve CUDA-Q!

⚠️ The pull request title should be concise and understandable for all.
⚠️ If your pull request fixes an open issue, please link to the issue.

Checklist:
- [ ] I have added tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Description
<!-- Include relevant issues here, describe what changed and why -->


Linked issue #2863: Wrong handling of `u3`'s adjoint
### Required prerequisites

- [x] Consult the [security policy](https://github.com/NVIDIA/cuda-quantum/security/policy). If reporting a security vulnerability, do not report the bug using this form. Use the process described in the policy to report the issue.
- [x] Make sure you've read the [documentation](https://nvidia.github.io/cuda-quantum/latest). Your issue may be addressed there.
- [x] Search the [issue tracker](https://github.com/NVIDIA/cuda-quantum/issues) to verify that this hasn't already been reported. +1 or comment there if it has.
- [ ] If possible, make a PR with a failing test to give us a starting point to work on!

### Describe the bug

@amccaskey found the issue.

The adjoint of `U3` is not only the negation of its parameters. On top of negating the parameters, on must also swap the last two. Using CUDA-Q's parameter ordering for `U3` the following should hold:

```
u3<adj>(a, b, c) != u3(-a, -b, -c)

# The adjoint is:
u3<adj>(a, b, c) == u3(-a, -c, -b)
```

### Steps to reproduce the bug

The following test shouldn't be passing:

https://github.com/NVIDIA/cuda-quantum/blob/b9e780db1be6a818502b8f95bf799caefbe7d1ab/unittests/qis/QubitQISTester.cpp#L419-L436

The `u3` mimicking `rx` should be (the minus sign on the second parameter):
```c++
     u3(1.1, -M_PI_2, M_PI_2, q); 
     // rx<adj>(angle) = u3<adj>(angle, -pi/2, pi/2) 
     u3<cudaq::adj>(1.1, -M_PI_2, M_PI_2, q); 
```

Fixing the test case for the correct code makes it not pass, thus we must be handling the `u3`'s adjoint wrong in the compiler and/or in the simulators. 

These are a couple of places in the code base where the adjoint of `u3` might be mishandled (Only negating the parameters):

https://github.com/NVIDIA/cuda-quantum/blob/b9e780db1be6a818502b8f95bf799caefbe7d1ab/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp#L1299-L1304

https://github.com/NVIDIA/cuda-quantum/blob/b9e780db1be6a818502b8f95bf799caefbe7d1ab/lib/Optimizer/Transforms/DecompositionPatterns.cpp#L1605-L1609

### Expected behavior

Correctly handling the adjoint of `u3` 

### Is this a regression? If it is, put the last known working version (or commit) here.

Not a regression

### Environment

- **CUDA-Q version**: main (b9e780db1be6a818502b8f95bf799caefbe7d1ab)


### Suggestions

_No response_

Diff:
diff --git a/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp b/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp
index 0eb41075702..5e788fc09cb 100644
--- a/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp
+++ b/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp
@@ -1298,9 +1298,21 @@ struct QuantumGatePattern : public OpConversionPattern<OP> {
     SmallVector<Value> opParams = adaptor.getParameters();
     if (!opParams.empty()) {
       // If this is adjoint, each parameter is negated.
-      if (op.getIsAdj())
+      if (op.getIsAdj()) {
         for (std::size_t i = 0; i < opParams.size(); ++i)
           opParams[i] = rewriter.create<arith::NegFOp>(loc, opParams[i]);
+        if constexpr (std::is_same_v<OP, quake::U2Op>) {
+          std::swap(opParams[0], opParams[1]);
+          auto fltTy = cast<FloatType>(opParams[0].getType());
+          Value pi = rewriter.create<arith::ConstantFloatOp>(
+              loc, llvm::APFloat{M_PI}, fltTy);
+          opParams[0] = rewriter.create<arith::SubFOp>(loc, opParams[0], pi);
+          opParams[1] = rewriter.create<arith::AddFOp>(loc, opParams[1], pi);
+        } else if constexpr (std::is_same_v<OP, quake::U3Op>) {
+          // swap the 2nd and 3rd parameter for correctness
+          std::swap(opParams[1], opParams[2]);
+        }
+      }
 
       // Each parameter must be converted to double-precision.
       auto f64Ty = rewriter.getF64Type();
diff --git a/lib/Optimizer/Dialect/Quake/QuakeOps.cpp b/lib/Optimizer/Dialect/Quake/QuakeOps.cpp
index ad3da493687..6f0090e5317 100644
--- a/lib/Optimizer/Dialect/Quake/QuakeOps.cpp
+++ b/lib/Optimizer/Dialect/Quake/QuakeOps.cpp
@@ -934,6 +934,7 @@ void quake::U3Op::getOperatorMatrix(Matrix &matrix) {
     theta *= -1;
     phi *= -1;
     lambda *= -1;
+    std::swap(phi, lambda);
   }
 
   matrix.assign({std::cos(theta / 2.),
diff --git a/lib/Optimizer/Transforms/DecompositionPatterns.cpp b/lib/Optimizer/Transforms/DecompositionPatterns.cpp
index 27e8a4082c3..300db88f10d 100644
--- a/lib/Optimizer/Transforms/DecompositionPatterns.cpp
+++ b/lib/Optimizer/Transforms/DecompositionPatterns.cpp
@@ -1604,6 +1604,8 @@ struct U3ToRotations : public OpRewritePattern<quake::U3Op> {
 
     if (op.isAdj()) {
       theta = rewriter.create<arith::NegFOp>(loc, theta);
+      // swap the 2nd and 3rd parameter for correctness
+      std::swap(phi, lam);
       phi = rewriter.create<arith::NegFOp>(loc, phi);
       lam = rewriter.create<arith::NegFOp>(loc, lam);
     }
diff --git a/lib/Optimizer/Transforms/QuakeSimplify.cpp b/lib/Optimizer/Transforms/QuakeSimplify.cpp
index 22e1ef4c09b..909970ebb93 100644
--- a/lib/Optimizer/Transforms/QuakeSimplify.cpp
+++ b/lib/Optimizer/Transforms/QuakeSimplify.cpp
@@ -258,8 +258,8 @@ class RotationCombine : public OpRewritePattern<QOP> {
       }
     }
 
-    auto params = qop.getParameters();
-    auto prevParams = prev.getParameters();
+    SmallVector<Value> params = qop.getParameters();
+    SmallVector<Value> prevParams = prev.getParameters();
     if (params.size() != prevParams.size()) {
       LLVM_DEBUG(llvm::dbgs()
                  << "Two identical ops with different numbers of parameters?\n"
@@ -550,7 +550,6 @@ class QuakeSimplifyPass
         HermitianElimination<quake::YOp>, HermitianElimination<quake::ZOp>,
         RotationCombine<quake::R1Op>, RotationCombine<quake::RxOp>,
         RotationCombine<quake::RyOp>, RotationCombine<quake::RzOp>,
-        RotationCombine<quake::U2Op>, RotationCombine<quake::U3Op>,
         RotationCombine<quake::PhasedRxOp>>(ctx);
     if (failed(applyPatternsAndFoldGreedily(op, std::move(patterns))))
       signalPassFailure();
diff --git a/python/tests/kernel/test_kernel_features.py b/python/tests/kernel/test_kernel_features.py
index 54f301b87d3..5838d6549c8 100644
--- a/python/tests/kernel/test_kernel_features.py
+++ b/python/tests/kernel/test_kernel_features.py
@@ -1518,8 +1518,8 @@ def rotation_adjoint_test():
 
         # implement Rx gate with U3
         u3(1.1, -np.pi / 2, np.pi / 2, q)
-        # rx.adj(angle) = u3.adj(angle, pi/2, -pi/2)
-        u3.adj(1.1, np.pi / 2, -np.pi / 2, q)
+        # rx.adj(angle) = u3.adj(angle, -pi/2, pi/2)
+        u3.adj(1.1, -np.pi / 2, np.pi / 2, q)
 
         # implement Ry gate with U3
         u3(1.1, 0, 0, q)
diff --git a/runtime/cudaq/qis/managers/BasicExecutionManager.h b/runtime/cudaq/qis/managers/BasicExecutionManager.h
index 6edcfd1bf64..ca33e7fab24 100644
--- a/runtime/cudaq/qis/managers/BasicExecutionManager.h
+++ b/runtime/cudaq/qis/managers/BasicExecutionManager.h
@@ -218,8 +218,17 @@ class BasicExecutionManager : public cudaq::ExecutionManager {
     //
     bool evenAdjointStack = (adjointQueueStack.size() % 2) == 0;
     if (isAdjoint != !evenAdjointStack) {
-      for (std::size_t i = 0; i < params.size(); i++)
-        mutable_params[i] = -1.0 * params[i];
+      if (gateName == "u3") {
+        mutable_params[0] = -1.0 * params[0];
+        mutable_params[1] = -1.0 * params[2];
+        mutable_params[2] = -1.0 * params[1];
+      } else if (gateName == "u2") {
+        mutable_params[0] = -1.0 * params[1] - M_PI;
+        mutable_params[1] = -1.0 * params[0] + M_PI;
+      } else {
+        for (std::size_t i = 0; i < params.size(); i++)
+          mutable_params[i] = -1.0 * params[i];
+      }
       if (gateName == "t")
         mutable_name = "tdg";
       else if (gateName == "s")
diff --git a/test/Quake/quake_simplify.qke b/test/Quake/quake_simplify.qke
index ca643b5cce4..e588bdbaaed 100644
--- a/test/Quake/quake_simplify.qke
+++ b/test/Quake/quake_simplify.qke
@@ -139,19 +139,23 @@ func.func @simp3() -> (i1, i1) {
 }
 
 // CHECK-LABEL:   func.func @simp3() -> (i1, i1) {
-// CHECK:           %[[VAL_0:.*]] = arith.constant 1.100000e+01 : f64
-// CHECK:           %[[VAL_1:.*]] = arith.constant -1.800000e+01 : f64
-// CHECK:           %[[VAL_2:.*]] = arith.constant -2.700000e+01 : f64
-// CHECK:           %[[VAL_3:.*]] = quake.null_wire
-// CHECK:           %[[VAL_4:.*]] = quake.null_wire
-// CHECK:           %[[VAL_5:.*]]:2 = quake.u3 (%[[VAL_0]], %[[VAL_1]], %[[VAL_2]]) {{\[}}%[[VAL_3]]] %[[VAL_4]] : (f64, f64, f64, !quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
-// CHECK:           %[[VAL_6:.*]], %[[VAL_7:.*]] = quake.mz %[[VAL_5]]#0 : (!quake.wire) -> (!quake.measure, !quake.wire)
-// CHECK:           %[[VAL_8:.*]], %[[VAL_9:.*]] = quake.mz %[[VAL_5]]#1 : (!quake.wire) -> (!quake.measure, !quake.wire)
-// CHECK:           %[[VAL_10:.*]] = quake.discriminate %[[VAL_6]] : (!quake.measure) -> i1
-// CHECK:           %[[VAL_11:.*]] = quake.discriminate %[[VAL_8]] : (!quake.measure) -> i1
-// CHECK:           quake.sink %[[VAL_7]] : !quake.wire
-// CHECK:           quake.sink %[[VAL_9]] : !quake.wire
-// CHECK:           return %[[VAL_10]], %[[VAL_11]] : i1, i1
+// CHECK-DAG:       %[[VAL_0:.*]] = arith.constant 3.000000e+01 : f64
+// CHECK-DAG:       %[[VAL_1:.*]] = arith.constant 3.000000e+00 : f64
+// CHECK-DAG:       %[[VAL_2:.*]] = arith.constant 2.000000e+01 : f64
+// CHECK-DAG:       %[[VAL_3:.*]] = arith.constant 2.000000e+00 : f64
+// CHECK-DAG:       %[[VAL_4:.*]] = arith.constant -1.000000e+01 : f64
+// CHECK-DAG:       %[[VAL_5:.*]] = arith.constant 1.000000e+00 : f64
+// CHECK:           %[[VAL_6:.*]] = quake.null_wire
+// CHECK:           %[[VAL_7:.*]] = quake.null_wire
+// CHECK:           %[[VAL_8:.*]]:2 = quake.u3 (%[[VAL_5]], %[[VAL_3]], %[[VAL_1]]) {{\[}}%[[VAL_6]]] %[[VAL_7]] : (f64, f64, f64, !quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
+// CHECK:           %[[VAL_9:.*]]:2 = quake.u3<adj> (%[[VAL_4]], %[[VAL_2]], %[[VAL_0]]) {{\[}}%[[VAL_8]]#0] %[[VAL_8]]#1 : (f64, f64, f64, !quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
+// CHECK:           %[[VAL_10:.*]], %[[VAL_11:.*]] = quake.mz %[[VAL_9]]#0 : (!quake.wire) -> (!quake.measure, !quake.wire)
+// CHECK:           %[[VAL_12:.*]], %[[VAL_13:.*]] = quake.mz %[[VAL_9]]#1 : (!quake.wire) -> (!quake.measure, !quake.wire)
+// CHECK:           %[[VAL_14:.*]] = quake.discriminate %[[VAL_10]] : (!quake.measure) -> i1
+// CHECK:           %[[VAL_15:.*]] = quake.discriminate %[[VAL_12]] : (!quake.measure) -> i1
+// CHECK:           quake.sink %[[VAL_11]] : !quake.wire
+// CHECK:           quake.sink %[[VAL_13]] : !quake.wire
+// CHECK:           return %[[VAL_14]], %[[VAL_15]] : i1, i1
 // CHECK:         }
 
 func.func @fail_to_simplify() -> (i1, i1) {
diff --git a/unittests/qis/QubitQISTester.cpp b/unittests/qis/QubitQISTester.cpp
index dd39fe25f2f..98ee657c90d 100644
--- a/unittests/qis/QubitQISTester.cpp
+++ b/unittests/qis/QubitQISTester.cpp
@@ -421,8 +421,8 @@ CUDAQ_TEST(QubitQISTester, checkU3Adj) {
     cudaq::qubit q;
     // mimic Rx gate
     u3(1.1, -M_PI_2, M_PI_2, q);
-    // rx<adj>(angle) = u3<adj>(angle, pi/2, -pi/2)
-    u3<cudaq::adj>(1.1, M_PI_2, -M_PI_2, q);
+    // rx<adj>(angle) = u3<adj>(angle, -pi/2, pi/2)
+    u3<cudaq::adj>(1.1, -M_PI_2, M_PI_2, q);
     // mimic Ry gate
     u3(1.1, 0., 0., q);
     u3<cudaq::adj>(1.1, 0., 0., q);


Reviews:
user: login: khalatepradnya
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T05:56:13Z
user: login: schweitzpgi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T14:26:45Z
user: login: boschmitt
body: Thanks for the work Eric. Couple of suggestions to fix the comments too.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-06T16:17:23Z
user: login: khalatepradnya
body: LGTM! Thanks, Eric. 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-06T19:43:58Z

Review comments:
diff_hunk: @@ -422,7 +422,7 @@ CUDAQ_TEST(QubitQISTester, checkU3Adj) {
     // mimic Rx gate
     u3(1.1, -M_PI_2, M_PI_2, q);
     // rx<adj>(angle) = u3<adj>(angle, pi/2, -pi/2)
-    u3<cudaq::adj>(1.1, M_PI_2, -M_PI_2, q);
+    u3<cudaq::adj>(1.1, -M_PI_2, M_PI_2, q);
path: unittests/qis/QubitQISTester.cpp
user: login: khalatepradnya
body: There's a Python counterpart to this test - https://github.com/NVIDIA/cuda-quantum/blob/main/python/tests/kernel/test_kernel_features.py#L1513 which should also be updated similarly.
created_at: 2025-05-06T05:56:13Z
author_association: COLLABORATOR
diff_hunk: @@ -422,7 +422,7 @@ CUDAQ_TEST(QubitQISTester, checkU3Adj) {
     // mimic Rx gate
     u3(1.1, -M_PI_2, M_PI_2, q);
     // rx<adj>(angle) = u3<adj>(angle, pi/2, -pi/2)
-    u3<cudaq::adj>(1.1, M_PI_2, -M_PI_2, q);
+    u3<cudaq::adj>(1.1, -M_PI_2, M_PI_2, q);
path: unittests/qis/QubitQISTester.cpp
user: login: schweitzpgi
body: See 471f0b85
created_at: 2025-05-06T14:26:45Z
author_association: COLLABORATOR
diff_hunk: @@ -422,7 +422,7 @@ CUDAQ_TEST(QubitQISTester, checkU3Adj) {
     // mimic Rx gate
     u3(1.1, -M_PI_2, M_PI_2, q);
     // rx<adj>(angle) = u3<adj>(angle, pi/2, -pi/2)
path: unittests/qis/QubitQISTester.cpp
user: login: boschmitt
body: ```suggestion
    // rx<adj>(angle) = u3<adj>(angle, -pi/2, pi/2)
```
created_at: 2025-05-06T16:08:47Z
author_association: COLLABORATOR
diff_hunk: @@ -1519,7 +1519,7 @@ def rotation_adjoint_test():
         # implement Rx gate with U3
         u3(1.1, -np.pi / 2, np.pi / 2, q)
         # rx.adj(angle) = u3.adj(angle, pi/2, -pi/2)
path: python/tests/kernel/test_kernel_features.py
user: login: boschmitt
body: ```suggestion
        # rx.adj(angle) = u3.adj(angle, -pi/2, pi/2)
```
created_at: 2025-05-06T16:09:42Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-05T22:32:19Z
body: **CUDA Quantum Docs Bot:** A preview of the documentation can be found [here](https://htmlpreview.github.io/?https://github.com/NVIDIA/cuda-quantum/blob/docs/preview/pr-2864/index.html).
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T00:25:39Z
body: **CUDA Quantum Docs Bot:** A preview of the documentation can be found [here](https://htmlpreview.github.io/?https://github.com/NVIDIA/cuda-quantum/blob/docs/preview/pr-2864/index.html).
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T15:44:02Z
body: **CUDA Quantum Docs Bot:** A preview of the documentation can be found [here](https://htmlpreview.github.io/?https://github.com/NVIDIA/cuda-quantum/blob/docs/preview/pr-2864/index.html).
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T19:28:07Z
body: **CUDA Quantum Docs Bot:** A preview of the documentation can be found [here](https://htmlpreview.github.io/?https://github.com/NVIDIA/cuda-quantum/blob/docs/preview/pr-2864/index.html).
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T21:45:08Z
body: **CUDA Quantum Docs Bot:** A preview of the documentation can be found [here](https://htmlpreview.github.io/?https://github.com/NVIDIA/cuda-quantum/blob/HEAD/pr-2864/index.html).
author_association: NONE