Repository: Qiskit/qiskit

Issue:
Incorrect error message when conversion of ParameterExpression  to float fails.
### Environment

- **Qiskit Terra version**:  main branch
- **Python version**:  3.11
- **Operating system**:  Linux



### What is happening?

When using SymEngine, sometimes a `ParameterExpression` wraps a SymEngine expression that is equivalent to a float. But trying to convert the `ParameterExpression` to a float fails with an error claiming that there are unbound parameters, when in fact all parameters *are* bound.

### How can we reproduce the issue?

The following prints `True` three times.
```python
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterExpression

x = Parameter("x")
pex = 1j * x
pexb = pex.bind({x : 1j})
print(str(pexb) == '-1.0')
print(complex(pexb) == -1)
print(isinstance(complex(pexb), complex))
```
But this fails:
```python
float(pexb)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
File ~/qiskit/qiskit-terra/qiskit/circuit/parameterexpression.py:474, in ParameterExpression.__float__(self)
    473 try:
--> 474     return float(self._symbol_expr)
    475 # TypeError is for sympy, RuntimeError for symengine

File symengine_wrapper.pyx:1151, in symengine.lib.symengine_wrapper.Basic.__float__()

File symengine_wrapper.pyx:976, in symengine.lib.symengine_wrapper.Basic.n()

File symengine_wrapper.pyx:4346, in symengine.lib.symengine_wrapper.evalf()

RuntimeError: Not Implemented

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[17], line 1
----> 1 float(pexb)

File ~/qiskit/qiskit-terra/qiskit/circuit/parameterexpression.py:477, in ParameterExpression.__float__(self)
    475 # TypeError is for sympy, RuntimeError for symengine
    476 except (TypeError, RuntimeError) as exc:
--> 477     raise TypeError(
    478         "ParameterExpression with unbound parameters ({}) "
    479         "cannot be cast to a float.".format(self.parameters)
    480     ) from exc

TypeError: ParameterExpression with unbound parameters (set()) cannot be cast to a float.
```

The error message is misleading. The attempt made to cast to a float failed, but for a different reason.

### What should happen?

Maybe
* Try to avoid calling `float` on the wrapped expression. Trap the attempt and print or forward the underlying reason (not implemented)
* Call `complex` instead, check if the result is a float. If so return it, else raise error. This is the approach in #10183

### Any suggestions?

_No response_


Discussion:
Duplicate


Resolving patch:
diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py
--- a/qiskit/circuit/parameterexpression.py
+++ b/qiskit/circuit/parameterexpression.py
@@ -464,30 +464,47 @@ def __complex__(self):
             return complex(self._symbol_expr)
         # TypeError is for sympy, RuntimeError for symengine
         except (TypeError, RuntimeError) as exc:
-            raise TypeError(
-                "ParameterExpression with unbound parameters ({}) "
-                "cannot be cast to a complex.".format(self.parameters)
-            ) from exc
+            if self.parameters:
+                raise TypeError(
+                    "ParameterExpression with unbound parameters ({}) "
+                    "cannot be cast to a complex.".format(self.parameters)
+                ) from None
+            raise TypeError("could not cast expression to complex") from exc
 
     def __float__(self):
         try:
             return float(self._symbol_expr)
         # TypeError is for sympy, RuntimeError for symengine
         except (TypeError, RuntimeError) as exc:
-            raise TypeError(
-                "ParameterExpression with unbound parameters ({}) "
-                "cannot be cast to a float.".format(self.parameters)
-            ) from exc
+            if self.parameters:
+                raise TypeError(
+                    "ParameterExpression with unbound parameters ({}) "
+                    "cannot be cast to a float.".format(self.parameters)
+                ) from None
+            try:
+                # In symengine, if an expression was complex at any time, its type is likely to have
+                # stayed "complex" even when the imaginary part symbolically (i.e. exactly)
+                # cancelled out.  Sympy tends to more aggressively recognise these as symbolically
+                # real.  This second attempt at a cast is a way of unifying the behaviour to the
+                # more expected form for our users.
+                cval = complex(self)
+                if cval.imag == 0.0:
+                    return cval.real
+            except TypeError:
+                pass
+            raise TypeError("could not cast expression to float") from exc
 
     def __int__(self):
         try:
             return int(self._symbol_expr)
         # TypeError is for sympy, RuntimeError for symengine
         except (TypeError, RuntimeError) as exc:
-            raise TypeError(
-                "ParameterExpression with unbound parameters ({}) "
-                "cannot be cast to an int.".format(self.parameters)
-            ) from exc
+            if self.parameters:
+                raise TypeError(
+                    "ParameterExpression with unbound parameters ({}) "
+                    "cannot be cast to an int.".format(self.parameters)
+                ) from None
+            raise TypeError("could not cast expression to int") from exc
 
     def __hash__(self):
         return hash((frozenset(self._parameter_symbols), self._symbol_expr))
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -2857,8 +2857,7 @@ def _assign_parameter(self, parameter: Parameter, value: ParameterValueType) ->
                         if new_param._symbol_expr.is_integer and new_param.is_real():
                             val = int(new_param)
                         elif new_param.is_real():
-                            # Workaround symengine not supporting float(<ComplexDouble>)
-                            val = complex(new_param).real
+                            val = float(new_param)
                         else:
                             # complex values may no longer be supported but we
                             # defer raising an exception to validdate_parameter
@@ -2924,8 +2923,7 @@ def _assign_calibration_parameters(
                                 if new_param._symbol_expr.is_integer:
                                     new_param = int(new_param)
                                 else:
-                                    # Workaround symengine not supporting float(<ComplexDouble>)
-                                    new_param = complex(new_param).real
+                                    new_param = float(new_param)
                             new_cal_params.append(new_param)
                         else:
                             new_cal_params.append(p)


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
@@ -1289,6 +1289,22 @@ def test_abs_function_when_not_bound(self):
         self.assertEqual(abs(x) * abs(y), abs(x * y))
         self.assertEqual(abs(x) / abs(y), abs(x / y))
 
+    def test_cast_to_complex_when_bound(self):
+        """Verify that the cast to complex works for bound objects."""
+        x = Parameter("x")
+        y = Parameter("y")
+        bound_expr = (x + y).bind({x: 1.0, y: 1j})
+        self.assertEqual(complex(bound_expr), 1 + 1j)
+
+    def test_raise_if_cast_to_complex_when_not_fully_bound(self):
+        """Verify raises if casting to complex and not fully bound."""
+
+        x = Parameter("x")
+        y = Parameter("y")
+        bound_expr = (x + y).bind({x: 1j})
+        with self.assertRaisesRegex(TypeError, "unbound parameters"):
+            complex(bound_expr)
+
     def test_cast_to_float_when_bound(self):
         """Verify expression can be cast to a float when fully bound."""
 
@@ -1303,6 +1319,22 @@ def test_cast_to_float_when_underlying_expression_bound(self):
         expr = x - x + 2.3
         self.assertEqual(float(expr), 2.3)
 
+    def test_cast_to_float_intermediate_complex_value(self):
+        """Verify expression can be cast to a float when it is fully bound, but an intermediate part
+        of the expression evaluation involved complex types.  Sympy is generally more permissive
+        than symengine here, and sympy's tends to be the expected behaviour for our users."""
+        x = Parameter("x")
+        bound_expr = (x + 1.0 + 1.0j).bind({x: -1.0j})
+        self.assertEqual(float(bound_expr), 1.0)
+
+    def test_cast_to_float_of_complex_fails(self):
+        """Test that an attempt to produce a float from a complex value fails if there is an
+        imaginary part, with a sensible error message."""
+        x = Parameter("x")
+        bound_expr = (x + 1.0j).bind({x: 1.0})
+        with self.assertRaisesRegex(TypeError, "could not cast expression to float"):
+            float(bound_expr)
+
     def test_raise_if_cast_to_float_when_not_fully_bound(self):
         """Verify raises if casting to float and not fully bound."""