From ff1a82e4985b5e0802fb16ef4f383803cb452f4e Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Fri, 19 Jan 2024 01:41:27 +0900 Subject: [PATCH] Revert "Revert pulse model (#11348)" This reverts commit c0702bf66d2e51678feceebbc3c09b674560d674. --- qiskit/pulse/__init__.py | 15 ++ qiskit/pulse/model/__init__.py | 102 ++++++++++++ qiskit/pulse/model/frames.py | 155 ++++++++++++++++++ qiskit/pulse/model/mixed_frames.py | 77 +++++++++ qiskit/pulse/model/pulse_target.py | 203 ++++++++++++++++++++++++ qiskit/transpiler/__init__.py | 2 +- test/python/pulse/test_frames.py | 77 +++++++++ test/python/pulse/test_mixed_frames.py | 65 ++++++++ test/python/pulse/test_pulse_targets.py | 89 +++++++++++ 9 files changed, 784 insertions(+), 1 deletion(-) create mode 100644 qiskit/pulse/model/__init__.py create mode 100644 qiskit/pulse/model/frames.py create mode 100644 qiskit/pulse/model/mixed_frames.py create mode 100644 qiskit/pulse/model/pulse_target.py create mode 100644 test/python/pulse/test_frames.py create mode 100644 test/python/pulse/test_mixed_frames.py create mode 100644 test/python/pulse/test_pulse_targets.py diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 58f13cda060e..39fcc56a9b51 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -40,6 +40,8 @@ .. automodule:: qiskit.pulse.schedule .. automodule:: qiskit.pulse.transforms .. automodule:: qiskit.pulse.builder +.. automodule:: qiskit.pulse.model + .. currentmodule:: qiskit.pulse @@ -156,3 +158,16 @@ ) from qiskit.pulse.library.samplers.decorators import functional_pulse from qiskit.pulse.schedule import Schedule, ScheduleBlock + +from qiskit.pulse.model import ( + PulseTarget, + Port, + LogicalElement, + Qubit, + Coupler, + Frame, + GenericFrame, + QubitFrame, + MeasurementFrame, + MixedFrame, +) diff --git a/qiskit/pulse/model/__init__.py b/qiskit/pulse/model/__init__.py new file mode 100644 index 000000000000..df12fa082e6a --- /dev/null +++ b/qiskit/pulse/model/__init__.py @@ -0,0 +1,102 @@ +# 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. + +r""" +========================================================== +Pulse Targets & Frames (:mod:`qiskit.pulse.model`) +========================================================== + +Pulse is meant to be agnostic to the underlying hardware implementation, while still allowing +low-level control. Qiskit Pulse's pulse targets and frames create a flexible framework +to define where pulse instructions are applied, and what would be their carrier frequency and phase +(because typically AC pulses are used). Each :class:`.PulseTarget` represents a separate component +in the quantum computing system on which instructions could be applied. On the other hand, +each :class:`.Frame` represents a frequency and phase duo for the carrier of the pulse. + +While :class:`.PulseTarget` includes a :class`.Port` variant allowing for direct control over +hardware ports, an abstraction layer is provided by :class:`.LogicalElement`. +The abstraction allows to write pulse level programs with less knowledge of the hardware, and in +a level which is more similar to the circuit level programing. i.e., instead of specifying +ports, one can use Qubits, Couplers, etc. + +This logical and virtual representation allows the user to write template pulse +programs without worrying about the exact details of the hardware implementation +(are the pulses to be played via the same port? Which NCO is used?), while still +allowing for effective utilization of the quantum hardware. The burden of mapping +the different combinations of :class:`.LogicalElement` and :class:`.Frame` +to hardware aware objects is left to the Pulse Compiler. + +.. _pulse_targets: + +PulseTarget +================ +:class:`.PulseTarget` includes :class:`.Port` who's objects are identified by a unique string identifier +defined by the control system, and :class:`.LogicalElement` who's objects are identified by their type +and index. Currently, the most prominent example of a :class:`.LogicalElement` is the +:class:`~.pulse.Qubit`. + +.. autosummary:: + :toctree: ../stubs/ + + Port + Qubit + Coupler + + +.. _frames: + +Frame +============= +:class:`.Frame` s are identified by their type and unique identifier. A :class:`.GenericFrame` is used to +specify custom frequency +and phase duos, while :class:`.QubitFrame` and :class:`.MeasurementFrame` are used to indicate that +backend defaults are to be used (for the qubit's driving frequency and measurement frequency +respectively). + +.. autosummary:: + :toctree: ../stubs/ + + GenericFrame + QubitFrame + MeasurementFrame + + +.. _mixed_frames: + +MixedFrame +============= +The combination of a :class:`.LogicalElement` and :class:`.Frame` is dubbed a :class:`.MixedFrame`. + +.. autosummary:: + :toctree: ../stubs/ + + MixedFrame +""" + +from .pulse_target import ( + PulseTarget, + Port, + LogicalElement, + Qubit, + Coupler, +) + +from .frames import ( + Frame, + GenericFrame, + QubitFrame, + MeasurementFrame, +) + +from .mixed_frames import ( + MixedFrame, +) diff --git a/qiskit/pulse/model/frames.py b/qiskit/pulse/model/frames.py new file mode 100644 index 000000000000..803573301320 --- /dev/null +++ b/qiskit/pulse/model/frames.py @@ -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)) diff --git a/qiskit/pulse/model/mixed_frames.py b/qiskit/pulse/model/mixed_frames.py new file mode 100644 index 000000000000..454cdbf0d2c5 --- /dev/null +++ b/qiskit/pulse/model/mixed_frames.py @@ -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))) diff --git a/qiskit/pulse/model/pulse_target.py b/qiskit/pulse/model/pulse_target.py new file mode 100644 index 000000000000..bb9702ccfad4 --- /dev/null +++ b/qiskit/pulse/model/pulse_target.py @@ -0,0 +1,203 @@ +# 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. + +""" +PulseTarget +""" +from abc import ABC, abstractmethod +from typing import Tuple +import numpy as np + +from qiskit.pulse.exceptions import PulseError + + +class PulseTarget(ABC): + """Base class of pulse target. + + A :class:`PulseTarget` object identifies a hardware component the user can control, the typical + example being playing pulses on. Other examples include measurement related instruments. + + When playing a pulse on a quantum hardware, one typically has to define on what hardware component + the pulse will be played, and the frame (frequency and phase) of the carrier wave. + :class:`PulseTarget` addresses only the first of the two, and identifies the component which is the + target of the pulse. Every played pulse and most other instructions are associated with a + :class:`PulseTarget` on which they are performed. + + A subclass of :class:`PulseTarget` has to be hashable. + """ + + @abstractmethod + def __hash__(self) -> int: + pass + + +class Port(PulseTarget): + """A ``Port`` type ``PulseTarget``. + + A :class:`Port` is the most basic ``PulseTarget`` - simply a hardware port the user can control, + (typically for playing pulses, but not only, for example data acquisition). + + A :class:`Port` is identified by a string, which is set, and must be recognized, by the + backend. Therefore, using pulse level control with :class:`Port` requires an extensive + knowledge of the hardware. Programs with string identifiers which are not recognized by the + backend will fail to execute. + """ + + def __init__(self, name: str): + """Create ``Port``. + + Args: + name: A string identifying the port. + """ + self._name = name + + @property + def name(self) -> str: + """Return the ``name`` of this port.""" + return self._name + + def __eq__(self, other: "Port") -> bool: + """Return True iff self and other are equal, specifically, iff they have the same type + and the same ``name``. + + Args: + other: The Port to compare to this one. + + Returns: + True iff equal. + """ + return type(self) is type(other) and self._name == other._name + + def __hash__(self) -> int: + return hash((self._name, type(self))) + + def __repr__(self) -> str: + return f"Port({self._name})" + + +class LogicalElement(PulseTarget, ABC): + """Base class of logical elements. + + Class :class:`LogicalElement` provides an abstraction layer to ``PulseTarget``. The abstraction + allows to write pulse level programs with less knowledge of the hardware, and in a level which + is more similar to the circuit level programing. i.e., instead of specifying specific ports, one + can use Qubits, Couplers, etc. + + A logical element is identified by its type and index. + """ + + def __init__(self, index: Tuple[int, ...]): + """Create ``LogicalElement``. + + Args: + index: Tuple of indices of the logical element. + """ + self._validate_index(index) + self._index = index + + @property + def index(self) -> Tuple[int, ...]: + """Return the ``index`` of this logical element.""" + return self._index + + @abstractmethod + def _validate_index(self, index) -> None: + """Raise a PulseError if the logical element ``index`` is invalid. + + Raises: + PulseError: If ``index`` is not valid. + """ + pass + + def __eq__(self, other: "LogicalElement") -> bool: + """Return True iff self and other are equal, specifically, iff they have the same type + and the same ``index``. + + Args: + other: The logical element to compare to this one. + + Returns: + True iff equal. + """ + return type(self) is type(other) and self._index == other._index + + def __hash__(self) -> int: + return hash((self._index, type(self))) + + def __repr__(self) -> str: + ind_str = str(self._index) if len(self._index) > 1 else f"({self._index[0]})" + return type(self).__name__ + ind_str + + +class Qubit(LogicalElement): + """Qubit logical element. + + ``Qubit`` represents the different qubits in the system, as identified by + their (positive integer) index values. + """ + + def __init__(self, index: int): + """Qubit logical element. + + Args: + index: Qubit index (positive integer). + """ + super().__init__((index,)) + + @property + def qubit_index(self): + """Index of the Qubit""" + return self.index[0] + + 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[0], (int, np.integer)) or index[0] < 0: + raise PulseError("Qubit index must be a non-negative integer") + + +class Coupler(LogicalElement): + """Coupler logical element. + + :class:`Coupler` represents an element which couples qubits, and can be controlled on its own. + It is identified by the tuple of indices of the coupled qubits. + """ + + def __init__(self, *qubits): + """Coupler logical element. + + The coupler ``index`` is defined as the ``tuple`` (\\*qubits). + + Args: + *qubits: any number of qubit indices coupled by the coupler. + """ + super().__init__(tuple(qubits)) + + def _validate_index(self, index) -> None: + """Raise a ``PulseError`` if the coupler ``index`` is invalid. Namely, + check if coupled qubit indices are non-negative integers, at least two indices were provided, + and that the indices don't repeat. + + Raises: + PulseError: If ``index`` is invalid. + """ + if len(index) < 2: + raise PulseError("At least two qubit indices are needed for a Coupler") + for qubit_index in index: + if not isinstance(qubit_index, (int, np.integer)) or qubit_index < 0: + raise PulseError("Both indices of coupled qubits must be non-negative integers") + if len(set(index)) != len(index): + raise PulseError("Indices of a coupler can not repeat") diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 6e1522e81fa9..454496bd8d9d 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -1059,7 +1059,7 @@ C ░░░░░░░░░░░░░░░░▒▒░ However, the :class:`.QuantumCircuit` representation is not accurate enough to represent -this model. In the circuit representation, the corresponding :class:`.circuit.Qubit` is occupied +this model. In the circuit representation, the corresponding :class:`.pulse.Qubit` is occupied by the stimulus microwave signal during the first half of the interval, and the :class:`.Clbit` is only occupied at the very end of the interval. diff --git a/test/python/pulse/test_frames.py b/test/python/pulse/test_frames.py new file mode 100644 index 000000000000..1a9b99e1139f --- /dev/null +++ b/test/python/pulse/test_frames.py @@ -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. + +"""Test pulse logical elements and frames""" + +from qiskit.pulse import ( + PulseError, + GenericFrame, + QubitFrame, + MeasurementFrame, +) +from qiskit.test import QiskitTestCase + + +class TestFrames(QiskitTestCase): + """Test frames.""" + + def test_generic_frame_initialization(self): + """Test that Frame objects are created correctly""" + frame = GenericFrame(name="frame1") + self.assertEqual(frame.name, "frame1") + self.assertEqual(str(frame), "GenericFrame(frame1)") + + def test_generic_frame_comparison(self): + """Test that GenericFrame objects are compared correctly""" + frame1 = GenericFrame(name="frame1") + + self.assertEqual(frame1, GenericFrame(name="frame1")) + self.assertNotEqual(frame1, GenericFrame(name="frame2")) + self.assertNotEqual(frame1, QubitFrame(3)) + + def test_qubit_frame_initialization(self): + """Test that QubitFrame type frames are created and validated correctly""" + frame = QubitFrame(2) + self.assertEqual(frame.index, 2) + self.assertEqual(str(frame), "QubitFrame(2)") + + with self.assertRaises(PulseError): + QubitFrame(0.5) + with self.assertRaises(PulseError): + QubitFrame(-0.5) + with self.assertRaises(PulseError): + QubitFrame(-1) + + def test_qubit_frame_comparison(self): + """Test the comparison of QubitFrame""" + self.assertEqual(QubitFrame(0), QubitFrame(0)) + self.assertNotEqual(QubitFrame(0), QubitFrame(1)) + self.assertNotEqual(MeasurementFrame(0), QubitFrame(0)) + + def test_measurement_frame_initialization(self): + """Test that MeasurementFrame type frames are created and validated correctly""" + frame = MeasurementFrame(2) + self.assertEqual(frame.index, 2) + self.assertEqual(str(frame), "MeasurementFrame(2)") + + with self.assertRaises(PulseError): + MeasurementFrame(0.5) + with self.assertRaises(PulseError): + MeasurementFrame(-0.5) + with self.assertRaises(PulseError): + MeasurementFrame(-1) + + def test_measurement_frame_comparison(self): + """Test the comparison of measurement frames""" + self.assertEqual(MeasurementFrame(0), MeasurementFrame(0)) + self.assertNotEqual(MeasurementFrame(0), MeasurementFrame(1)) + self.assertNotEqual(MeasurementFrame(0), QubitFrame(0)) diff --git a/test/python/pulse/test_mixed_frames.py b/test/python/pulse/test_mixed_frames.py new file mode 100644 index 000000000000..95ebd556d1c0 --- /dev/null +++ b/test/python/pulse/test_mixed_frames.py @@ -0,0 +1,65 @@ +# 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. + +"""Test pulse logical elements and frames""" + +from qiskit.pulse import ( + Port, + Qubit, + GenericFrame, + MixedFrame, +) +from qiskit.test import QiskitTestCase + + +class TestMixedFrames(QiskitTestCase): + """Test mixed frames.""" + + def test_mixed_frame_initialization(self): + """Test that MixedFrame objects are created correctly""" + frame = GenericFrame("frame1") + qubit = Qubit(1) + mixed_frame = MixedFrame(qubit, frame) + self.assertEqual(mixed_frame.pulse_target, qubit) + self.assertEqual(mixed_frame.frame, frame) + + port = Port("d0") + mixed_frame = MixedFrame(port, frame) + self.assertEqual(mixed_frame.pulse_target, port) + + def test_mixed_frames_comparison(self): + """Test the comparison of various mixed frames""" + self.assertEqual( + MixedFrame(Qubit(1), GenericFrame("a")), + MixedFrame(Qubit(1), GenericFrame("a")), + ) + + self.assertEqual( + MixedFrame(Port("s"), GenericFrame("a")), + MixedFrame(Port("s"), GenericFrame("a")), + ) + + self.assertNotEqual( + MixedFrame(Qubit(1), GenericFrame("a")), + MixedFrame(Qubit(2), GenericFrame("a")), + ) + self.assertNotEqual( + MixedFrame(Qubit(1), GenericFrame("a")), + MixedFrame(Qubit(1), GenericFrame("b")), + ) + + def test_mixed_frame_repr(self): + """Test MixedFrame __repr__""" + frame = GenericFrame("frame1") + qubit = Qubit(1) + mixed_frame = MixedFrame(qubit, frame) + self.assertEqual(str(mixed_frame), f"MixedFrame({qubit},{frame})") diff --git a/test/python/pulse/test_pulse_targets.py b/test/python/pulse/test_pulse_targets.py new file mode 100644 index 000000000000..d3a8c92c3216 --- /dev/null +++ b/test/python/pulse/test_pulse_targets.py @@ -0,0 +1,89 @@ +# 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. + +"""Test pulse logical elements and frames""" + +from qiskit.pulse import ( + PulseError, + Qubit, + Coupler, + Port, +) +from qiskit.test import QiskitTestCase + + +class TestLogicalElements(QiskitTestCase): + """Test logical elements.""" + + def test_qubit_initialization(self): + """Test that Qubit type logical elements are created and validated correctly""" + qubit = Qubit(0) + self.assertEqual(qubit.index, (0,)) + self.assertEqual(qubit.qubit_index, 0) + self.assertEqual(str(qubit), "Qubit(0)") + + with self.assertRaises(PulseError): + Qubit(0.5) + with self.assertRaises(PulseError): + Qubit(-0.5) + with self.assertRaises(PulseError): + Qubit(-1) + + def test_coupler_initialization(self): + """Test that Coupler type logical elements are created and validated correctly""" + coupler = Coupler(0, 3) + self.assertEqual(coupler.index, (0, 3)) + self.assertEqual(str(coupler), "Coupler(0, 3)") + + coupler = Coupler(0, 3, 2) + self.assertEqual(coupler.index, (0, 3, 2)) + + with self.assertRaises(PulseError): + Coupler(-1, 0) + with self.assertRaises(PulseError): + Coupler(2, -0.5) + with self.assertRaises(PulseError): + Coupler(3, -1) + with self.assertRaises(PulseError): + Coupler(0, 0, 1) + with self.assertRaises(PulseError): + Coupler(0) + + def test_logical_elements_comparison(self): + """Test the comparison of various logical elements""" + self.assertEqual(Qubit(0), Qubit(0)) + self.assertNotEqual(Qubit(0), Qubit(1)) + + self.assertEqual(Coupler(0, 1), Coupler(0, 1)) + self.assertNotEqual(Coupler(0, 1), Coupler(0, 2)) + + +class TestPorts(QiskitTestCase): + """Test ports.""" + + def test_ports_initialization(self): + """Test that Ports are created correctly""" + port = Port("d0") + self.assertEqual(port.name, "d0") + + def test_ports_comparison(self): + """Test that Ports are compared correctly""" + port1 = Port("d0") + port2 = Port("d0") + port3 = Port("d1") + self.assertEqual(port1, port2) + self.assertNotEqual(port1, port3) + + def test_ports_representation(self): + """Test Ports repr""" + port1 = Port("d0") + self.assertEqual(str(port1), "Port(d0)")