Repository: quantumlib/Cirq

Issue:
cirq.Rx/Ry/Rz(pi) has confusing QASM output since these are not gates on their own. 
as `cirq.Rx()` is just an `XPowGate` instantiation, `Rx(pi)` becomes `x`. This is just true up to a global phase, but also might be surprising for users. Also `Rx(0)` is not `id`, so it is not consistent. 

Recommendations: 

- `cirq.Rx/Ry/Rx` would become proper subclasses of `XPowGate` instead of just methods and override the `_qasm_` to keep the `rx(<angle>) q;` QASM output form.
- `XPowGate._qasm_` should handle the identity case when _exponent == 0 


Discussion:
Now that we have `cirq.GlobalPhaseOperation`, I think we should consider removing `global_shift` from `EigenGate` resulting in splitting `RxGate` out of `XPowGate`.
That's a pretty big change
Yeah, there's no particular rush on making it. We could start by perhaps extracting RxGate as a child class of XPowGate.
As this is QasmOutput related there might be other ways around it - i.e we could handle known gates in the Qasm protocol, this way this Qasm specific structure doesn't have to force anything on the Cirq representation - there are other places where this is an issue, namely the S and T gates  (and maybe CH, CY could be supported too?):

<details>
  <summary>Code generating QASM</summary>

```

import cirq
import numpy as np 

a, b, c, d = cirq.LineQubit.range(4)

ops = [
# these have a "differing" qasm output
cirq.Rx(np.pi).on(a),
cirq.Ry(np.pi).on(b),
cirq.Rz(np.pi).on(b),
cirq.S.on(a),
cirq.T.on(b),
cirq.S.on(c)**-1,
cirq.T.on(d)**-1,
cirq.ControlledGate(cirq.Y).on(c,d),
cirq.ControlledGate(cirq.H).on(b,c),
    
# these are okay
cirq.Rx(np.pi / 2).on(a),
cirq.Ry(np.pi / 2).on(b),
cirq.Rz(np.pi / 2).on(b),
cirq.Rx(0).on(a),
cirq.Ry(0).on(b),
cirq.Rz(0).on(b),
cirq.X.on(a),
cirq.Y.on(b),
cirq.Z.on(c),
cirq.H.on(d),
cirq.CSWAP.on(d, a, b),
cirq.SWAP.on(c, d),
cirq.CX.on(a,b),
cirq.CZ.on(a,b),
cirq.IdentityGate(1).on(c),
cirq.circuits.qasm_output.QasmUGate(1.0, 2.0, 3.0).on(d)
] 

for op in ops: 
    c = cirq.Circuit.from_ops(op)
    print(c)
    print('qasm: {}'.format(cirq.qasm(c).splitlines()[-1]))
    print()
```

</details>

<details>
  <summary> QASM output </summary>

```
0: ───Rx(π)───
qasm: x q[0];

1: ───Ry(π)───
qasm: y q[0];

1: ───Rz(π)───
qasm: z q[0];

0: ───S───
qasm: rz(pi*0.5) q[0];

1: ───T───
qasm: rz(pi*0.25) q[0];

2: ───S^-1───
qasm: rz(pi*-0.5) q[0];

3: ───T^-1───
qasm: rz(pi*-0.25) q[0];

2: ───@───
      │
3: ───Y───
qasm: u3(pi*0.25,pi*1.0,pi*0.5) q[1];

1: ───@───
      │
2: ───H───
qasm: u3(pi*0.9775301667,pi*1.5,pi*1.5) q[1];

0: ───Rx(0.5π)───
qasm: rx(pi*0.5) q[0];

1: ───Ry(0.5π)───
qasm: ry(pi*0.5) q[0];

1: ───Rz(0.5π)───
qasm: rz(pi*0.5) q[0];

0: ───Rx(0.0π)───
qasm: rx(0) q[0];

1: ───Ry(0.0π)───
qasm: ry(0) q[0];

1: ───Rz(0.0π)───
qasm: rz(0) q[0];

0: ───X───
qasm: x q[0];

1: ───Y───
qasm: y q[0];

2: ───Z───
qasm: z q[0];

3: ───H───
qasm: h q[0];

0: ───×───
      │
1: ───×───
      │
3: ───@───
qasm: cswap q[2],q[0],q[1];

2: ───×───
      │
3: ───×───
qasm: swap q[0],q[1];

0: ───@───
      │
1: ───X───
qasm: cx q[0],q[1];

0: ───@───
      │
1: ───@───
qasm: cz q[0],q[1];

2: ───I───
qasm: id q[0];

3: ───cirq.QasmUGate(1.0, 0.0, 1.0)───
qasm: u3(pi*1.0,0,pi*1.0) q[0];
```
</details>
xref #3640. 

Resolving patch:
diff --git a/cirq-core/cirq/ops/common_gates.py b/cirq-core/cirq/ops/common_gates.py
--- a/cirq-core/cirq/ops/common_gates.py
+++ b/cirq-core/cirq/ops/common_gates.py
@@ -219,12 +219,13 @@ def _circuit_diagram_info_(
 
     def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]:
         args.validate_version('2.0')
-        if self._exponent == 1 and self._global_shift != -0.5:
-            return args.format('x {0};\n', qubits[0])
-        elif self._exponent == 0.5:
-            return args.format('sx {0};\n', qubits[0])
-        elif self._exponent == -0.5:
-            return args.format('sxdg {0};\n', qubits[0])
+        if self._global_shift == 0:
+            if self._exponent == 1:
+                return args.format('x {0};\n', qubits[0])
+            elif self._exponent == 0.5:
+                return args.format('sx {0};\n', qubits[0])
+            elif self._exponent == -0.5:
+                return args.format('sxdg {0};\n', qubits[0])
         return args.format('rx({0:half_turns}) {1};\n', self._exponent, qubits[0])
 
     def _quil_(
@@ -308,6 +309,10 @@ def __str__(self) -> str:
     def __repr__(self) -> str:
         return f'cirq.Rx(rads={proper_repr(self._rads)})'
 
+    def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]:
+        args.validate_version('2.0')
+        return args.format('rx({0:half_turns}) {1};\n', self._exponent, qubits[0])
+
     def _json_dict_(self) -> Dict[str, Any]:
         return {'rads': self._rads}
 
@@ -480,6 +485,10 @@ def __str__(self) -> str:
     def __repr__(self) -> str:
         return f'cirq.Ry(rads={proper_repr(self._rads)})'
 
+    def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]:
+        args.validate_version('2.0')
+        return args.format('ry({0:half_turns}) {1};\n', self._exponent, qubits[0])
+
     def _json_dict_(self) -> Dict[str, Any]:
         return {'rads': self._rads}
 
@@ -654,13 +663,18 @@ def _circuit_diagram_info_(
 
     def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]:
         args.validate_version('2.0')
-        if self._exponent == 1 and self.global_shift != -0.5:
-            return args.format('z {0};\n', qubits[0])
-        elif self._exponent == 0.5:
-            return args.format('s {0};\n', qubits[0])
-        elif self._exponent == -0.5:
-            return args.format('sdg {0};\n', qubits[0])
 
+        if self.global_shift == 0:
+            if self._exponent == 1:
+                return args.format('z {0};\n', qubits[0])
+            elif self._exponent == 0.5:
+                return args.format('s {0};\n', qubits[0])
+            elif self._exponent == -0.5:
+                return args.format('sdg {0};\n', qubits[0])
+            elif self._exponent == 0.25:
+                return args.format('t {0};\n', qubits[0])
+            elif self._exponent == -0.25:
+                return args.format('tdg {0};\n', qubits[0])
         return args.format('rz({0:half_turns}) {1};\n', self._exponent, qubits[0])
 
     def _quil_(
@@ -756,6 +770,10 @@ def __str__(self) -> str:
     def __repr__(self) -> str:
         return f'cirq.Rz(rads={proper_repr(self._rads)})'
 
+    def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]:
+        args.validate_version('2.0')
+        return args.format('rz({0:half_turns}) {1};\n', self._exponent, qubits[0])
+
     def _json_dict_(self) -> Dict[str, Any]:
         return {'rads': self._rads}
 
@@ -860,7 +878,9 @@ def _circuit_diagram_info_(
 
     def _qasm_(self, args: 'cirq.QasmArgs', qubits: Tuple['cirq.Qid', ...]) -> Optional[str]:
         args.validate_version('2.0')
-        if self._exponent == 1:
+        if self._exponent == 0:
+            return args.format('id {0};\n', qubits[0])
+        elif self._exponent == 1 and self._global_shift == 0:
             return args.format('h {0};\n', qubits[0])
 
         return args.format(
diff --git a/cirq-core/cirq/ops/controlled_operation.py b/cirq-core/cirq/ops/controlled_operation.py
--- a/cirq-core/cirq/ops/controlled_operation.py
+++ b/cirq-core/cirq/ops/controlled_operation.py
@@ -30,7 +30,14 @@
 import numpy as np
 
 from cirq import protocols, qis, value
-from cirq.ops import raw_types, gate_operation, controlled_gate, matrix_gates
+from cirq.ops import (
+    controlled_gate,
+    common_gates,
+    eigen_gate,
+    gate_operation,
+    matrix_gates,
+    raw_types,
+)
 from cirq.type_workarounds import NotImplementedType
 
 if TYPE_CHECKING:
@@ -186,6 +193,32 @@ def _apply_unitary_(self, args: 'protocols.ApplyUnitaryArgs') -> np.ndarray:
     def _has_unitary_(self) -> bool:
         return protocols.has_unitary(self.sub_operation)
 
+    def _qasm_(self, args: 'cirq.QasmArgs') -> Optional[str]:
+        if (
+            hasattr(self._sub_operation, "gate")
+            and len(self._controls) == 1
+            and self._control_values == ((1,),)
+        ):
+            gate = self.sub_operation.gate
+            if (
+                isinstance(gate, eigen_gate.EigenGate)
+                and gate.exponent == 1
+                and gate.global_shift == 0
+            ):
+                instr = None
+                if isinstance(gate, common_gates.XPowGate):
+                    instr = 'cx {0},{1};\n'
+                elif isinstance(gate, common_gates.YPowGate):
+                    instr = 'cy {0},{1};\n'
+                elif isinstance(gate, common_gates.ZPowGate):
+                    instr = 'cz {0},{1};\n'
+                elif isinstance(gate, common_gates.HPowGate):
+                    instr = 'ch {0},{1};\n'
+                if instr:
+                    return args.format(instr, self._controls[0], self.sub_operation.qubits[0])
+        # Fallback to decompose.
+        return None
+
     def _extend_matrix(self, sub_matrix: np.ndarray) -> np.ndarray:
         qid_shape = protocols.qid_shape(self)
         sub_n = len(qid_shape) - len(self.controls)


Test patch:
diff --git a/cirq-core/cirq/circuits/qasm_output_test.py b/cirq-core/cirq/circuits/qasm_output_test.py
--- a/cirq-core/cirq/circuits/qasm_output_test.py
+++ b/cirq-core/cirq/circuits/qasm_output_test.py
@@ -270,19 +270,39 @@ def __repr__(self):
             return 'DummyCompositeOperation()'
 
     return (
+        cirq.I(q0),
         cirq.Z(q0),
         cirq.Z(q0) ** 0.625,
+        cirq.Z(q0) ** 0,
         cirq.Y(q0),
         cirq.Y(q0) ** 0.375,
+        cirq.Y(q0) ** 0,
         cirq.X(q0),
         cirq.X(q0) ** 0.875,
-        cirq.H(q1),
+        cirq.X(q0) ** 0,
+        cirq.H(q0),
+        cirq.H(q0) ** 0,
         cirq.X(q0) ** 0.5,
         cirq.X(q0) ** -0.5,
+        cirq.S(q0),
+        cirq.Z(q0) ** -0.5,
+        cirq.T(q0),
+        cirq.Z(q0) ** -0.25,
+        cirq.Rx(rads=np.pi)(q0),
+        cirq.Rx(rads=np.pi / 2)(q0),
+        cirq.Rx(rads=np.pi / 4)(q0),
+        cirq.Ry(rads=np.pi)(q0),
+        cirq.Ry(rads=np.pi / 2)(q0),
+        cirq.Ry(rads=np.pi / 4)(q0),
+        cirq.Rz(rads=np.pi)(q0),
+        cirq.Rz(rads=np.pi / 2)(q0),
+        cirq.Rz(rads=np.pi / 4)(q0),
         cirq.CZ(q0, q1),
         cirq.CZ(q0, q1) ** 0.25,  # Requires 2-qubit decomposition
         cirq.CNOT(q0, q1),
         cirq.CNOT(q0, q1) ** 0.5,  # Requires 2-qubit decomposition
+        cirq.ControlledGate(cirq.Y)(q0, q1),
+        cirq.ControlledGate(cirq.H)(q0, q1),
         cirq.SWAP(q0, q1),
         cirq.SWAP(q0, q1) ** 0.75,  # Requires 2-qubit decomposition
         cirq.CCZ(q0, q1, q2),
@@ -356,15 +376,33 @@ def filter_unpredictable_numbers(text):
 creg m_multi[3];
 
 
+id q[0];
 z q[0];
 rz(pi*0.625) q[0];
+rz(0) q[0];
 y q[0];
 ry(pi*0.375) q[0];
+ry(0) q[0];
 x q[0];
 rx(pi*0.875) q[0];
-h q[1];
+rx(0) q[0];
+h q[0];
+id q[0];
 sx q[0];
 sxdg q[0];
+s q[0];
+sdg q[0];
+t q[0];
+tdg q[0];
+rx(pi*1.0) q[0];
+rx(pi*0.5) q[0];
+rx(pi*0.25) q[0];
+ry(pi*1.0) q[0];
+ry(pi*0.5) q[0];
+ry(pi*0.25) q[0];
+rz(pi*1.0) q[0];
+rz(pi*0.5) q[0];
+rz(pi*0.25) q[0];
 cz q[0],q[1];
 
 // Gate: CZ**0.25
@@ -399,6 +437,8 @@ def filter_unpredictable_numbers(text):
 u3(pi*0.5,pi*0.5,pi*1.0) q[1];
 ry(pi*0.5) q[1];
 
+cy q[0],q[1];
+ch q[0],q[1];
 swap q[0],q[1];
 
 // Gate: SWAP**0.75