diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py index 175d93ef8e87..c88fddadd77b 100644 --- a/qiskit/pulse/channels.py +++ b/qiskit/pulse/channels.py @@ -165,6 +165,12 @@ class PulseChannel(Channel, metaclass=ABCMeta): pass +class ClassicalIOChannel(Channel, metaclass=ABCMeta): + """Base class of classical IO channels. These cannot have instructions scheduled on them.""" + + pass + + class DriveChannel(PulseChannel): """Drive channels transmit signals to qubits which enact gate operations.""" @@ -192,7 +198,7 @@ class AcquireChannel(Channel): prefix = "a" -class SnapshotChannel(Channel): +class SnapshotChannel(ClassicalIOChannel): """Snapshot channels are used to specify instructions for simulators.""" prefix = "s" @@ -202,13 +208,13 @@ def __init__(self): super().__init__(0) -class MemorySlot(Channel): +class MemorySlot(ClassicalIOChannel): """Memory slot channels represent classical memory storage.""" prefix = "m" -class RegisterSlot(Channel): +class RegisterSlot(ClassicalIOChannel): """Classical resister slot channels represent classical registers (low-latency classical memory). """ diff --git a/qiskit/pulse/instructions/frequency.py b/qiskit/pulse/instructions/frequency.py index 09233080b593..15866e0ae79a 100644 --- a/qiskit/pulse/instructions/frequency.py +++ b/qiskit/pulse/instructions/frequency.py @@ -16,7 +16,7 @@ from typing import Optional, Union, Tuple from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.channels import PulseChannel +from qiskit.pulse.channels import PulseChannel, PulseError from qiskit.pulse.instructions.instruction import Instruction @@ -46,7 +46,13 @@ def __init__( frequency: New frequency of the channel in Hz. channel: The channel this instruction operates on. name: Name of this set channel frequency instruction. + Raises: + PulseError: If channel is not a PulseChannel. """ + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `SetFrequency` must be of type `channels.PulseChannel`." + ) if not isinstance(frequency, ParameterExpression): frequency = float(frequency) super().__init__(operands=(frequency, channel), name=name) @@ -93,9 +99,15 @@ def __init__( frequency: Frequency shift of the channel in Hz. channel: The channel this instruction operates on. name: Name of this set channel frequency instruction. + Raises: + PulseError: If channel is not a PulseChannel. """ if not isinstance(frequency, ParameterExpression): frequency = float(frequency) + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `ShiftFrequency` must be of type `channels.PulseChannel`." + ) super().__init__(operands=(frequency, channel), name=name) @property diff --git a/qiskit/pulse/instructions/phase.py b/qiskit/pulse/instructions/phase.py index b9067486d923..d8db037a5aa1 100644 --- a/qiskit/pulse/instructions/phase.py +++ b/qiskit/pulse/instructions/phase.py @@ -18,7 +18,7 @@ from typing import Optional, Union, Tuple from qiskit.circuit import ParameterExpression -from qiskit.pulse.channels import PulseChannel +from qiskit.pulse.channels import PulseChannel, PulseError from qiskit.pulse.instructions.instruction import Instruction @@ -52,7 +52,14 @@ def __init__( phase: The rotation angle in radians. channel: The channel this instruction operates on. name: Display name for this instruction. + + Raises: + PulseError: If channel is not a PulseChannel. """ + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `ShiftPhase` must be of type `channels.PulseChannel`." + ) super().__init__(operands=(phase, channel), name=name) @property @@ -108,7 +115,13 @@ def __init__( phase: The rotation angle in radians. channel: The channel this instruction operates on. name: Display name for this instruction. + Raises: + PulseError: If channel is not a PulseChannel. """ + if not isinstance(channel, PulseChannel): + raise PulseError( + "The `channel` argument to `SetPhase` must be of type `channels.PulseChannel`." + ) super().__init__(operands=(phase, channel), name=name) @property diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 2a8536643e76..41effc205d83 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -41,7 +41,7 @@ def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = No name: Name of the instruction for display purposes. Defaults to ``pulse.name``. Raises: - PulseError: If pulse is not a Pulse type. + PulseError: If pulse is not a Pulse type, or channel is not a PulseChannel. """ if not isinstance(pulse, Pulse): raise PulseError("The `pulse` argument to `Play` must be of type `library.Pulse`.") diff --git a/qiskit/pulse/transforms/canonicalization.py b/qiskit/pulse/transforms/canonicalization.py index a019de2eda27..4e2409ffa354 100644 --- a/qiskit/pulse/transforms/canonicalization.py +++ b/qiskit/pulse/transforms/canonicalization.py @@ -18,6 +18,7 @@ import numpy as np from qiskit.pulse import channels as chans, exceptions, instructions +from qiskit.pulse.channels import ClassicalIOChannel from qiskit.pulse.exceptions import PulseError from qiskit.pulse.exceptions import UnassignedDurationError from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap @@ -475,6 +476,8 @@ def pad( channels = channels or schedule.channels for channel in channels: + if isinstance(channel, ClassicalIOChannel): + continue if channel not in schedule.channels: schedule |= instructions.Delay(until, channel) continue diff --git a/releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml b/releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml new file mode 100644 index 000000000000..71a79556a4c8 --- /dev/null +++ b/releasenotes/notes/introduce-classical-io-channel-0a616e6ca75b7687.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + In the qiskit.pulse.channels package, ClassicalIOChannel class has been added + as an abstract base class of MemorySlot, RegisterSlot, and SnapshotChannel. + + The qiskit.pulse.transforms.canonicalization.pad method does not introduce + delays to any channels which are instances of ClassicalIOChannel. + + In qiskit.pulse.instructions, the constructors to SetPhase, ShiftPhase, + SetFrequency and ShiftFrequency now throw a PulseError if the channel parameter + is not of type PulseChannel. + diff --git a/test/python/pulse/test_channels.py b/test/python/pulse/test_channels.py index 3637a1cea121..604202fb5754 100644 --- a/test/python/pulse/test_channels.py +++ b/test/python/pulse/test_channels.py @@ -17,8 +17,9 @@ from qiskit.pulse.channels import ( AcquireChannel, Channel, - DriveChannel, + ClassicalIOChannel, ControlChannel, + DriveChannel, MeasureChannel, MemorySlot, PulseChannel, @@ -67,8 +68,17 @@ def test_channel_hash(self): self.assertEqual(hash_1, hash_2) +class TestClassicalIOChannel(QiskitTestCase): + """Test base classical IO channel.""" + + def test_cannot_be_instantiated(self): + """Test base classical IO channel cannot be instantiated.""" + with self.assertRaises(NotImplementedError): + ClassicalIOChannel(0) + + class TestMemorySlot(QiskitTestCase): - """AcquireChannel tests.""" + """MemorySlot tests.""" def test_default(self): """Test default memory slot.""" @@ -76,6 +86,7 @@ def test_default(self): self.assertEqual(memory_slot.index, 123) self.assertEqual(memory_slot.name, "m123") + self.assertTrue(isinstance(memory_slot, ClassicalIOChannel)) class TestRegisterSlot(QiskitTestCase): @@ -87,6 +98,7 @@ def test_default(self): self.assertEqual(register_slot.index, 123) self.assertEqual(register_slot.name, "c123") + self.assertTrue(isinstance(register_slot, ClassicalIOChannel)) class TestSnapshotChannel(QiskitTestCase): @@ -98,6 +110,7 @@ def test_default(self): self.assertEqual(snapshot_channel.index, 0) self.assertEqual(snapshot_channel.name, "s0") + self.assertTrue(isinstance(snapshot_channel, ClassicalIOChannel)) class TestDriveChannel(QiskitTestCase): diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index a5284c27ed2f..111ebd35f4ae 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -156,6 +156,64 @@ def test_freq(self): ) self.assertEqual(repr(set_freq), "SetFrequency(4500000000.0, DriveChannel(1), name='test')") + def test_freq_non_pulse_channel(self): + """Test set frequency constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.SetFrequency(4.5e9, channels.RegisterSlot(1), name="test") + + +class TestShiftFrequency(QiskitTestCase): + """Shift frequency tests.""" + + def test_shift_freq(self): + """Test shift frequency basic functionality.""" + shift_freq = instructions.ShiftFrequency(4.5e9, channels.DriveChannel(1), name="test") + + self.assertIsInstance(shift_freq.id, int) + self.assertEqual(shift_freq.duration, 0) + self.assertEqual(shift_freq.frequency, 4.5e9) + self.assertEqual(shift_freq.operands, (4.5e9, channels.DriveChannel(1))) + self.assertEqual( + shift_freq, instructions.ShiftFrequency(4.5e9, channels.DriveChannel(1), name="test") + ) + self.assertNotEqual( + shift_freq, instructions.ShiftFrequency(4.5e8, channels.DriveChannel(1), name="test") + ) + self.assertEqual( + repr(shift_freq), "ShiftFrequency(4500000000.0, DriveChannel(1), name='test')" + ) + + def test_freq_non_pulse_channel(self): + """Test shift frequency constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.ShiftFrequency(4.5e9, channels.RegisterSlot(1), name="test") + + +class TestSetPhase(QiskitTestCase): + """Test the instruction construction.""" + + def test_default(self): + """Test basic SetPhase.""" + set_phase = instructions.SetPhase(1.57, channels.DriveChannel(0)) + + self.assertIsInstance(set_phase.id, int) + self.assertEqual(set_phase.name, None) + self.assertEqual(set_phase.duration, 0) + self.assertEqual(set_phase.phase, 1.57) + self.assertEqual(set_phase.operands, (1.57, channels.DriveChannel(0))) + self.assertEqual( + set_phase, instructions.SetPhase(1.57, channels.DriveChannel(0), name="test") + ) + self.assertNotEqual( + set_phase, instructions.SetPhase(1.57j, channels.DriveChannel(0), name="test") + ) + self.assertEqual(repr(set_phase), "SetPhase(1.57, DriveChannel(0))") + + def test_set_phase_non_pulse_channel(self): + """Test shift phase constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.SetPhase(1.57, channels.RegisterSlot(1), name="test") + class TestShiftPhase(QiskitTestCase): """Test the instruction construction.""" @@ -177,6 +235,11 @@ def test_default(self): ) self.assertEqual(repr(shift_phase), "ShiftPhase(1.57, DriveChannel(0))") + def test_shift_phase_non_pulse_channel(self): + """Test shift phase constructor with illegal channel""" + with self.assertRaises(exceptions.PulseError): + instructions.ShiftPhase(1.57, channels.RegisterSlot(1), name="test") + class TestSnapshot(QiskitTestCase): """Snapshot tests.""" diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index 49038afdb096..e19d024c2b4f 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -29,7 +29,13 @@ Constant, ) from qiskit.pulse import transforms, instructions -from qiskit.pulse.channels import MemorySlot, DriveChannel, AcquireChannel +from qiskit.pulse.channels import ( + MemorySlot, + DriveChannel, + AcquireChannel, + RegisterSlot, + SnapshotChannel, +) from qiskit.pulse.instructions import directives from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeOpenPulse2Q @@ -349,6 +355,23 @@ def test_padding_prepended_delay(self): self.assertEqual(transforms.pad(sched, until=30, inplace=True), ref_sched) + def test_pad_no_delay_on_classical_io_channels(self): + """Test padding does not apply to classical IO channels.""" + delay = 10 + sched = ( + Delay(delay, MemorySlot(0)).shift(20) + + Delay(delay, RegisterSlot(0)).shift(10) + + Delay(delay, SnapshotChannel()) + ) + + ref_sched = ( + Delay(delay, MemorySlot(0)).shift(20) + + Delay(delay, RegisterSlot(0)).shift(10) + + Delay(delay, SnapshotChannel()) + ) + + self.assertEqual(transforms.pad(sched, until=15), ref_sched) + def get_pulse_ids(schedules: List[Schedule]) -> Set[int]: """Returns ids of pulses used in Schedules."""