Repository: Qiskit/qiskit-terra
Pull request #5681 (2021-01-22 11:20:39)

ScheduleBlock 1/4 - remove timeslot dependency from instruction
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary
This is 1/4 step of `ScheduleBlock` implementation #5679 


### Details and comments

This PR enables to assign `ParameterExpression` object to the duration of pulse `Instruction` and `Pulse`. This is the basic requirement of the `ScheduleBlock` implementation. The class variable `timeslots` is completely removed from `Instruction` and this is generated by `Schedule` on the fly (thus `Schedule` will become only place we evaluate the instruction overlap). With this update we can assign `ParameterExpression` object to `Instruction` and `Pulse`.

nkanazawa1989: Thanks for reviewing the code. I added a reno and minor updated the code based on your feedback. I'll move on to the 2/4.

Patch:
diff --git a/qiskit/pulse/exceptions.py b/qiskit/pulse/exceptions.py
index a67c6dc8a..4c1abd310 100644
--- a/qiskit/pulse/exceptions.py
+++ b/qiskit/pulse/exceptions.py
@@ -33,3 +33,7 @@ class BackendNotSet(PulseError):
 
 class NoActiveBuilder(PulseError):
     """Raised if no builder context is active."""
+
+
+class UnassignedDurationError(PulseError):
+    """Raised if instruction duration is unassigned."""
diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py
index 9fe386dad..b67e0f71c 100644
--- a/qiskit/pulse/instructions/acquire.py
+++ b/qiskit/pulse/instructions/acquire.py
@@ -13,8 +13,9 @@
 """The Acquire instruction is used to trigger the qubit measurement unit and provide
 some metadata for the acquisition process, for example, where to store classified readout data.
 """
-from typing import Optional
+from typing import Optional, Union
 
+from qiskit.circuit import ParameterExpression
 from qiskit.pulse.channels import MemorySlot, RegisterSlot, AcquireChannel
 from qiskit.pulse.configuration import Kernel, Discriminator
 from qiskit.pulse.exceptions import PulseError
@@ -39,7 +40,7 @@ class Acquire(Instruction):
     """
 
     def __init__(self,
-                 duration: int,
+                 duration: Union[int, ParameterExpression],
                  channel: AcquireChannel,
                  mem_slot: Optional[MemorySlot] = None,
                  reg_slot: Optional[RegisterSlot] = None,
@@ -73,9 +74,8 @@ class Acquire(Instruction):
         self._kernel = kernel
         self._discriminator = discriminator
 
-        all_channels = [chan for chan in [channel, mem_slot, reg_slot] if chan is not None]
-        super().__init__((duration, channel, mem_slot, reg_slot),
-                         duration, all_channels, name=name)
+        all_channels = tuple([chan for chan in [channel, mem_slot, reg_slot] if chan is not None])
+        super().__init__((duration, channel, mem_slot, reg_slot), None, all_channels, name=name)
 
     @property
     def channel(self) -> AcquireChannel:
@@ -84,6 +84,11 @@ class Acquire(Instruction):
         """
         return self.operands[1]
 
+    @property
+    def duration(self) -> Union[int, ParameterExpression]:
+        """Duration of this instruction."""
+        return self.operands[0]
+
     @property
     def kernel(self) -> Kernel:
         """Return kernel settings."""
diff --git a/qiskit/pulse/instructions/delay.py b/qiskit/pulse/instructions/delay.py
index 083f841a7..61e2a623f 100644
--- a/qiskit/pulse/instructions/delay.py
+++ b/qiskit/pulse/instructions/delay.py
@@ -11,8 +11,9 @@
 # that they have been altered from the originals.
 
 """An instruction for blocking time on a channel; useful for scheduling alignment."""
-from typing import Optional
+from typing import Optional, Union
 
+from qiskit.circuit import ParameterExpression
 from qiskit.pulse.channels import Channel
 from qiskit.pulse.instructions.instruction import Instruction
 
@@ -33,7 +34,7 @@ class Delay(Instruction):
         The ``channel`` will output no signal from time=0 up until time=10.
     """
 
-    def __init__(self, duration: int,
+    def __init__(self, duration: Union[int, ParameterExpression],
                  channel: Channel,
                  name: Optional[str] = None):
         """Create a new delay instruction.
@@ -45,7 +46,7 @@ class Delay(Instruction):
             channel: The channel that will have the delay.
             name: Name of the delay for display purposes.
         """
-        super().__init__((duration, channel), duration, (channel,), name=name)
+        super().__init__((duration, channel), None, (channel,), name=name)
 
     @property
     def channel(self) -> Channel:
@@ -53,3 +54,8 @@ class Delay(Instruction):
         scheduled on.
         """
         return self.operands[1]
+
+    @property
+    def duration(self) -> Union[int, ParameterExpression]:
+        """Duration of this instruction."""
+        return self.operands[0]
diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py
index 6177f6f0a..62ab8f317 100644
--- a/qiskit/pulse/instructions/directives.py
+++ b/qiskit/pulse/instructions/directives.py
@@ -24,6 +24,11 @@ class Directive(instruction.Instruction, ABC):
     This is a hint to the pulse compiler and is not loaded into hardware.
     """
 
+    @property
+    def duration(self) -> int:
+        """Duration of this instruction."""
+        return 0
+
 
 class RelativeBarrier(Directive):
     """Pulse ``RelativeBarrier`` directive."""
@@ -41,7 +46,7 @@ class RelativeBarrier(Directive):
             channels: The channel that the barrier applies to.
             name: Name of the directive for display purposes.
         """
-        super().__init__(tuple(channels), 0, tuple(channels), name=name)
+        super().__init__(tuple(channels), None, tuple(channels), name=name)
 
     def __eq__(self, other):
         """Verify two barriers are equivalent."""
diff --git a/qiskit/pulse/instructions/frequency.py b/qiskit/pulse/instructions/frequency.py
index 1d2e8f554..77730b975 100644
--- a/qiskit/pulse/instructions/frequency.py
+++ b/qiskit/pulse/instructions/frequency.py
@@ -46,7 +46,7 @@ class SetFrequency(Instruction):
         """
         if not isinstance(frequency, ParameterExpression):
             frequency = float(frequency)
-        super().__init__((frequency, channel), 0, (channel,), name=name)
+        super().__init__((frequency, channel), None, (channel,), name=name)
 
     @property
     def frequency(self) -> Union[float, ParameterExpression]:
@@ -60,6 +60,11 @@ class SetFrequency(Instruction):
         """
         return self.operands[1]
 
+    @property
+    def duration(self) -> int:
+        """Duration of this instruction."""
+        return 0
+
 
 class ShiftFrequency(Instruction):
     """Shift the channel frequency away from the current frequency."""
@@ -77,7 +82,7 @@ class ShiftFrequency(Instruction):
         """
         if not isinstance(frequency, ParameterExpression):
             frequency = float(frequency)
-        super().__init__((frequency, channel), 0, (channel,), name=name)
+        super().__init__((frequency, channel), None, (channel,), name=name)
 
     @property
     def frequency(self) -> Union[float, ParameterExpression]:
@@ -90,3 +95,8 @@ class ShiftFrequency(Instruction):
         scheduled on.
         """
         return self.operands[1]
+
+    @property
+    def duration(self) -> int:
+        """Duration of this instruction."""
+        return 0
diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py
index 8475e3023..1fdc3ce33 100644
--- a/qiskit/pulse/instructions/instruction.py
+++ b/qiskit/pulse/instructions/instruction.py
@@ -22,17 +22,16 @@ For example::
     sched += Delay(duration, channel)  # Delay is a specific subclass of Instruction
 """
 import warnings
-
 from abc import ABC
 from collections import defaultdict
 from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple
 
-import numpy as np
-
 from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
 from qiskit.pulse.channels import Channel
 from qiskit.pulse.exceptions import PulseError
 from qiskit.pulse.utils import format_parameter_value
+
+
 # pylint: disable=missing-return-doc
 
 
@@ -50,7 +49,7 @@ class Instruction(ABC):
 
         Args:
             operands: The argument list.
-            duration: Length of time taken by the instruction in terms of dt.
+            duration: Deprecated.
             channels: Tuple of pulse channels that this instruction operates on.
             name: Optional display name for this instruction.
 
@@ -59,20 +58,18 @@ class Instruction(ABC):
             PulseError: If the input ``channels`` are not all of
                 type :class:`Channel`.
         """
-        if not isinstance(duration, (int, np.integer)):
-            raise PulseError("Instruction duration must be an integer, "
-                             "got {} instead.".format(duration))
-        if duration < 0:
-            raise PulseError("{} duration of {} is invalid: must be nonnegative."
-                             "".format(self.__class__.__name__, duration))
-
         for channel in channels:
             if not isinstance(channel, Channel):
                 raise PulseError("Expected a channel, got {} instead.".format(channel))
 
-        self._duration = duration
+        if duration is not None:
+            warnings.warn('Specifying duration in the constructor is deprecated. '
+                          'Now duration is an abstract property rather than class variable. '
+                          'All subclasses should implement ``duration`` accordingly. '
+                          'See Qiskit-Terra #5679 for more information.',
+                          DeprecationWarning)
+
         self._channels = channels
-        self._timeslots = {channel: [(0, self.duration)] for channel in channels}
         self._operands = operands
         self._name = name
         self._hash = None
@@ -106,12 +103,6 @@ class Instruction(ABC):
         """Returns channels that this schedule uses."""
         return self._channels
 
-    @property
-    def timeslots(self) -> Dict[Channel, List[Tuple[int, int]]]:
-        """Occupied time slots by this instruction."""
-        warnings.warn("Access to Instruction timeslots is deprecated.")
-        return self._timeslots
-
     @property
     def start_time(self) -> int:
         """Relative begin time of this instruction."""
@@ -125,7 +116,7 @@ class Instruction(ABC):
     @property
     def duration(self) -> int:
         """Duration of this instruction."""
-        return self._duration
+        raise NotImplementedError
 
     @property
     def _children(self) -> Tuple['Instruction']:
diff --git a/qiskit/pulse/instructions/phase.py b/qiskit/pulse/instructions/phase.py
index 18e06d6ce..9a20e545a 100644
--- a/qiskit/pulse/instructions/phase.py
+++ b/qiskit/pulse/instructions/phase.py
@@ -50,7 +50,7 @@ class ShiftPhase(Instruction):
             channel: The channel this instruction operates on.
             name: Display name for this instruction.
         """
-        super().__init__((phase, channel), 0, (channel,), name=name)
+        super().__init__((phase, channel), None, (channel,), name=name)
 
     @property
     def phase(self) -> Union[complex, ParameterExpression]:
@@ -64,6 +64,11 @@ class ShiftPhase(Instruction):
         """
         return self.operands[1]
 
+    @property
+    def duration(self) -> int:
+        """Duration of this instruction."""
+        return 0
+
 
 class SetPhase(Instruction):
     r"""The set phase instruction sets the phase of the proceeding pulses on that channel
@@ -90,7 +95,7 @@ class SetPhase(Instruction):
             channel: The channel this instruction operates on.
             name: Display name for this instruction.
         """
-        super().__init__((phase, channel), 0, (channel,), name=name)
+        super().__init__((phase, channel), None, (channel,), name=name)
 
     @property
     def phase(self) -> Union[complex, ParameterExpression]:
@@ -103,3 +108,8 @@ class SetPhase(Instruction):
         scheduled on.
         """
         return self.operands[1]
+
+    @property
+    def duration(self) -> int:
+        """Duration of this instruction."""
+        return 0
diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py
index dcf85f079..af3c6d9bd 100644
--- a/qiskit/pulse/instructions/play.py
+++ b/qiskit/pulse/instructions/play.py
@@ -13,7 +13,7 @@
 """An instruction to transmit a given pulse on a ``PulseChannel`` (i.e., those which support
 transmitted pulses, such as ``DriveChannel``).
 """
-from typing import Dict, Optional
+from typing import Dict, Optional, Union
 
 from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
 from qiskit.pulse.channels import PulseChannel
@@ -49,7 +49,7 @@ class Play(Instruction):
             raise PulseError("The `pulse` argument to `Play` must be of type `library.Pulse`.")
         if name is None:
             name = pulse.name
-        super().__init__((pulse, channel), pulse.duration, (channel,), name=name)
+        super().__init__((pulse, channel), None, (channel,), name=name)
 
         if pulse.is_parameterized():
             for value in pulse.parameters.values():
@@ -70,6 +70,11 @@ class Play(Instruction):
         """
         return self.operands[1]
 
+    @property
+    def duration(self) -> Union[int, ParameterExpression]:
+        """Duration of this instruction."""
+        return self.pulse.duration
+
     def assign_parameters(self,
                           value_dict: Dict[ParameterExpression, ParameterValueType]
                           ) -> 'Play':
diff --git a/qiskit/pulse/instructions/snapshot.py b/qiskit/pulse/instructions/snapshot.py
index 1c0536ed2..bae4a9223 100644
--- a/qiskit/pulse/instructions/snapshot.py
+++ b/qiskit/pulse/instructions/snapshot.py
@@ -41,7 +41,7 @@ class Snapshot(Instruction):
         self._channel = SnapshotChannel()
         if name is None:
             name = label
-        super().__init__((label, snapshot_type), 0, (self.channel,), name=name)
+        super().__init__((label, snapshot_type), None, (self.channel,), name=name)
 
     @property
     def label(self) -> str:
@@ -59,3 +59,8 @@ class Snapshot(Instruction):
         scheduled on; trivially, a ``SnapshotChannel``.
         """
         return self._channel
+
+    @property
+    def duration(self) -> int:
+        """Duration of this instruction."""
+        return 0
diff --git a/qiskit/pulse/library/parametric_pulses.py b/qiskit/pulse/library/parametric_pulses.py
index 2b184fa03..352280173 100644
--- a/qiskit/pulse/library/parametric_pulses.py
+++ b/qiskit/pulse/library/parametric_pulses.py
@@ -55,7 +55,9 @@ class ParametricPulse(Pulse):
     """The abstract superclass for parametric pulses."""
 
     @abstractmethod
-    def __init__(self, duration: int, name: Optional[str] = None):
+    def __init__(self,
+                 duration: Union[int, ParameterExpression],
+                 name: Optional[str] = None):
         """Create a parametric pulse and validate the input parameters.
 
         Args:
@@ -138,7 +140,7 @@ class Gaussian(ParametricPulse):
     """
 
     def __init__(self,
-                 duration: int,
+                 duration: Union[int, ParameterExpression],
                  amp: Union[complex, ParameterExpression],
                  sigma: Union[float, ParameterExpression],
                  name: Optional[str] = None):
@@ -209,7 +211,7 @@ class GaussianSquare(ParametricPulse):
     """
 
     def __init__(self,
-                 duration: int,
+                 duration: Union[int, ParameterExpression],
                  amp: Union[complex, ParameterExpression],
                  sigma: Union[float, ParameterExpression],
                  width: Union[float, ParameterExpression],
@@ -306,7 +308,7 @@ class Drag(ParametricPulse):
     """
 
     def __init__(self,
-                 duration: int,
+                 duration: Union[int, ParameterExpression],
                  amp: Union[complex, ParameterExpression],
                  sigma: Union[float, ParameterExpression],
                  beta: Union[float, ParameterExpression],
@@ -399,7 +401,7 @@ class Constant(ParametricPulse):
     """
 
     def __init__(self,
-                 duration: int,
+                 duration: Union[int, ParameterExpression],
                  amp: Union[complex, ParameterExpression],
                  name: Optional[str] = None):
         """
diff --git a/qiskit/pulse/library/pulse.py b/qiskit/pulse/library/pulse.py
index 483c544d2..7f6288f72 100644
--- a/qiskit/pulse/library/pulse.py
+++ b/qiskit/pulse/library/pulse.py
@@ -14,14 +14,10 @@
 to the device.
 """
 import warnings
-
-from typing import Dict, Optional, Any, Tuple
 from abc import ABC, abstractmethod
-
-import numpy as np
+from typing import Dict, Optional, Any, Tuple, Union
 
 from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType
-from qiskit.pulse.exceptions import PulseError
 
 
 class Pulse(ABC):
@@ -30,10 +26,10 @@ class Pulse(ABC):
     """
 
     @abstractmethod
-    def __init__(self, duration: int, name: Optional[str] = None):
-        if not isinstance(duration, (int, np.integer)):
-            raise PulseError('Pulse duration should be integer.')
-        self.duration = int(duration)
+    def __init__(self,
+                 duration: Union[int, ParameterExpression],
+                 name: Optional[str] = None):
+        self.duration = duration
         self.name = name
 
     @property
diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py
index 0ecb956ae..3c2340983 100644
--- a/qiskit/pulse/schedule.py
+++ b/qiskit/pulse/schedule.py
@@ -30,6 +30,7 @@ from qiskit.pulse.channels import Channel
 from qiskit.pulse.exceptions import PulseError
 # pylint: disable=cyclic-import, unused-import
 from qiskit.pulse.instructions import Instruction
+from qiskit.pulse.utils import instruction_duration_validation
 from qiskit.utils.multiprocessing import is_main_process
 
 # pylint: disable=missing-return-doc
@@ -37,6 +38,9 @@ from qiskit.utils.multiprocessing import is_main_process
 Interval = Tuple[int, int]
 """An interval type is a tuple of a start time (inclusive) and an end time (exclusive)."""
 
+TimeSlots = Dict[Channel, List[Tuple[int, int]]]
+"""List of timeslots occupied by instructions for each channel."""
+
 
 class Schedule(abc.ABC):
     """A quantum program *schedule* with exact time constraints for its instructions, operating
@@ -95,7 +99,7 @@ class Schedule(abc.ABC):
         return self._name
 
     @property
-    def timeslots(self) -> Dict[Channel, List[Interval]]:
+    def timeslots(self) -> TimeSlots:
         """Time keeping attribute."""
         return self._timeslots
 
@@ -520,24 +524,24 @@ class Schedule(abc.ABC):
         if not isinstance(time, int):
             raise PulseError("Schedule start time must be an integer.")
 
+        other_timeslots = _get_timeslots(schedule)
         self._duration = max(self._duration, time + schedule.duration)
 
         for channel in schedule.channels:
-
             if channel not in self._timeslots:
                 if time == 0:
-                    self._timeslots[channel] = copy.copy(schedule._timeslots[channel])
+                    self._timeslots[channel] = copy.copy(other_timeslots[channel])
                 else:
                     self._timeslots[channel] = [(i[0] + time, i[1] + time)
-                                                for i in schedule._timeslots[channel]]
+                                                for i in other_timeslots[channel]]
                 continue
 
-            for idx, interval in enumerate(schedule._timeslots[channel]):
+            for idx, interval in enumerate(other_timeslots[channel]):
                 if interval[0] + time >= self._timeslots[channel][-1][1]:
                     # Can append the remaining intervals
                     self._timeslots[channel].extend(
                         [(i[0] + time, i[1] + time)
-                         for i in schedule._timeslots[channel][idx:]])
+                         for i in other_timeslots[channel][idx:]])
                     break
 
                 try:
@@ -576,7 +580,9 @@ class Schedule(abc.ABC):
                     'The channel {} is not present in the schedule'.format(channel))
 
             channel_timeslots = self._timeslots[channel]
-            for interval in schedule._timeslots[channel]:
+            other_timeslots = _get_timeslots(schedule)
+
+            for interval in other_timeslots[channel]:
                 if channel_timeslots:
                     interval = (interval[0] + time, interval[1] + time)
                     index = _interval_index(channel_timeslots, interval)
@@ -1134,7 +1140,7 @@ def _overlaps(first: Interval, second: Interval) -> bool:
     return second[0] < first[1]
 
 
-def _check_nonnegative_timeslot(timeslots):
+def _check_nonnegative_timeslot(timeslots: TimeSlots):
     """Test that a channel has no negative timeslots.
 
     Raises:
@@ -1146,3 +1152,24 @@ def _check_nonnegative_timeslot(timeslots):
                 raise PulseError(
                     "An instruction on {} has a negative "
                     " starting time.".format(chan))
+
+
+def _get_timeslots(schedule: Union[Instruction, Schedule]) -> TimeSlots:
+    """Generate timeslots from given schedule component.
+
+    Args:
+        schedule: Input schedule component.
+
+    Raises:
+        PulseError: When invalid schedule type is specified.
+    """
+    if isinstance(schedule, Instruction):
+        duration = schedule.duration
+        instruction_duration_validation(duration)
+        timeslots = {channel: [(0, duration)] for channel in schedule.channels}
+    elif isinstance(schedule, Schedule):
+        timeslots = schedule.timeslots
+    else:
+        raise PulseError('Invalid schedule type {} is specified.'.format(type(schedule)))
+
+    return timeslots
diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py
index ff3d1335f..a4b3a57fc 100644
--- a/qiskit/pulse/utils.py
+++ b/qiskit/pulse/utils.py
@@ -17,6 +17,7 @@ from typing import List, Dict, Union
 import numpy as np
 
 from qiskit.circuit.parameterexpression import ParameterExpression
+from qiskit.pulse.exceptions import UnassignedDurationError, QiskitError
 
 
 def format_meas_map(meas_map: List[List[int]]) -> Dict[int, List[int]]:
@@ -71,3 +72,26 @@ def format_parameter_value(operand: Union[ParameterExpression]
         pass
 
     return operand
+
+
+def instruction_duration_validation(duration: int):
+    """Validate instruction duration.
+
+    Args:
+        duration: Instruction duration value to validate.
+
+    Raises:
+        UnassignedDurationError: When duration is unassigned.
+        QiskitError: When invalid duration is assigned.
+    """
+    if isinstance(duration, ParameterExpression):
+        raise UnassignedDurationError(
+            'Instruction duration {} is not assigned. '
+            'Please bind all durations to an integer value before playing in the Schedule, '
+            'or use ScheduleBlock to align instructions with unassigned duration.'
+            ''.format(repr(duration)))
+
+    if not isinstance(duration, (int, np.integer)) or duration < 0:
+        raise QiskitError(
+            'Instruction duration must be a non-negative integer, '
+            'got {} instead.'.format(duration))
diff --git a/releasenotes/notes/allow-parametric-instruction-duration-b4c6677544ed28c1.yaml b/releasenotes/notes/allow-parametric-instruction-duration-b4c6677544ed28c1.yaml
new file mode 100644
index 000000000..b6047bceb
--- /dev/null
+++ b/releasenotes/notes/allow-parametric-instruction-duration-b4c6677544ed28c1.yaml
@@ -0,0 +1,14 @@
+---
+upgrade:
+  - |
+    Pulse `Instruction` object and `ParametricPulse` object now support parameter objects
+    as `duration`.
+
+    .. code-block:: python
+
+      dur = Parameter('x_pulse_duration')
+      rx_pulse = Gaussian(dur, 0.1, dur/4)
+
+    Note that we cannot add this instruction to `Schedule` because of the evaluation of
+    instruction overlap constraints. We will implement new program representation
+    that allows lazy instruction scheduling with parameterized durations.


Test patch:
diff --git a/test/python/pulse/test_parameters.py b/test/python/pulse/test_parameters.py
index 636375b21..980ad342a 100644
--- a/test/python/pulse/test_parameters.py
+++ b/test/python/pulse/test_parameters.py
@@ -275,3 +275,67 @@ class TestPulseParameters(QiskitTestCase):
         schedule += pulse.Play(waveform2, DriveChannel(1))
 
         self.assertEqual(len(schedule.get_parameters('amp')), 2)
+
+
+class TestParameterDuration(QiskitTestCase):
+    """Tests parametrization of instruction duration."""
+
+    def test_pulse_duration(self):
+        """Test parametrization of pulse duration."""
+        dur = Parameter('dur')
+
+        test_pulse = pulse.Gaussian(dur, 0.1, dur/4)
+        ref_pulse = pulse.Gaussian(160, 0.1, 40)
+
+        self.assertEqual(test_pulse.assign_parameters({dur: 160}), ref_pulse)
+
+    def test_play_duration(self):
+        """Test parametrization of play instruction duration."""
+        dur = Parameter('dur')
+        ch = pulse.DriveChannel(0)
+
+        test_play = pulse.Play(pulse.Gaussian(dur, 0.1, dur/4), ch)
+        test_play.assign_parameters({dur: 160})
+
+        self.assertEqual(test_play.duration, 160)
+
+    def test_delay_duration(self):
+        """Test parametrization of delay duration."""
+        dur = Parameter('dur')
+        ch = pulse.DriveChannel(0)
+
+        test_delay = pulse.Delay(dur, ch)
+        test_delay.assign_parameters({dur: 300})
+
+        self.assertEqual(test_delay.duration, 300)
+
+    def test_acquire_duration(self):
+        """Test parametrization of acquire duration."""
+        dur = Parameter('dur')
+        ch = pulse.AcquireChannel(0)
+        mem_slot = pulse.MemorySlot(0)
+
+        test_acquire = pulse.Acquire(dur, ch, mem_slot=mem_slot)
+        test_acquire.assign_parameters({dur: 300})
+
+        self.assertEqual(test_acquire.duration, 300)
+
+    def test_is_parameterized(self):
+        """Test is parameterized method for parameter duration."""
+        dur = Parameter('dur')
+        ch = pulse.DriveChannel(0)
+
+        test_play = pulse.Play(pulse.Gaussian(dur, 0.1, dur/4), ch)
+
+        self.assertEqual(test_play.is_parameterized(), True)
+
+    def test_cannot_build_schedule(self):
+        """Test we cannot build schedule with parameterized instructions"""
+        dur = Parameter('dur')
+        ch = pulse.DriveChannel(0)
+
+        test_play = pulse.Play(pulse.Gaussian(dur, 0.1, dur/4), ch)
+
+        sched = pulse.Schedule()
+        with self.assertRaises(pulse.exceptions.UnassignedDurationError):
+            sched.insert(0, test_play)