Repository: Qiskit/qiskit

Issue:
Parameter instances  can compare equal while having different hash values
### Environment

- **Qiskit Terra version**: 0.45.2
- **Python version**: 3.12
- **Operating system**: Fedora Linux 39


### What is happening?

Currently, two `Parameter` instances can compare equal while not having the same hash value. This condition violates the Python data model which is [documented](https://docs.python.org/3/reference/datamodel.html#object.__hash__) as requiring:

> The only required property is that objects which compare equal have the same hash value

### How can we reproduce the issue?

```python
a = Parameter("a")
b = Parameter("b", uuid=a._uuid)

a == b  # True
hash(a) == hash(b)  # False
```

### What should happen?

Here there is a decision to make. Parameters are essentially a tuple of a name string and a Python UUID. Currently, `__eq__` uses only the UUID while `__hash__` uses the name and the UUID. The two methods should use the same data.

In https://github.com/Qiskit/qiskit/pull/11647#issuecomment-1912087645, @jakelishman mentioned that the documentation states that the name is required for equality, so I created #11652 to add the name to the equality check (I think @jakelishman was referring to "Setting the ``uuid`` field when creating two parameters to the same thing (along with the same name) allows them to be equal." in the `Parameter.__init__` docstring). However, in the pulse module, there is code that makes use of the fact that parameter names are not part of the equality test by redefining the name:

https://github.com/Qiskit/qiskit/blob/ce02f068ae1cf919373709e74a3dfbb6be3a2aa2/qiskit/pulse/schedule.py#L882-L912

So we have to decide which way to go:

1. Remove `name` from the hash calculation so that only the UUID matters for Parameter instances. In practice, this is probably fine since the UUID's should always be unique.
2. Add `name` to the equality test and rework the pulse code. It is just using the renaming for convenience of labeling parameters. Perhaps the labeling could be done differently (`scoped_parameters` could return a dict with the keys being the scoped parameter names?). I don't see `scoped_parameters` used anywhere here or qiskit-experiments currently.

I lean toward option 2. I think a key question is how tied is `Parameter` to `ParameterExpression` (technically it is a direct subclass so very tied :slightly_smiling_face:). If `Parameter` just needs to be a variable that floats around through code objects and can later be assigned a value, allowing different names seems fine. However, if it is important that a `Parameter` can always be used in an expression and then later substituted for a different value, allowing different names is a problem because `ParameterExpression` maps the names directly to sympy/symengine symbols and wants to substitute them using the sympy/symengine mechanism. If `Parameter`s are being passed around and manipulated, you could end up calling `assign` with a `Parameter` instance with a different name from the one that was used to create the underlying symbolic expression of a `ParameterExpression`. Or you could say that both use cases are allowed -- when doing simple value substitution renaming `Parameter`s is okay; when doing symbolic expressions, you have to be careful not to reuse UUID's with different names.

@nkanazawa1989 What do you think as the author of the scoped_parameters code?

### Any suggestions?

_No response_


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py
--- a/qiskit/circuit/parameter.py
+++ b/qiskit/circuit/parameter.py
@@ -138,7 +138,7 @@ def __repr__(self):
 
     def __eq__(self, other):
         if isinstance(other, Parameter):
-            return self._uuid == other._uuid
+            return (self._uuid, self._name) == (other._uuid, other._name)
         elif isinstance(other, ParameterExpression):
             return super().__eq__(other)
         else:


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
@@ -111,10 +111,14 @@ def test_equality(self):
         param = Parameter("a")
         param_copy = Parameter(param.name, uuid=param.uuid)
         param_different = Parameter("a")
+        param_same_uuid_diff_name = Parameter("b", uuid=param.uuid)
 
         self.assertEqual(param, param, "Parameter does not equal itself")
         self.assertEqual(param, param_copy, "Parameters with same data are not equal")
         self.assertNotEqual(param, param_different, "Different Parameters are treated as equal")
+        self.assertNotEqual(
+            param, param_same_uuid_diff_name, "Parameters with different names are treated as equal"
+        )
 
     def test_gate(self):
         """Test instantiating gate with variable parameters"""