Repository: Qiskit/qiskit

Issue:
Visualize pi-values as pi in parameter expressions
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

If the rotation angles are close to pi, `pi` will be displayed instead of the numeric value. This should also be the case for more complex `ParameterExpression`s. The `pi_check` function in `qiskit.visualization.tools` already recognizes if the angle is a `ParameterExpression` and calls the `__str__` method of this class. We probably can update this method to replace numerical values close to pi with the string literal `pi`.

Here's an example:
```python
>>> from qiskit.circuit import QuantumCircuit, Parameter
>>> from qiskit.qasm import pi
>>> x, y = Parameter('x'), Parameter('y')
>>> circuit = QuantumCircuit(1)
>>> circuit.rx((pi - x) * (pi - y), 0)
>>> circuit.draw()
     ┌───────────────────────────────────────────────────┐
q_0: ┤ Rx((3.14159265358979 - x)*(3.14159265358979 - y)) ├
     └───────────────────────────────────────────────────┘
```
should produce
```
     ┌───────────────────────┐
q_0: ┤ Rx((pi - x)*(pi - y)) ├
     └───────────────────────┘
```


Discussion:
Mind if I try this one? I haven't contributed to the project yet and it seems like I could handle this. Any suggestions/tips while I look around and get started?
Sure! 

The visualizers check if the parameter values to go inside the box are close to `pi` using the `qiskit.visualization.tools.pi_check`. There we have the line 
https://github.com/Qiskit/qiskit-terra/blob/f38b617748d855e98f682d259b7cf597ef16b2a8/qiskit/visualization/tools/pi_check.py#L47
which means that, if the value is a `ParameterExpression` the `__str__` method of this object is used. This is implemented as 
https://github.com/Qiskit/qiskit-terra/blob/f38b617748d855e98f682d259b7cf597ef16b2a8/qiskit/circuit/parameterexpression.py#L231
where `self._symbol_expr` is a SymPy expression. 

Since SymPy only renders `sympy.pi` objects as `pi`, but not numeric values close to pi the expression does not render `numpy.pi` or any other pi-values in the correct way. You would probably have to check how SymPy can be forced to show close-to-pi values as pi.

However that might not be that simple, I imagine that it would be done by default if it were easy. Another possibility would be to recursively go through `self._symbol_expr.args` and replace `3.1415...` by `sympy.pi`. 

Here's a short code example you could use to test your ideas
```python
import numpy as np
from qiskit.circuit import QuantumCircuit, Parameter

qc = QuantumCircuit(1)
x = Parameter('x')
qc.ry((np.pi - x) * (np.pi - x) * 2, 0)
# qc.data[0] is the first operation in the circuit: (Ry, qargs, cargs)
# qc.data[0][0] is the Ry gate
# qc.data[0][0].params are the parameters in the Ry gate, from which we want the first one
parameter_expr = qc.data[0][0].params[0]
sympy_expr = parameter_expr._symbol_expr
print(sympy_expr)
```
This prints
```
2*(3.14159265358979 - x)**2
```
but should instead produce
```
2*(pi - x)**2
```

If you have any questions or need help feel free to reach out!
I think this might be a duplicate with https://github.com/Qiskit/qiskit-terra/issues/3868 with a pending PR https://github.com/Qiskit/qiskit-terra/pull/3876

Please, @ljeanette15 and @Cryoris redirect your efforts on that way (if it is true that is a duplicated, otherwise repoen)
That only checks if the whole `ParameterExpression` is close to pi, not if elements of it are close to pi. I double checked by checking out the PR branch, and the problem persisted, so I'll reopen 🙂 
Let me push up #3876 in my priority list so we can merge it soon and start working on this one. Thanks for checking! 
#3876 merged!
Hi @1ucian0 and @Cryoris , the issue seems to be still not resolved (checked by the running the code on my system). I would like to give it a try if it is still not done.
Yes, it's still open! Let us know if you want to discuss something or could use some help 👍 
For sure @Cryoris 
I am trying to install qiskit-terra from source 
when I do 
pip install -e .
It give me this error
I have install visual c++ from the link in error
I did not check and box during its installation   
Please help me out of this so I can proceed solving this issue

 ERROR: Command errored out with exit status 1:
     command: 'C:\Users\Bhatnagar\Anaconda3\envs\IssueFixingenv\python.exe' -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\Bhatnagar\\qiskit-terra\\setup.py'"'"'; __file__='"'"'C:\\Users\\Bhatnagar\\qiskit-terra\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' develop --no-deps
         cwd: C:\Users\Bhatnagar\qiskit-terra\
    Complete output (13 lines):
    running develop
    running egg_info
    writing qiskit_terra.egg-info\PKG-INFO
    writing dependency_links to qiskit_terra.egg-info\dependency_links.txt
    writing requirements to qiskit_terra.egg-info\requires.txt
    writing top-level names to qiskit_terra.egg-info\top_level.txt
    reading manifest file 'qiskit_terra.egg-info\SOURCES.txt'
    reading manifest template 'MANIFEST.in'
    warning: no files found matching 'test\python\pickles\*.pickle'
    writing manifest file 'qiskit_terra.egg-info\SOURCES.txt'
    running build_ext
    building 'qiskit.transpiler.passes.routing.cython.stochastic_swap.utils' extension
    error: Microsoft Visual C++ 14.0 is required. Get it with "Build Tools for Visual Studio": https://visualstudio.microsoft.com/downloads/
    ----------------------------------------
ERROR: Command errored out with exit status 1: 'C:\Users\Bhatnagar\Anaconda3\envs\IssueFixingenv\python.exe' -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\Bhatnagar\\qiskit-terra\\setup.py'"'"'; __file__='"'"'C:\\Users\\Bhatnagar\\qiskit-terra\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' develop --no-deps Check the logs for full command output.
I downloaded visual c++ compiler from this link 
https://visualstudio.microsoft.com/visual-cpp-build-tools/

Did you install `cython`? If you follow all steps on https://qiskit.org/documentation/contributing_to_qiskit.html#installing-terra-from-source (choose the Windows Tab) you should be able to install Terra from source.
Yes I did. I followed all the steps on the documentation link.
It still gives the error.

I forked qiskit-terra to my account 
so rather then doing 

git clone https://github.com/Qiskit/qiskit-terra.git

I used my link to clone could that be a reason for the error?
No that should not matter. It seems like pip cannot find Microsoft Visual C++, maybe try re-installing it or browsing similar issues in stackoverflow, such as https://stackoverflow.com/questions/49610212/pip-error-even-microsoft-visual-c-14-0-is-installed. 
Another option would be to try:

python setup.py build_ext —inplace

To see if it can build the modules that way. That works for me. 
Thanks @Cryoris and @nonhermitian it worked for me. I am starting to resolve the issue.
I am trying to debug.
This issue persists with matplotlib and ASKII circuit.draw()
it does not go away even with pi = np.pi (using numpy)
any idea where can the issue be which can lead to this issue persisting in all the above cases?
It's a bit difficult to give comments without explicit information, what did you try to fix the issue? 🙂  
@Cryoris, Could we do something like the following in pi_check.py

```python
except (ValueError, TypeError) as e:
    pi_pattern = re.compile("3+\.+14159\d+")
    inpt = re.sub(pi_pattern, 'pi', str(inpt))
    return inpt
```

This should print something like 

q_0: ┤ RX((pi - x)*(pi - y)) ├

Also, the code example you've provided gives me

` 19.7392088021787*(1 - 0.318309886183791*x)**2 ` instead of `2*(3.14159265358979 - x)**2`

Not sure if this is expected behavior

I'm running it on Ubuntu 20.04 with python3
Hi,

I was going through the issue. Wanted to give a try. I found the the print is actually coming from _symbol_expr of parameterexpression and it is an instance of Sympy (sympy.core.add.Add) and might be using __str__ of Sympy, which I think is not part of Qiskit Terra. so may be in the __str__ of parameter expression, we do not use sympi and write something custom. Do you agree? Can i give a try? 

Souvik Saha Bhowmik
> Also, the code example you've provided gives me
>
> 19.7392088021787*(1 - 0.318309886183791*x)**2 instead of 2*(3.14159265358979 - x)**2
>
> Not sure if this is expected behavior
>
> I'm running it on Ubuntu 20.04 with python3

This is definitely not the expected behaviour, it seems this happened with the last SymPy update. Now that is actually much worse 🤔 

> @Cryoris, Could we do something like the following in pi_check.py

Since the value of pi does not even show up anymore we cannot do a regex match, I think.
Hi @souviksbhowmik, great that you're interested! 

> Can i give a try?

I think @shshnkvadali is currently actively working on this, it is probably best for you two to exchange ideas on this, if that is fine with @shshnkvadali. 
@shshnkvadali ,
Have you found a solution/approach for this? Otherwise can I collaborate on this?
regards
Souvik Saha Bhowmik
@souviksbhowmik 
Sorry about the delay. I haven't found a conventional solution to this yet. Let me know if you have any ideas. We can definitely work on this together. Thanks
@Cryoris 
I am interested to work on this problem.
`print(float(sp.pi-np.pi))` gives `1.2246467991473532e-16`
So I was thinking of adding an if statement here that checks if the actual pi value is within `1.2246467991473532e-16` of sympy.pi.
If so, then sympy.pi will be rendered.
https://github.com/Qiskit/qiskit-terra/blob/f38b617748d855e98f682d259b7cf597ef16b2a8/qiskit/visualization/tools/pi_check.py#L47
However, this is in a branch. So how can I edit this?

Resolving patch:
diff --git a/qiskit/circuit/tools/pi_check.py b/qiskit/circuit/tools/pi_check.py
--- a/qiskit/circuit/tools/pi_check.py
+++ b/qiskit/circuit/tools/pi_check.py
@@ -21,6 +21,8 @@
 MAX_FRAC = 16
 N, D = np.meshgrid(np.arange(1, MAX_FRAC+1), np.arange(1, MAX_FRAC+1))
 FRAC_MESH = N / D * np.pi
+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):
@@ -43,10 +45,21 @@ def pi_check(inpt, eps=1e-6, output='text', ndigits=5):
         QiskitError: if output is not a valid option.
     """
     if isinstance(inpt, ParameterExpression):
-        try:
-            return pi_check(float(inpt), eps=eps, output=output, ndigits=ndigits)
-        except (ValueError, TypeError):
-            return str(inpt)
+        param_str = str(inpt)
+        syms = inpt._symbol_expr.expr_free_symbols
+        for sym in syms:
+            if not sym.is_number:
+                continue
+            pi = pi_check(float(sym), eps=eps, output=output, ndigits=ndigits)
+            try:
+                _ = float(pi)
+            except (ValueError, TypeError):
+                # Strip leading '-' from pi since must replace with abs(sym)
+                # in order to preserve spacing around minuses in expression
+                if pi[0] == '-':
+                    pi = pi[1:]
+                param_str = param_str.replace(str(abs(sym)), pi)
+        return param_str
     elif isinstance(inpt, str):
         return inpt
 
@@ -64,86 +77,87 @@ def normalize(single_inpt):
             raise QiskitError('pi_check parameter output should be text, '
                               'latex, mpl, or qasm.')
 
+        neg_str = '-' if single_inpt < 0 else ''
+
         # First check is for whole multiples of pi
         val = single_inpt / np.pi
         if abs(val) >= 1 - eps:
             if abs(abs(val) - abs(round(val))) < eps:
-                val = int(round(val))
-                if val == 1:
-                    str_out = '{}'.format(pi)
-                elif val == -1:
-                    str_out = '-{}'.format(pi)
+                val = int(abs(round(val)))
+                if abs(val) == 1:
+                    str_out = '{}{}'.format(neg_str, pi)
                 else:
                     if output == 'qasm':
-                        str_out = '{}*{}'.format(val, pi)
+                        str_out = '{}{}*{}'.format(neg_str, val, pi)
                     else:
-                        str_out = '{}{}'.format(val, pi)
+                        str_out = '{}{}{}'.format(neg_str, val, pi)
+                return str_out
+
+        # Second is a check for powers of pi
+        if abs(single_inpt) > np.pi:
+            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)
+                elif output == 'latex':
+                    str_out = '{}{}^{}'.format(neg_str, pi, power[0][0] + 2)
+                elif output == 'mpl':
+                    str_out = '{}{}$^{}$'.format(neg_str, pi, power[0][0] + 2)
+                else:
+                    str_out = '{}{}**{}'.format(neg_str, pi, power[0][0] + 2)
                 return str_out
 
-        # Second is a check for a number larger than MAX_FRAC*np.pi, since no
-        # fractions will exceed that value
-        if abs(single_inpt) >= MAX_FRAC*np.pi:
-            str_out = '%.{}g'.format(ndigits) % single_inpt
+        # 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)
             return str_out
 
-        # Third check is for fractions for 1*np.pi in the numer and any
+        # Fourth check is for fractions for 1*pi in the numer and any
         # number in the denom.
         val = np.pi / single_inpt
         if abs(abs(val) - abs(round(val))) < eps:
-            val = int(round(val))
-            if val > 0:
-                if output == 'latex':
-                    str_out = '\\frac{%s}{%s}' % (pi, abs(val))
-                else:
-                    str_out = '{}/{}'.format(pi, val)
+            val = int(abs(round(val)))
+            if output == 'latex':
+                str_out = '\\frac{%s%s}{%s}' % (neg_str, pi, val)
             else:
-                if output == 'latex':
-                    str_out = '\\frac{-%s}{%s}' % (pi, abs(val))
-                else:
-                    str_out = '-{}/{}'.format(pi, abs(val))
+                str_out = '{}{}/{}'.format(neg_str, pi, val)
             return str_out
 
-        # Fourth check for fractions where the numer > 1*np.pi and numer
-        # is up to MAX_FRAC*np.pi and denom is up to MAX_FRAC and all
-        # fractions are reduced. Ex. 15pi/16, 2pi/5, 15pi/2, 8pi/3, 16pi/9.
-        abs_val = abs(single_inpt)
-        frac = np.where(np.abs(abs_val - FRAC_MESH) < 1e-8)
+        # Fifth check is for fractions where the numer > 1*pi and numer
+        # is up to MAX_FRAC*pi and denom is up to MAX_FRAC and all
+        # fractions are reduced. Ex. 15pi/16, 2pi/5, 15pi/2, 16pi/9.
+        frac = np.where(np.abs(abs(single_inpt) - FRAC_MESH) < eps)
         if frac[0].shape[0]:
             numer = int(frac[1][0]) + 1
             denom = int(frac[0][0]) + 1
-            if single_inpt < 0:
-                numer *= -1
-
-            if numer == 1 and denom == 1:
-                str_out = '{}'.format(pi)
-            elif numer == -1 and denom == 1:
-                str_out = '-{}'.format(pi)
-            elif numer == 1:
-                if output == 'latex':
-                    str_out = '\\frac{%s}{%s}' % (pi, denom)
-                else:
-                    str_out = '{}/{}'.format(pi, denom)
-            elif numer == -1:
-                if output == 'latex':
-                    str_out = '\\frac{-%s}{%s}' % (pi, denom)
-                else:
-                    str_out = '-{}/{}'.format(pi, denom)
-            elif denom == 1:
-                if output == 'latex':
-                    str_out = '\\frac{%s}{%s}' % (numer, pi)
-                else:
-                    str_out = '{}/{}'.format(numer, pi)
+            if output == 'latex':
+                str_out = '\\frac{%s%s%s}{%s}' % (neg_str, numer, pi, denom)
+            elif output == 'qasm':
+                str_out = '{}{}*{}/{}'.format(neg_str, numer, pi, denom)
             else:
-                if output == 'latex':
-                    str_out = '\\frac{%s%s}{%s}' % (numer, pi, denom)
-                elif output == 'qasm':
-                    str_out = '{}*{}/{}'.format(numer, pi, denom)
-                else:
-                    str_out = '{}{}/{}'.format(numer, pi, denom)
+                str_out = '{}{}{}/{}'.format(neg_str, numer, pi, denom)
+            return str_out
 
+        # Sixth check is for fractions where the numer > 1 and numer
+        # is up to MAX_FRAC and denom is up to MAX_FRAC*pi and all
+        # fractions are reduced. Ex. 15/16pi, 2/5pi, 15/2pi, 16/9pi
+        frac = np.where(np.abs(abs(single_inpt) - RECIP_MESH) < eps)
+        if frac[0].shape[0]:
+            numer = int(frac[1][0]) + 1
+            denom = int(frac[0][0]) + 1
+            if denom == 1 and output != 'qasm':
+                denom = ''
+            if output == 'latex':
+                str_out = '\\frac{%s%s}{%s%s}' % (neg_str, numer, denom, pi)
+            elif output == 'qasm':
+                str_out = '{}{}/({}*{})'.format(neg_str, numer, denom, pi)
+            else:
+                str_out = '{}{}/{}{}'.format(neg_str, numer, denom, pi)
             return str_out
-        # nothing found
-        str_out = '%.{}g'.format(ndigits) % single_inpt
+
+        # Nothing found
+        str_out = '{:.{}g}'.format(single_inpt, ndigits)
         return str_out
 
     complex_inpt = complex(inpt)


Test patch:
diff --git a/test/ipynb/mpl/references/pi_in_param_expr.png b/test/ipynb/mpl/references/pi_in_param_expr.png
new file mode 100644
Binary files /dev/null and b/test/ipynb/mpl/references/pi_in_param_expr.png differ
diff --git a/test/ipynb/mpl/test_circuit_matplotlib_drawer.py b/test/ipynb/mpl/test_circuit_matplotlib_drawer.py
--- a/test/ipynb/mpl/test_circuit_matplotlib_drawer.py
+++ b/test/ipynb/mpl/test_circuit_matplotlib_drawer.py
@@ -331,6 +331,14 @@ def test_scale(self):
         self.circuit_drawer(circuit, filename='scale_half.png', scale=0.5)
         self.circuit_drawer(circuit, filename='scale_double.png', scale=2)
 
+    def test_pi_param_expr(self):
+        """Text pi in circuit with parameter expression."""
+        x, y = Parameter('x'), Parameter('y')
+        circuit = QuantumCircuit(1)
+        circuit.rx((pi - x) * (pi - y), 0)
+
+        self.circuit_drawer(circuit, filename='pi_in_param_expr.png')
+
     def test_partial_layout(self):
         """Tests partial_layout
         See: https://github.com/Qiskit/qiskit-terra/issues/4757"""
diff --git a/test/python/circuit/test_tools.py b/test/python/circuit/test_tools.py
--- a/test/python/circuit/test_tools.py
+++ b/test/python/circuit/test_tools.py
@@ -18,6 +18,7 @@
 from numpy import pi
 from qiskit.test import QiskitTestCase
 from qiskit.circuit.tools.pi_check import pi_check
+from qiskit.circuit import Parameter
 
 
 @ddt
@@ -31,12 +32,36 @@ class TestPiCheck(QiskitTestCase):
                    (2.999999999999999, '3'),
                    (0.99, '0.99'),
                    (0.999999999999999, '1'),
+                   (pi, 'pi'),
+                   (-pi, '-pi'),
+                   (3*pi, '3pi'),
+                   (-3*pi, '-3pi'),
+                   (pi/35, 'pi/35'),
+                   (-pi/35, '-pi/35'),
+                   (3*pi/35, '0.26928'),
+                   (-3*pi/35, '-0.26928'),
+                   (pi**2, 'pi**2'),
+                   (-pi**2, '-pi**2'),
                    (1e9, '1e+09'),
+                   (-1e9, '-1e+09'),
                    (1e-9, '1e-09'),
+                   (-1e-9, '-1e-09'),
+                   (6*pi/11, '6pi/11'),
+                   (-6*pi/11, '-6pi/11'),
+                   (6*pi/1, '6pi'),
+                   (-6*pi/1, '-6pi'),
+                   (6*pi/2, '3pi'),
+                   (-6*pi/2, '-3pi'),
+                   (1j*3/(7*pi), '3/7pij'),
+                   (-1j*3/(7*pi), '-3/7pij'),
                    (6*pi/5+1j*3*pi/7, '6pi/5+3pi/7j'),
                    (-6*pi/5+1j*3*pi/7, '-6pi/5+3pi/7j'),
                    (6*pi/5-1j*3*pi/7, '6pi/5-3pi/7j'),
                    (-6*pi/5-1j*3*pi/7, '-6pi/5-3pi/7j'),
+                   (1/pi, '1/pi'),
+                   (-1/pi, '-1/pi'),
+                   (6/(5*pi), '6/5pi'),
+                   (-6/(5*pi), '-6/5pi'),
                    (-382578.0+.0234567j, '-3.8258e+05+0.023457j'),
                    (-382578.0-.0234567j, '-3.8258e+05-0.023457j')
                    ])
@@ -47,6 +72,17 @@ def test_default(self, case):
         result = pi_check(input_number)
         self.assertEqual(result, expected_string)
 
+    def test_params(self):
+        """Test pi_check with Parameters"""
+        x = Parameter('x')
+        y = Parameter('y')
+        z = Parameter('z')
+        pcase = (pi**3 * x + 3/(4*pi) * y - 13*pi/2 * z, 'pi**3*x + 3/4pi*y - 13pi/2*z')
+        input_number = pcase[0]
+        expected_string = pcase[1]
+        result = pi_check(input_number)
+        self.assertEqual(result, expected_string)
+
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py
--- a/test/python/visualization/test_circuit_text_drawer.py
+++ b/test/python/visualization/test_circuit_text_drawer.py
@@ -1265,6 +1265,17 @@ def test_text_bound_parameters(self):
 
         self.assertEqual(str(_text_circuit_drawer(circuit)), expected)
 
+    def test_text_pi_param_expr(self):
+        """Text pi in circuit with parameter expression."""
+        expected = '\n'.join(["     ┌───────────────────────┐",
+                              "q_0: ┤ RX((pi - x)*(pi - y)) ├",
+                              "     └───────────────────────┘"])
+
+        x, y = Parameter('x'), Parameter('y')
+        circuit = QuantumCircuit(1)
+        circuit.rx((pi - x) * (pi - y), 0)
+        self.assertEqual(circuit.draw(output='text').single_string(), expected)
+
 
 class TestTextDrawerVerticalCompressionLow(QiskitTestCase):
     """Test vertical_compression='low' """