Repository: Qiskit/qiskit-terra (python)

Issue:
Truncating floats in QASM dump alters circuit's functionality
### Information

- **Qiskit Terra version**: 0.18.3 (dates back to 0.14)
- **Python version**: Any
- **Operating system**: Any

### What is the current behavior?
In #4276, the `pi_check` function was introduced for pretty printing the list of parameters when dumping a `QuantumCircuit` to `qasm`, see 
https://github.com/Qiskit/qiskit-terra/blob/637acc0c408d8199dd58780cc3dafabc14e61a0f/qiskit/circuit/instruction.py#L450-L463
This also introduced a fixed truncation (`ndigits=8`) of floating point numbers that are not close to any fraction or fraction of pi. However, *8* digits is not enough to uniquely identify a double precision floating point number. You need up to *17* significant digits for that. This leads to the following situation:

Let's say I have a circuit that contains, e.g., a phase gate with more than 8 significant digits in its rotation angle.
Then, I dump the circuit to a `.qasm` file and read that file back into a `QuantumCircuit`.
If I compare both unitary functionalities (i.e., the respective unitary matrices), these will no longer be equal due to the numerical error introduced by the truncation.

### Steps to reproduce the problem

```python
from qiskit import QuantumCircuit, execute, Aer

qc = QuantumCircuit(1)
qc.p(1/4096, 0)

qasm_str = qc.qasm()

backend = Aer.get_backend('aer_simulator')

qc.save_unitary()
job = execute(qc, backend)
result = job.result()
unitary = result.get_unitary(qc)

qc_qasm = QuantumCircuit.from_qasm_str(qasm_str)
qc_qasm.save_unitary()
job = execute(qc_qasm, backend)
result = job.result()
unitary_qasm = result.get_unitary(qc_qasm)
    
print(unitary == unitary_qasm)
```
yields
```bash
[[ True  True]
 [ True False]]
```

### What is the expected behavior?

Merely dumping a file to `.qasm` should not alter a circuit's functionality.

### Suggested solutions

A quick fix for this would be to just change the default value for the `ndigits` parameter in the respective function call to `17`.


Pull request description:
Increase Accuracy of OpenQASM 2.0 Dump
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [x] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [x] I have read the CONTRIBUTING document.
-->

### Summary

This PR improves the accuracy of the OpenQASM 2.0 export by not truncating parameters of gates to `single` precision and using a tighter bound for checking whether a given parameter is close to a fraction of pi.
Resolves #7166.

### Details and comments

For details, see the discussion in #7166. The additional precision is accomplished by relying on Python's built-in float formatting as a default in the `pi_check` function.

Patch:
diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py
index c40ae57bb..20643d14d 100644
--- a/qiskit/circuit/instruction.py
+++ b/qiskit/circuit/instruction.py
@@ -455,7 +455,7 @@ class Instruction(Operation):
         if self.params:
             name_param = "{}({})".format(
                 name_param,
-                ",".join([pi_check(i, ndigits=8, output="qasm") for i in self.params]),
+                ",".join([pi_check(i, output="qasm", eps=1e-12) for i in self.params]),
             )
 
         return self._qasmif(name_param)
diff --git a/qiskit/circuit/tools/pi_check.py b/qiskit/circuit/tools/pi_check.py
index 58a87ebc0..a55249d7d 100644
--- a/qiskit/circuit/tools/pi_check.py
+++ b/qiskit/circuit/tools/pi_check.py
@@ -25,7 +25,7 @@ RECIP_MESH = N / D / np.pi
 POW_LIST = np.pi ** np.arange(2, 5)
 
 
-def pi_check(inpt, eps=1e-6, output="text", ndigits=5):
+def pi_check(inpt, eps=1e-9, output="text", ndigits=None):
     """Computes if a number is close to an integer
     fraction or multiple of PI and returns the
     corresponding string.
@@ -35,8 +35,10 @@ def pi_check(inpt, eps=1e-6, output="text", ndigits=5):
         eps (float): EPS to check against.
         output (str): Options are 'text' (default),
                       'latex', 'mpl', and 'qasm'.
-        ndigits (int): Number of digits to print
-                       if returning raw inpt.
+        ndigits (int or None): Number of digits to print
+                               if returning raw inpt.
+                               If `None` (default), Python's
+                               default float formatting is used.
 
     Returns:
         str: string representation of output.
@@ -66,7 +68,7 @@ def pi_check(inpt, eps=1e-6, output="text", ndigits=5):
         return inpt
 
     def normalize(single_inpt):
-        if abs(single_inpt) < 1e-14:
+        if abs(single_inpt) < eps:
             return "0"
 
         if output == "text":
@@ -101,7 +103,10 @@ def pi_check(inpt, eps=1e-6, output="text", ndigits=5):
             power = np.where(abs(abs(single_inpt) - POW_LIST) < eps)
             if power[0].shape[0]:
                 if output == "qasm":
-                    str_out = "{:.{}g}".format(single_inpt, ndigits)
+                    if ndigits is None:
+                        str_out = "{}".format(single_inpt)
+                    else:
+                        str_out = "{:.{}g}".format(single_inpt, ndigits)
                 elif output == "latex":
                     str_out = f"{neg_str}{pi}^{power[0][0] + 2}"
                 elif output == "mpl":
@@ -113,7 +118,10 @@ def pi_check(inpt, eps=1e-6, output="text", ndigits=5):
         # Third is a check for a number larger than MAX_FRAC * pi, not a
         # multiple or power of pi, since no fractions will exceed MAX_FRAC * pi
         if abs(single_inpt) >= (MAX_FRAC * np.pi):
-            str_out = "{:.{}g}".format(single_inpt, ndigits)
+            if ndigits is None:
+                str_out = "{}".format(single_inpt)
+            else:
+                str_out = "{:.{}g}".format(single_inpt, ndigits)
             return str_out
 
         # Fourth check is for fractions for 1*pi in the numer and any
@@ -160,7 +168,10 @@ def pi_check(inpt, eps=1e-6, output="text", ndigits=5):
             return str_out
 
         # Nothing found
-        str_out = "{:.{}g}".format(single_inpt, ndigits)
+        if ndigits is None:
+            str_out = "{}".format(single_inpt)
+        else:
+            str_out = "{:.{}g}".format(single_inpt, ndigits)
         return str_out
 
     complex_inpt = complex(inpt)
diff --git a/releasenotes/notes/change-qasm-float-output-d69d0c2896f8ecbb.yaml b/releasenotes/notes/change-qasm-float-output-d69d0c2896f8ecbb.yaml
new file mode 100644
index 000000000..9302a6c50
--- /dev/null
+++ b/releasenotes/notes/change-qasm-float-output-d69d0c2896f8ecbb.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    The OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`) will now emit higher
+    precision floating point numbers for gate parameters per default. This is
+    accomplished by relying on Python's built-in float formatting in case no
+    suitable approximation to some power/fraction of `pi` can be found.
+
+    In addition, a tighter bound (`1e-12` instead of `1e-6`) is used for checking
+    whether a given parameter is close to a fraction/power of `pi`.
+
+    Fixed `#7166 <https://github.com/Qiskit/qiskit-terra/issues/7166>`__.


Test patch:
diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py
index 8c199523e..417ed8a66 100644
--- a/test/python/circuit/test_circuit_qasm.py
+++ b/test/python/circuit/test_circuit_qasm.py
@@ -451,6 +451,36 @@ custom_{id(gate2)} q[1],q[0];\n"""
         for idx, instruction in enumerate(base._data):
             self.assertEqual(instruction.operation.name, names[idx])
 
+    def test_circuit_qasm_with_double_precision_rotation_angle(self):
+        """Test that qasm() emits high precision rotation angles per default."""
+        from qiskit.circuit.tools.pi_check import MAX_FRAC
+
+        qc = QuantumCircuit(1)
+        qc.p(0.123456789, 0)
+        qc.p(pi * pi, 0)
+        qc.p(MAX_FRAC * pi + 1, 0)
+
+        expected_qasm = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[1];
+p(0.123456789) q[0];
+p(9.869604401089358) q[0];
+p(51.26548245743669) q[0];\n"""
+        self.assertEqual(qc.qasm(), expected_qasm)
+
+    def test_circuit_qasm_with_rotation_angles_close_to_pi(self):
+        """Test that qasm() properly rounds values closer than 1e-12 to pi."""
+
+        qc = QuantumCircuit(1)
+        qc.p(pi + 1e-11, 0)
+        qc.p(pi + 1e-12, 0)
+        expected_qasm = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[1];
+p(3.141592653599793) q[0];
+p(pi) q[0];\n"""
+        self.assertEqual(qc.qasm(), expected_qasm)
+
     def test_circuit_raises_on_single_bit_condition(self):
         """OpenQASM 2 can't represent single-bit conditions, so test that a suitable error is
         printed if this is attempted."""
diff --git a/test/python/circuit/test_tools.py b/test/python/circuit/test_tools.py
index 061248600..8496b825f 100644
--- a/test/python/circuit/test_tools.py
+++ b/test/python/circuit/test_tools.py
@@ -31,21 +31,21 @@ class TestPiCheck(QiskitTestCase):
             (3.141592653589793, "π"),
             (6.283185307179586, "2π"),
             (2.99, "2.99"),
-            (2.999999999999999, "3"),
+            (2.9999999999999999, "3.0"),
             (0.99, "0.99"),
-            (0.999999999999999, "1"),
+            (0.99999999999999999, "1.0"),
             (pi, "π"),
             (-pi, "-π"),
             (3 * pi, "3π"),
             (-3 * pi, "-3π"),
             (pi / 35, "π/35"),
             (-pi / 35, "-π/35"),
-            (3 * pi / 35, "0.26928"),
-            (-3 * pi / 35, "-0.26928"),
+            (3 * pi / 35, "0.26927937030769655"),
+            (-3 * pi / 35, "-0.26927937030769655"),
             (pi**2, "π**2"),
             (-(pi**2), "-π**2"),
-            (1e9, "1e+09"),
-            (-1e9, "-1e+09"),
+            (1e9, "1000000000.0"),
+            (-1e9, "-1000000000.0"),
             (1e-9, "1e-09"),
             (-1e-9, "-1e-09"),
             (6 * pi / 11, "6π/11"),
@@ -64,8 +64,8 @@ class TestPiCheck(QiskitTestCase):
             (-1 / pi, "-1/π"),
             (6 / (5 * pi), "6/5π"),
             (-6 / (5 * pi), "-6/5π"),
-            (-382578.0 + 0.0234567j, "-3.8258e+05+0.023457j"),
-            (-382578.0 - 0.0234567j, "-3.8258e+05-0.023457j"),
+            (-382578.0 + 0.0234567j, "-382578.0+0.0234567j"),
+            (-382578.0 - 0.0234567j, "-382578.0-0.0234567j"),
         ]
     )
     def test_default(self, case):