Skip to content

Commit

Permalink
Fix Channel.__hash__ in multiprocessing contexts (#11251)
Browse files Browse the repository at this point in the history
* Fix `Channel.__hash__` in multiprocessing contexts

Storing an explicit hash key is fragile in cases that a channel might be
created in a different process to where it might be compared or the hash
used, because the hash seeding can vary depending on how the new
interpreter process was created, especially if it's not done by `fork`.

In this case, transmitting the stored `_hash` over pickle meant that a
`DriveChannel(0)` created in the main process of a macOS runner could
compare equal to a `DriveChannel(0)` created in a separate process
(standard start method `spawn`) and pickled over the wire to the main
process, but have different hashes, violating the Python data model.

Instead, we can just use the standard Python behaviour of creating the
hash on demand when requested; this should typically be preferred unless
absolutely necessary for critical performance reasons, because it will
generally fail safe.

* Fix `hash` and equality in other pulse objects

This removes all caching of items' `hash`es.  This practice is quite
fraught in multiprocessing contexts, and should only be done when it is
absolutely performance critical.

In a couple of cases, the pulse objects were using the cached `hash` as
the main component of their `__eq__` methods, which is not correct; it's
totally valid to have hash collisions without implying that two objects
are equal.

(cherry picked from commit 3c1a87c)

# Conflicts:
#	qiskit/pulse/model/frames.py
#	qiskit/pulse/model/mixed_frames.py
#	qiskit/pulse/model/pulse_target.py
  • Loading branch information
jakelishman authored and mergify[bot] committed Nov 15, 2023
1 parent 31a84c5 commit 3747119
Show file tree
Hide file tree
Showing 6 changed files with 443 additions and 6 deletions.
3 changes: 1 addition & 2 deletions qiskit/pulse/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ def __init__(self, index: int):
"""
self._validate_index(index)
self._index = index
self._hash = hash((self.__class__.__name__, self._index))

@property
def index(self) -> Union[int, ParameterExpression]:
Expand Down Expand Up @@ -156,7 +155,7 @@ def __eq__(self, other: "Channel") -> bool:
return type(self) is type(other) and self._index == other._index

def __hash__(self):
return self._hash
return hash((type(self), self._index))


class PulseChannel(Channel, metaclass=ABCMeta):
Expand Down
5 changes: 1 addition & 4 deletions qiskit/pulse/instructions/instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def __init__(
"""
self._operands = operands
self._name = name
self._hash = None
self._validate()

def _validate(self):
Expand Down Expand Up @@ -301,9 +300,7 @@ def __eq__(self, other: "Instruction") -> bool:
return isinstance(other, type(self)) and self.operands == other.operands

def __hash__(self) -> int:
if self._hash is None:
self._hash = hash((type(self), self.operands, self.name))
return self._hash
return hash((type(self), self.operands, self.name))

def __add__(self, other):
"""Return a new schedule with `other` inserted within `self` at `start_time`.
Expand Down
155 changes: 155 additions & 0 deletions qiskit/pulse/model/frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Frames
"""

from abc import ABC

import numpy as np

from qiskit.pulse.exceptions import PulseError


class Frame(ABC):
"""Base class for pulse module frame.
Because pulses used in Quantum hardware are typically AC pulses, the carrier frequency and phase
must be defined. The :class:`Frame` is the object which identifies the frequency and phase for
the carrier.
and each pulse and most other instructions are associated with a frame. The different types of frames
dictate how the frequency and phase duo are defined.
The default initial phase for every frame is 0.
"""


class GenericFrame(Frame):
"""Pulse module GenericFrame.
The :class:`GenericFrame` is used for custom user defined frames, which are not associated with any
backend defaults. It is especially useful when the frame doesn't correspond to any frame of
the typical qubit model, like qudit control for example. Because no backend defaults exist for
these frames, during compilation an initial frequency and phase will need to be provided.
:class:`GenericFrame` objects are identified by their unique name.
"""

def __init__(self, name: str):
"""Create ``GenericFrame``.
Args:
name: A unique identifier used to identify the frame.
"""
self._name = name

@property
def name(self) -> str:
"""Return the name of the frame."""
return self._name

def __repr__(self) -> str:
return f"GenericFrame({self._name})"

def __eq__(self, other):
return type(self) is type(other) and self._name == other._name

def __hash__(self):
return hash((type(self), self._name))


class QubitFrame(Frame):
"""A frame associated with the driving of a qubit.
:class:`QubitFrame` is a frame associated with the driving of a specific qubit.
The initial frequency of
the frame will be taken as the default driving frequency provided by the backend
during compilation.
"""

def __init__(self, index: int):
"""Create ``QubitFrame``.
Args:
index: The index of the qubit represented by the frame.
"""
self._validate_index(index)
self._index = index

@property
def index(self) -> int:
"""Return the qubit index of the qubit frame."""
return self._index

def _validate_index(self, index) -> None:
"""Raise a ``PulseError`` if the qubit index is invalid. Namely, check if the index is a
non-negative integer.
Raises:
PulseError: If ``identifier`` (index) is a negative integer.
"""
if not isinstance(index, (int, np.integer)) or index < 0:
raise PulseError("Qubit index must be a non-negative integer")

def __repr__(self) -> str:
return f"QubitFrame({self._index})"

def __eq__(self, other):
return type(self) is type(other) and self._index == other._index

def __hash__(self):
return hash((type(self), self._index))


class MeasurementFrame(Frame):
"""A frame associated with the measurement of a qubit.
``MeasurementFrame`` is a frame associated with the readout of a specific qubit,
which requires a stimulus tone driven at frequency off resonant to qubit drive.
If not set otherwise, the initial frequency of the frame will be taken as the default
measurement frequency provided by the backend during compilation.
"""

def __init__(self, index: int):
"""Create ``MeasurementFrame``.
Args:
index: The index of the qubit represented by the frame.
"""
self._validate_index(index)
self._index = index

@property
def index(self) -> int:
"""Return the qubit index of the measurement frame."""
return self._index

def _validate_index(self, index) -> None:
"""Raise a ``PulseError`` if the qubit index is invalid. Namely, check if the index is a
non-negative integer.
Raises:
PulseError: If ``index`` is a negative integer.
"""
if not isinstance(index, (int, np.integer)) or index < 0:
raise PulseError("Qubit index must be a non-negative integer")

def __repr__(self) -> str:
return f"MeasurementFrame({self._index})"

def __eq__(self, other):
return type(self) is type(other) and self._index == other._index

def __hash__(self):
return hash((type(self), self._index))
77 changes: 77 additions & 0 deletions qiskit/pulse/model/mixed_frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Mixed Frames
"""

from .frames import Frame
from .pulse_target import PulseTarget


class MixedFrame:
"""Representation of a :class:`PulseTarget` and :class:`Frame` combination.
Most instructions need to be associated with both a :class:`PulseTarget` and a
:class:`Frame`. The combination
of the two is called a mixed frame and is represented by a :class:`MixedFrame` object.
In most cases the :class:`MixedFrame` is used more by the compiler, and a pulse program
can be written without :class:`MixedFrame` s, by setting :class:`PulseTarget` and
:class:`Frame` independently. However, in some cases using :class:`MixedFrame` s can
better convey the meaning of the code, and change the compilation process. One example
is the use of the shift/set frequency/phase instructions which are not broadcasted to other
:class:`MixedFrame` s if applied on a specific :class:`MixedFrame` (unlike the behavior
of :class:`Frame`). User can also use a subclass of :class:`MixedFrame` for a particular
combination of logical elements and frames as if a syntactic sugar. This might
increase the readability of a user pulse program. As an example consider the cross
resonance architecture, in which a pulse is played on a target qubit frame and applied
to a control qubit logical element.
"""

def __init__(self, pulse_target: PulseTarget, frame: Frame):
"""Create ``MixedFrame``.
Args:
pulse_target: The ``PulseTarget`` associated with the mixed frame.
frame: The frame associated with the mixed frame.
"""
self._pulse_target = pulse_target
self._frame = frame

@property
def pulse_target(self) -> PulseTarget:
"""Return the target of this mixed frame."""
return self._pulse_target

@property
def frame(self) -> Frame:
"""Return the ``Frame`` of this mixed frame."""
return self._frame

def __repr__(self) -> str:
return f"MixedFrame({self.pulse_target},{self.frame})"

def __eq__(self, other: "MixedFrame") -> bool:
"""Return True iff self and other are equal, specifically, iff they have the same target
and frame.
Args:
other: The mixed frame to compare to this one.
Returns:
True iff equal.
"""
return self._pulse_target == other._pulse_target and self._frame == other._frame

def __hash__(self) -> int:
return hash((self._pulse_target, self._frame, type(self)))
Loading

0 comments on commit 3747119

Please sign in to comment.