Repository: Qiskit/qiskit

Issue:
ParameterExpression.is_real() inconsistent between Linux/MacOS and Windows
### Environment

- **Qiskit version**: v0.34.2
- **Python version**: v3.8.7 (Windows), v3.8.10 (Ubuntu)
- **Operating system**: Windows 10, Ubuntu 20.04


### What is happening?

ParameterExpression.is_real() reports inconsistent results between Windows and Linux/MacOS for the same code values.

We're developing custom gates (not included in this bug report) that use some float and some complex parameters. When we parameterize our circuits with Parameter and bind_parameters, we attempt to cast appropriately to float or complex; however, Parameter.is_real() doesn't return true/false the same across platforms.

I've been able to reproduce the issue with RXGate, though I realize this gate requires float parameters. If I send in complex numbers, it consistently fails on Windows and inconsistently fails on Linux, depending on values passed in (see example below).

While not exactly the same behavior as what we were seeing with our custom gates, I believe it is failing in a similar enough way to reproduce the issue we saw.

For the test code given below, the error is raised in Gate.validate_parameter() when Parameter.is_real() is false: https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/circuit/gate.py#L221-L223

### How can we reproduce the issue?

"CircuitError: Bound parameter expression is complex in gate rx" on both Windows and Ubuntu:

```
qr = qiskit.QuantumRegister(1)
circuit = qiskit.circuit.QuantumCircuit(qr)

theta = qiskit.circuit.Parameter("theta")
circuit.rx(theta*1j, qr[0])

bound_circuit = circuit.bind_parameters({theta: 1}) 
```

Same CircuitError as above in Windows, no error in Ubuntu:

```
qr = qiskit.QuantumRegister(1)
circuit = qiskit.circuit.QuantumCircuit(qr)

theta = qiskit.circuit.Parameter("theta")
circuit.rx(theta*1j, qr[0])

bound_circuit = circuit.bind_parameters({theta: 2}) 
```

### What should happen?

Parameter.is_real() should report false when the value is a complex and true when it is a float. The return values should be consistent between OS platforms.

### Any suggestions?

_No response_


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py
--- a/qiskit/circuit/parameterexpression.py
+++ b/qiskit/circuit/parameterexpression.py
@@ -508,14 +508,21 @@ def __eq__(self, other):
     def is_real(self):
         """Return whether the expression is real"""
 
-        if not self._symbol_expr.is_real and self._symbol_expr.is_real is not None:
+        # workaround for symengine behavior that const * (0 + 1 * I) is not real
+        # see https://github.com/symengine/symengine.py/issues/414
+        if _optionals.HAS_SYMENGINE and self._symbol_expr.is_real is None:
+            symbol_expr = self._symbol_expr.evalf()
+        else:
+            symbol_expr = self._symbol_expr
+
+        if not symbol_expr.is_real and symbol_expr.is_real is not None:
             # Symengine returns false for is_real on the expression if
             # there is a imaginary component (even if that component is 0),
             # but the parameter will evaluate as real. Check that if the
             # expression's is_real attribute returns false that we have a
             # non-zero imaginary
             if _optionals.HAS_SYMENGINE:
-                if self._symbol_expr.imag != 0.0:
+                if symbol_expr.imag != 0.0:
                     return False
             else:
                 return False


Test patch:
diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py
--- a/test/python/circuit/test_parameters.py
+++ b/test/python/circuit/test_parameters.py
@@ -1718,6 +1718,14 @@ def test_parameter_expression_grad(self):
             self.assertEqual(expr.gradient(x), 2 * x)
             self.assertEqual(expr.gradient(x).gradient(x), 2)
 
+    def test_bound_expression_is_real(self):
+        """Test is_real on bound parameters."""
+        x = Parameter("x")
+        expr = 1j * x
+        bound = expr.bind({x: 2})
+
+        self.assertFalse(bound.is_real())
+
 
 class TestParameterEquality(QiskitTestCase):
     """Test equality of Parameters and ParameterExpressions."""