Repository: Qiskit/qiskit (python)

Issue:
Bound `ParameterExpression`s do not match the hash of an equal object of a numeric type
### Environment

- **Qiskit version**: 1.1.0
- **Python version**: 3.11
- **Operating system**: Fedora Linux 40


### What is happening?

When all the parameters in a `ParameterExpression` are bound, it becomes equal to a `float`, `int`, or `complex`. However, it still uses its own hashing algorithm, so it does not hash to the same value as the numeric type. The Python data model documents that objects that compare equal should hash to the same value.

### How can we reproduce the issue?

```
from qiskit.circuit import Parameter


a = Parameter("a")
b = a.bind({a: 1.2})
b == 1.2  # True
hash(b)  == hash(1.2)  # False
```

### What should happen?

As much as possible, the Python data model should be followed. At some points in the parameter handling code, bound `ParameterExpression`s are eagerly cast to `float`. For example, the `float(p)` here:

https://github.com/Qiskit/qiskit/blob/26886a1b2926a474ea06aa3f9ce9e11e6ce28020/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py#L71

I think this is done to avoid a bound `p` failing the lookup if the calibrations dict was keyed with a plain `float`. It would be preferable not to have to deal with type casting in these cases.

### Any suggestions?

_No response_

Pull request description:
Use hash of numeric value for bound parameter expressions (backport #12488)





### Summary

If a `ParameterExpression` has no unbound parameters, the underlying bound value can be hashed instead of the tuple that accounts for the symbolic expression. Doing this allows for the `ParameterExpression` to match the hash for the numeric value it compares equal to.

### Details and comments

Closes #12487<hr>This is an automatic backport of pull request #12488 done by [Mergify](https://mergify.com).

Patch:
diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py
index 4f6453f90..b98a294b0 100644
--- a/qiskit/circuit/parameterexpression.py
+++ b/qiskit/circuit/parameterexpression.py
@@ -439,6 +439,9 @@ class ParameterExpression:
             raise TypeError("could not cast expression to int") from exc
 
     def __hash__(self):
+        if not self._parameter_symbols:
+            # For fully bound expressions, fall back to the underlying value
+            return hash(self.numeric())
         return hash((self._parameter_keys, self._symbol_expr))
 
     def __copy__(self):
diff --git a/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml b/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml
new file mode 100644
index 000000000..075de45b3
--- /dev/null
+++ b/releasenotes/notes/parameterexpression-hash-d2593ab1715aa42c.yaml
@@ -0,0 +1,8 @@
+---
+fixes:
+  - |
+    :class:`.ParameterExpression` was updated so that fully bound instances
+    that compare equal to instances of Python's built-in numeric types (like
+    ``float`` and ``int``) also have hash values that match those of the other
+    instances. This change ensures that these types can be used interchangeably
+    as dictionary keys. See `#12488 <https://github.com/Qiskit/qiskit/pull/12488>`__.


Test patch:
diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py
index 7bcc2cd35..3681998c0 100644
--- a/test/python/circuit/test_parameters.py
+++ b/test/python/circuit/test_parameters.py
@@ -1425,6 +1425,7 @@ class TestParameterExpressions(QiskitTestCase):
         x = Parameter("x")
         bound_expr = x.bind({x: 2.3})
         self.assertEqual(bound_expr, 2.3)
+        self.assertEqual(hash(bound_expr), hash(2.3))
 
     def test_abs_function_when_bound(self):
         """Verify expression can be used with