-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Description
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 as requiring:
The only required property is that objects which compare equal have the same hash value
How can we reproduce the issue?
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 #11647 (comment), @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:
qiskit/qiskit/pulse/schedule.py
Lines 882 to 912 in ce02f06
.. rubric:: Program Scoping | |
When you call a subroutine from another subroutine, or append a schedule block | |
to another schedule block, the management of references and parameters | |
can be a hard task. Schedule block offers a convenient feature to help with this | |
by automatically scoping the parameters and subroutines. | |
.. code-block:: | |
from qiskit import pulse | |
from qiskit.circuit.parameter import Parameter | |
amp1 = Parameter("amp") | |
with pulse.build() as sched1: | |
pulse.play(pulse.Constant(100, amp1), pulse.DriveChannel(0)) | |
print(sched1.scoped_parameters()) | |
.. parsed-literal:: | |
(Parameter(root::amp),) | |
The :meth:`~ScheduleBlock.scoped_parameters` method returns all :class:`~.Parameter` | |
objects defined in the schedule block. The parameter name is updated to reflect | |
its scope information, i.e. where it is defined. | |
The outer scope is called "root". Since the "amp" parameter is directly used | |
in the current builder context, it is prefixed with "root". | |
Note that the :class:`Parameter` object returned by :meth:`~ScheduleBlock.scoped_parameters` | |
preserves the hidden `UUID`_ key, and thus the scoped name doesn't break references | |
to the original :class:`Parameter`. |
So we have to decide which way to go:
- 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. - 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 seescoped_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 🙂). 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