Skip to content

Parameter instances can compare equal while having different hash values #11654

@wshanks

Description

@wshanks

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:

.. 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:

  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 🙂). 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 Parameters 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 Parameters 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions