diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 34b7bfd0309a..684bb0824b45 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -102,6 +102,7 @@ :toctree: ../stubs/ Schedule + ScheduleBlock Instruction @@ -117,7 +118,7 @@ Schedule Transforms =================== -These functions take :class:`Schedule` s as input and return modified +Schedule transforms take :class:`Schedule` s as input and return modified :class:`Schedule` s. .. autosummary:: @@ -127,7 +128,6 @@ transforms.add_implicit_acquires transforms.pad - Exceptions ========== @@ -469,4 +469,4 @@ Waveform, ) from qiskit.pulse.library.samplers.decorators import functional_pulse -from qiskit.pulse.schedule import Schedule +from qiskit.pulse.schedule import Schedule, ScheduleBlock diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index dc211ba28891..88423cdea245 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -1899,9 +1899,6 @@ def measure(qubit: int): sched.draw() - - - Args: func: The Python function to enable as a builder macro. There are no requirements on the signature of the function, any calls to pulse diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py index 8df1a922ab94..fb5971fb7a25 100644 --- a/qiskit/pulse/channels.py +++ b/qiskit/pulse/channels.py @@ -21,13 +21,14 @@ assembler. """ from abc import ABCMeta -from typing import Any, Set +from typing import Any, Set, Union import numpy as np from qiskit.circuit import Parameter from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.exceptions import PulseError +from qiskit.pulse.utils import deprecated_functionality class Channel(metaclass=ABCMeta): @@ -68,13 +69,14 @@ def __init__(self, index: int): """ self._validate_index(index) self._index = index - self._hash = None + self._hash = hash((self.__class__.__name__, self._index)) + self._parameters = set() if isinstance(index, ParameterExpression): self._parameters = index.parameters @property - def index(self) -> int: + def index(self) -> Union[int, ParameterExpression]: """Return the index of this channel. The index is a label for a control signal line typically mapped trivially to a qubit index. For instance, ``DriveChannel(0)`` labels the signal line driving the qubit labeled with index 0. @@ -100,14 +102,16 @@ def _validate_index(self, index: Any) -> None: raise PulseError('Channel index must be a nonnegative integer') @property + @deprecated_functionality def parameters(self) -> Set: """Parameters which determine the channel index.""" return self._parameters def is_parameterized(self) -> bool: """Return True iff the channel is parameterized.""" - return bool(self.parameters) + return isinstance(self.index, ParameterExpression) + @deprecated_functionality def assign(self, parameter: Parameter, value: ParameterValueType) -> 'Channel': """Return a new channel with the input Parameter assigned to value. @@ -153,8 +157,6 @@ def __eq__(self, other: 'Channel') -> bool: return type(self) is type(other) and self._index == other._index def __hash__(self): - if self._hash is None: - self._hash = hash((type(self), self._index)) return self._hash diff --git a/qiskit/pulse/filters.py b/qiskit/pulse/filters.py new file mode 100644 index 000000000000..ff5174492fe0 --- /dev/null +++ b/qiskit/pulse/filters.py @@ -0,0 +1,185 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""A collection of functions that filter instructions in a pulse program.""" + +import abc +from typing import Callable, List, Union, Iterable, Optional, Tuple, Any + +import numpy as np + +from qiskit.pulse import Schedule +from qiskit.pulse.channels import Channel +from qiskit.pulse.schedule import Interval + + +def filter_instructions(sched: Schedule, + filters: List[Callable], + negate: bool = False, + recurse_subroutines: bool = True) -> Schedule: + """A filtering function that takes a schedule and returns a schedule consisting of + filtered instructions. + + Args: + sched: A pulse schedule to be filtered. + filters: List of callback functions that take an instruction and return boolean. + negate: Set `True` to accept an instruction if a filter function returns `False`. + Otherwise the instruction is accepted when the filter function returns `False`. + recurse_subroutines: Set `True` to individually filter instructions inside of a subroutine + defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. + + Returns: + Filtered pulse schedule. + """ + from qiskit.pulse.transforms import flatten, inline_subroutines + + target_sched = flatten(sched) + if recurse_subroutines: + target_sched = inline_subroutines(target_sched) + + time_inst_tuples = np.array(target_sched.instructions) + + valid_insts = np.ones(len(time_inst_tuples), dtype=bool) + for filt in filters: + valid_insts = np.logical_and(valid_insts, np.array(list(map(filt, time_inst_tuples)))) + + if negate and len(filters) > 0: + valid_insts = ~valid_insts + + return Schedule(*time_inst_tuples[valid_insts], name=sched.name, metadata=sched.metadata) + + +def composite_filter(channels: Optional[Union[Iterable[Channel], Channel]] = None, + instruction_types: Optional[Union[Iterable[abc.ABCMeta], abc.ABCMeta]] = None, + time_ranges: Optional[Iterable[Tuple[int, int]]] = None, + intervals: Optional[Iterable[Interval]] = None) -> List[Callable]: + """A helper function to generate a list of filter functions based on + typical elements to be filtered. + + Args: + channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. + instruction_types (Optional[Iterable[Type[qiskit.pulse.Instruction]]]): For example, + ``[PulseInstruction, AcquireInstruction]``. + time_ranges: For example, ``[(0, 5), (6, 10)]``. + intervals: For example, ``[(0, 5), (6, 10)]``. + + Returns: + List of filtering functions. + """ + filters = [] + + # An empty list is also valid input for filter generators. + # See unittest `test.python.pulse.test_schedule.TestScheduleFilter.test_empty_filters`. + if channels is not None: + filters.append(with_channels(channels)) + if instruction_types is not None: + filters.append(with_instruction_types(instruction_types)) + if time_ranges is not None: + filters.append(with_intervals(time_ranges)) + if intervals is not None: + filters.append(with_intervals(intervals)) + + return filters + + +def with_channels(channels: Union[Iterable[Channel], Channel]) -> Callable: + """Channel filter generator. + + Args: + channels: List of channels to filter. + + Returns: + A callback function to filter channels. + """ + channels = _if_scalar_cast_to_list(channels) + + def channel_filter(time_inst) -> bool: + """Filter channel. + + Args: + time_inst (Tuple[int, Instruction]): Time + + Returns: + If instruction matches with condition. + """ + return any(chan in channels for chan in time_inst[1].channels) + return channel_filter + + +def with_instruction_types(types: Union[Iterable[abc.ABCMeta], abc.ABCMeta]) -> Callable: + """Instruction type filter generator. + + Args: + types: List of instruction types to filter. + + Returns: + A callback function to filter instructions. + """ + types = _if_scalar_cast_to_list(types) + + def instruction_filter(time_inst) -> bool: + """Filter instruction. + + Args: + time_inst (Tuple[int, Instruction]): Time + + Returns: + If instruction matches with condition. + """ + return isinstance(time_inst[1], tuple(types)) + + return instruction_filter + + +def with_intervals(ranges: Union[Iterable[Interval], Interval]) -> Callable: + """Interval filter generator. + + Args: + ranges: List of intervals ``[t0, t1]`` to filter. + + Returns: + A callback function to filter intervals. + """ + ranges = _if_scalar_cast_to_list(ranges) + + def interval_filter(time_inst) -> bool: + """Filter interval. + Args: + time_inst (Tuple[int, Instruction]): Time + + Returns: + If instruction matches with condition. + """ + for t0, t1 in ranges: + inst_start = time_inst[0] + inst_stop = inst_start + time_inst[1].duration + if t0 <= inst_start and inst_stop <= t1: + return True + return False + + return interval_filter + + +def _if_scalar_cast_to_list(to_list: Any) -> List[Any]: + """A helper function to create python list of input arguments. + + Args: + to_list: Arbitrary object can be converted into a python list. + + Returns: + Python list of input object. + """ + try: + iter(to_list) + except TypeError: + to_list = [to_list] + return to_list diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py index fc29b58b579a..f89658c70cd5 100644 --- a/qiskit/pulse/instructions/acquire.py +++ b/qiskit/pulse/instructions/acquire.py @@ -13,7 +13,7 @@ """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, Union +from typing import Optional, Union, Tuple from qiskit.circuit import ParameterExpression from qiskit.pulse.channels import MemorySlot, RegisterSlot, AcquireChannel @@ -74,8 +74,7 @@ def __init__(self, self._kernel = kernel self._discriminator = discriminator - 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) + super().__init__(operands=(duration, channel, mem_slot, reg_slot), name=name) @property def channel(self) -> AcquireChannel: @@ -84,6 +83,11 @@ def channel(self) -> AcquireChannel: """ return self.operands[1] + @property + def channels(self) -> Tuple[Union[AcquireChannel, MemorySlot, RegisterSlot]]: + """Returns the channels that this schedule uses.""" + return tuple(self.operands[ind] for ind in (1, 2, 3) if self.operands[ind] is not None) + @property def duration(self) -> Union[int, ParameterExpression]: """Duration of this instruction.""" @@ -118,6 +122,10 @@ def reg_slot(self) -> RegisterSlot: """ return self.operands[3] + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return isinstance(self.duration, ParameterExpression) or super().is_parameterized() + def __repr__(self) -> str: return "{}({}{}{}{}{}{})".format( self.__class__.__name__, diff --git a/qiskit/pulse/instructions/call.py b/qiskit/pulse/instructions/call.py index 1bfd4b7fac14..b1f3749b4d52 100644 --- a/qiskit/pulse/instructions/call.py +++ b/qiskit/pulse/instructions/call.py @@ -15,10 +15,10 @@ from typing import Optional, Union, Dict, Tuple, Any, Set from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType +from qiskit.pulse.channels import Channel +from qiskit.pulse.exceptions import PulseError from qiskit.pulse.instructions import instruction -from qiskit.pulse.utils import format_parameter_value - -# TODO This instruction should support ScheduleBlock when it's ready. +from qiskit.pulse.utils import format_parameter_value, deprecated_functionality class Call(instruction.Instruction): @@ -38,35 +38,81 @@ def __init__(self, subroutine, .. note:: Inline subroutine is mutable. This requires special care for modification. Args: - subroutine (Schedule): A program subroutine to be referred to. + subroutine (Union[Schedule, ScheduleBlock]): A program subroutine to be referred to. value_dict: Mapping of parameter object to assigned value. name: Unique ID of this subroutine. If not provided, this is generated based on the subroutine name. + + Raises: + PulseError: If subroutine is not valid data format. """ - if name is None: - name = f"{self.prefix}_{subroutine.name}" + from qiskit.pulse.schedule import ScheduleBlock, Schedule + + if not isinstance(subroutine, (ScheduleBlock, Schedule)): + raise PulseError(f'Subroutine type {subroutine.__class__.__name__} cannot be called.') + + value_dict = value_dict or dict() - super().__init__((subroutine,), None, - channels=tuple(subroutine.channels), - name=name) - if value_dict: - self.assign_parameters(value_dict) + # initialize parameter template + # TODO remove self._parameter_table + self._arguments = dict() + if subroutine.is_parameterized(): + for param in subroutine.parameters: + self._arguments[param] = value_dict.get(param, param) + + # create cache data of parameter-assigned subroutine + assigned_subroutine = subroutine.assign_parameters( + value_dict=self.arguments, + inplace=False + ) + self._assigned_cache = tuple((self._get_arg_hash(), assigned_subroutine)) + + super().__init__(operands=(subroutine, ), name=name or f"{self.prefix}_{subroutine.name}") @property def duration(self) -> Union[int, ParameterExpression]: """Duration of this instruction.""" return self.subroutine.duration + @property + def channels(self) -> Tuple[Channel]: + """Returns the channels that this schedule uses.""" + return self.assigned_subroutine().channels + # pylint: disable=missing-return-type-doc @property def subroutine(self): """Return attached subroutine. Returns: - schedule (Schedule): Attached schedule. + program (Union[Schedule, ScheduleBlock]): The program referenced by the call. """ return self.operands[0] + def assigned_subroutine(self): + """Returns this subroutine with the parameters assigned. + + .. note:: This function may be often called internally for class equality check + despite its overhead of parameter assignment. + The subroutine with parameter assigned is cached based on ``.argument`` hash. + Once this argument is updated, new assigned instance will be returned. + Note that this update is not mutable operation. + + Returns: + program (Union[Schedule, ScheduleBlock]): Attached program. + """ + if self._get_arg_hash() != self._assigned_cache[0]: + subroutine = self.subroutine.assign_parameters( + value_dict=self.arguments, + inplace=False + ) + # update cache data + self._assigned_cache = tuple((self._get_arg_hash(), subroutine)) + else: + subroutine = self._assigned_cache[1] + + return subroutine + def _initialize_parameter_table(self, operands: Tuple[Any]): """A helper method to initialize parameter table. @@ -89,6 +135,7 @@ def _initialize_parameter_table(self, for value in operands[0].parameters: self._parameter_table[value] = value + @deprecated_functionality def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType] ) -> 'Call': @@ -123,9 +170,9 @@ def is_parameterized(self) -> bool: @property def parameters(self) -> Set: - """Parameters which determine the instruction behavior.""" + """Unassigned parameters which determine the instruction behavior.""" params = set() - for value in self._parameter_table.values(): + for value in self._arguments.values(): if isinstance(value, ParameterExpression): for param in value.parameters: params.add(param) @@ -133,5 +180,43 @@ def parameters(self) -> Set: @property def arguments(self) -> Dict[ParameterExpression, ParameterValueType]: - """Parameters dictionary which determine the subroutine behavior.""" - return self._parameter_table + """Parameters dictionary to be assigned to subroutine.""" + return self._arguments + + @arguments.setter + def arguments(self, new_arguments: Dict[ParameterExpression, ParameterValueType]): + """Set new arguments. + + Args: + new_arguments: Dictionary of new parameter value mapping to update. + + Raises: + PulseError: When new arguments doesn't match with existing arguments. + """ + # validation + if new_arguments.keys() != self._arguments.keys(): + new_arg_names = ', '.join(map(repr, new_arguments.keys())) + old_arg_names = ', '.join(map(repr, self.arguments.keys())) + raise PulseError('Key mismatch between new arguments and existing arguments. ' + f'{new_arg_names} != {old_arg_names}') + + self._arguments = new_arguments + + def _get_arg_hash(self): + """A helper function to generate hash of parameters.""" + return hash(tuple(self.arguments.items())) + + def __eq__(self, other: 'Instruction') -> bool: + """Check if this instruction is equal to the `other` instruction. + + Instructions are equal if they share the same type, operands, and channels. + """ + # type check + if not isinstance(other, self.__class__): + return False + + # compare subroutine. assign parameter values before comparison + if self.assigned_subroutine() != other.assigned_subroutine(): + return False + + return True diff --git a/qiskit/pulse/instructions/delay.py b/qiskit/pulse/instructions/delay.py index 61e2a623ff1d..8340e8e017d3 100644 --- a/qiskit/pulse/instructions/delay.py +++ b/qiskit/pulse/instructions/delay.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """An instruction for blocking time on a channel; useful for scheduling alignment.""" -from typing import Optional, Union +from typing import Optional, Union, Tuple from qiskit.circuit import ParameterExpression from qiskit.pulse.channels import Channel @@ -46,7 +46,7 @@ def __init__(self, duration: Union[int, ParameterExpression], channel: The channel that will have the delay. name: Name of the delay for display purposes. """ - super().__init__((duration, channel), None, (channel,), name=name) + super().__init__(operands=(duration, channel), name=name) @property def channel(self) -> Channel: @@ -55,7 +55,16 @@ def channel(self) -> Channel: """ return self.operands[1] + @property + def channels(self) -> Tuple[Channel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> Union[int, ParameterExpression]: """Duration of this instruction.""" return self.operands[0] + + def is_parameterized(self) -> bool: + """Return ``True`` iff the instruction is parameterized.""" + return isinstance(self.duration, ParameterExpression) or super().is_parameterized() diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index 52bdd89fe4e8..78c11f910115 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -13,7 +13,7 @@ """Directives are hints to the pulse compiler for how to process its input programs.""" from abc import ABC -from typing import Optional +from typing import Optional, Tuple from qiskit.pulse import channels as chans from qiskit.pulse.instructions import instruction @@ -47,7 +47,12 @@ def __init__(self, channels: The channel that the barrier applies to. name: Name of the directive for display purposes. """ - super().__init__(tuple(channels), None, tuple(channels), name=name) + super().__init__(operands=tuple(channels), name=name) + + @property + def channels(self) -> Tuple[chans.Channel]: + """Returns the channels that this schedule uses.""" + return self.operands def __eq__(self, other): """Verify two barriers are equivalent.""" diff --git a/qiskit/pulse/instructions/frequency.py b/qiskit/pulse/instructions/frequency.py index 77730b9751b4..0a0146dd8873 100644 --- a/qiskit/pulse/instructions/frequency.py +++ b/qiskit/pulse/instructions/frequency.py @@ -13,7 +13,7 @@ """Frequency instructions module. These instructions allow the user to manipulate the frequency of a channel. """ -from typing import Optional, Union +from typing import Optional, Union, Tuple from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.pulse.channels import PulseChannel @@ -46,7 +46,7 @@ def __init__(self, frequency: Union[float, ParameterExpression], """ if not isinstance(frequency, ParameterExpression): frequency = float(frequency) - super().__init__((frequency, channel), None, (channel,), name=name) + super().__init__(operands=(frequency, channel), name=name) @property def frequency(self) -> Union[float, ParameterExpression]: @@ -60,11 +60,20 @@ def channel(self) -> PulseChannel: """ return self.operands[1] + @property + def channels(self) -> Tuple[PulseChannel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> int: """Duration of this instruction.""" return 0 + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return isinstance(self.frequency, ParameterExpression) or super().is_parameterized() + class ShiftFrequency(Instruction): """Shift the channel frequency away from the current frequency.""" @@ -82,7 +91,7 @@ def __init__(self, """ if not isinstance(frequency, ParameterExpression): frequency = float(frequency) - super().__init__((frequency, channel), None, (channel,), name=name) + super().__init__(operands=(frequency, channel), name=name) @property def frequency(self) -> Union[float, ParameterExpression]: @@ -96,7 +105,16 @@ def channel(self) -> PulseChannel: """ return self.operands[1] + @property + def channels(self) -> Tuple[PulseChannel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> int: """Duration of this instruction.""" return 0 + + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return isinstance(self.frequency, ParameterExpression) or super().is_parameterized() diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py index 3375e384f5e5..4b916c6803af 100644 --- a/qiskit/pulse/instructions/instruction.py +++ b/qiskit/pulse/instructions/instruction.py @@ -22,14 +22,14 @@ sched += Delay(duration, channel) # Delay is a specific subclass of Instruction """ import warnings -from abc import ABC +from abc import ABC, abstractproperty from collections import defaultdict from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Any 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 +from qiskit.pulse.utils import format_parameter_value, deprecated_functionality # pylint: disable=missing-return-doc @@ -42,15 +42,15 @@ class Instruction(ABC): def __init__(self, operands: Tuple, - duration: int, - channels: Tuple[Channel], + duration: int = None, + channels: Tuple[Channel] = None, name: Optional[str] = None): """Instruction initializer. Args: operands: The argument list. duration: Deprecated. - channels: Tuple of pulse channels that this instruction operates on. + channels: Deprecated. name: Optional display name for this instruction. Raises: @@ -58,10 +58,6 @@ def __init__(self, PulseError: If the input ``channels`` are not all of type :class:`Channel`. """ - for channel in channels: - if not isinstance(channel, Channel): - raise PulseError("Expected a channel, got {} instead.".format(channel)) - if duration is not None: warnings.warn('Specifying duration in the constructor is deprecated. ' 'Now duration is an abstract property rather than class variable. ' @@ -69,7 +65,11 @@ def __init__(self, 'See Qiskit-Terra #5679 for more information.', DeprecationWarning) - self._channels = channels + if channels is not None: + warnings.warn('Specifying ``channels`` in the constructor is deprecated. ' + 'All channels should be stored in ``operands``.', + DeprecationWarning) + self._operands = operands self._name = name self._hash = None @@ -77,6 +77,10 @@ def __init__(self, self._parameter_table = defaultdict(list) self._initialize_parameter_table(operands) + for channel in self.channels: + if not isinstance(channel, Channel): + raise PulseError("Expected a channel, got {} instead.".format(channel)) + @property def name(self) -> str: """Name of this instruction.""" @@ -92,10 +96,10 @@ def operands(self) -> Tuple: """Return instruction operands.""" return self._operands - @property + @abstractproperty def channels(self) -> Tuple[Channel]: - """Returns channels that this schedule uses.""" - return self._channels + """Returns the channels that this schedule uses.""" + raise NotImplementedError @property def start_time(self) -> int: @@ -226,13 +230,14 @@ def append(self, schedule, return self.insert(time, schedule, name=name) @property + @deprecated_functionality def parameters(self) -> Set: """Parameters which determine the instruction behavior.""" return set(self._parameter_table.keys()) def is_parameterized(self) -> bool: """Return True iff the instruction is parameterized.""" - return bool(self.parameters) + return any(chan.is_parameterized() for chan in self.channels) def _initialize_parameter_table(self, operands: Tuple[Any]): @@ -245,10 +250,11 @@ def _initialize_parameter_table(self, if isinstance(op, ParameterExpression): for param in op.parameters: self._parameter_table[param].append(idx) - elif isinstance(op, Channel) and op.is_parameterized(): - for param in op.parameters: + elif isinstance(op, Channel) and isinstance(op.index, ParameterExpression): + for param in op.index.parameters: self._parameter_table[param].append(idx) + @deprecated_functionality def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType] ) -> 'Instruction': @@ -284,6 +290,7 @@ def assign_parameters(self, self._parameter_table[new_parameter] = entry self._operands = tuple(new_operands) + return self def draw(self, dt: float = 1, style=None, diff --git a/qiskit/pulse/instructions/phase.py b/qiskit/pulse/instructions/phase.py index 9a20e545ad20..ad8482c55452 100644 --- a/qiskit/pulse/instructions/phase.py +++ b/qiskit/pulse/instructions/phase.py @@ -15,7 +15,7 @@ at that moment, and ``ShiftPhase`` instructions which increase the existing phase by a relative amount. """ -from typing import Optional, Union +from typing import Optional, Union, Tuple from qiskit.circuit import ParameterExpression from qiskit.pulse.channels import PulseChannel @@ -50,7 +50,7 @@ def __init__(self, phase: Union[complex, ParameterExpression], channel: The channel this instruction operates on. name: Display name for this instruction. """ - super().__init__((phase, channel), None, (channel,), name=name) + super().__init__(operands=(phase, channel), name=name) @property def phase(self) -> Union[complex, ParameterExpression]: @@ -64,11 +64,20 @@ def channel(self) -> PulseChannel: """ return self.operands[1] + @property + def channels(self) -> Tuple[PulseChannel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> int: """Duration of this instruction.""" return 0 + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return isinstance(self.phase, ParameterExpression) or super().is_parameterized() + class SetPhase(Instruction): r"""The set phase instruction sets the phase of the proceeding pulses on that channel @@ -95,7 +104,7 @@ def __init__(self, channel: The channel this instruction operates on. name: Display name for this instruction. """ - super().__init__((phase, channel), None, (channel,), name=name) + super().__init__(operands=(phase, channel), name=name) @property def phase(self) -> Union[complex, ParameterExpression]: @@ -109,7 +118,16 @@ def channel(self) -> PulseChannel: """ return self.operands[1] + @property + def channels(self) -> Tuple[PulseChannel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> int: """Duration of this instruction.""" return 0 + + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return isinstance(self.phase, ParameterExpression) or super().is_parameterized() diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 6872a9a40b56..4697a90c4082 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -20,6 +20,7 @@ from qiskit.pulse.exceptions import PulseError from qiskit.pulse.library.pulse import Pulse from qiskit.pulse.instructions.instruction import Instruction +from qiskit.pulse.utils import deprecated_functionality class Play(Instruction): @@ -52,14 +53,7 @@ def __init__(self, pulse: Pulse, "`channels.PulseChannel`.") if name is None: name = pulse.name - super().__init__((pulse, channel), None, (channel,), name=name) - - if pulse.is_parameterized(): - for value in pulse.parameters.values(): - if isinstance(value, ParameterExpression): - for param in value.parameters: - # Table maps parameter to operand index, 0 for ``pulse`` - self._parameter_table[param].append(0) + super().__init__(operands=(pulse, channel), name=name) @property def pulse(self) -> Pulse: @@ -73,6 +67,11 @@ def channel(self) -> PulseChannel: """ return self.operands[1] + @property + def channels(self) -> Tuple[PulseChannel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> Union[int, ParameterExpression]: """Duration of this instruction.""" @@ -87,13 +86,14 @@ def _initialize_parameter_table(self, """ super()._initialize_parameter_table(operands) - if self.pulse.is_parameterized(): + if any(isinstance(val, ParameterExpression) for val in self.pulse.parameters.values()): for value in self.pulse.parameters.values(): if isinstance(value, ParameterExpression): for param in value.parameters: # Table maps parameter to operand index, 0 for ``pulse`` self._parameter_table[param].append(0) + @deprecated_functionality def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType] ) -> 'Play': @@ -101,3 +101,7 @@ def assign_parameters(self, pulse = self.pulse.assign_parameters(value_dict) self._operands = (pulse, self.channel) return self + + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return self.pulse.is_parameterized() or super().is_parameterized() diff --git a/qiskit/pulse/instructions/snapshot.py b/qiskit/pulse/instructions/snapshot.py index bae4a922314c..91d0aa349e6d 100644 --- a/qiskit/pulse/instructions/snapshot.py +++ b/qiskit/pulse/instructions/snapshot.py @@ -13,7 +13,7 @@ """A simulator instruction to capture output within a simulation. The types of snapshot instructions available are determined by the simulator being used. """ -from typing import Optional +from typing import Optional, Tuple from qiskit.pulse.channels import SnapshotChannel from qiskit.pulse.exceptions import PulseError @@ -41,7 +41,7 @@ def __init__(self, label: str, snapshot_type: str = 'statevector', name: Optiona self._channel = SnapshotChannel() if name is None: name = label - super().__init__((label, snapshot_type), None, (self.channel,), name=name) + super().__init__(operands=(label, snapshot_type), name=name) @property def label(self) -> str: @@ -60,7 +60,16 @@ def channel(self) -> SnapshotChannel: """ return self._channel + @property + def channels(self) -> Tuple[SnapshotChannel]: + """Returns the channels that this schedule uses.""" + return (self.channel, ) + @property def duration(self) -> int: """Duration of this instruction.""" return 0 + + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return False diff --git a/qiskit/pulse/library/parametric_pulses.py b/qiskit/pulse/library/parametric_pulses.py index 0facbcad7224..ee6997eba391 100644 --- a/qiskit/pulse/library/parametric_pulses.py +++ b/qiskit/pulse/library/parametric_pulses.py @@ -48,7 +48,7 @@ class ParametricPulseShapes(Enum): from qiskit.pulse.library.discrete import gaussian, gaussian_square, drag, constant from qiskit.pulse.library.pulse import Pulse from qiskit.pulse.library.waveform import Waveform -from qiskit.pulse.utils import format_parameter_value +from qiskit.pulse.utils import format_parameter_value, deprecated_functionality class ParametricPulse(Pulse): @@ -85,8 +85,10 @@ def validate_parameters(self) -> None: raise NotImplementedError def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" return any(_is_parameterized(val) for val in self.parameters.values()) + @deprecated_functionality def assign(self, parameter: ParameterExpression, value: ParameterValueType) -> 'ParametricPulse': """Assign one parameter to a value, which can either be numeric or another parameter @@ -94,6 +96,7 @@ def assign(self, parameter: ParameterExpression, """ return self.assign_parameters({parameter: value}) + @deprecated_functionality def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType] ) -> 'ParametricPulse': @@ -115,7 +118,7 @@ def assign_parameters(self, if _is_parameterized(op_value) and parameter in op_value.parameters: op_value = format_parameter_value(op_value.assign(parameter, value)) new_parameters[op] = op_value - return type(self)(**new_parameters) + return type(self)(**new_parameters, name=self.name) def __eq__(self, other: Pulse) -> bool: return super().__eq__(other) and self.parameters == other.parameters diff --git a/qiskit/pulse/library/pulse.py b/qiskit/pulse/library/pulse.py index 7078c9e8d164..cb87addaf28d 100644 --- a/qiskit/pulse/library/pulse.py +++ b/qiskit/pulse/library/pulse.py @@ -18,6 +18,7 @@ from typing import Dict, Optional, Any, Tuple, Union from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType +from qiskit.pulse.utils import deprecated_functionality class Pulse(ABC): @@ -43,11 +44,11 @@ def parameters(self) -> Dict[str, Any]: """Return a dictionary containing the pulse's parameters.""" pass - @abstractmethod def is_parameterized(self) -> bool: """Return True iff the instruction is parameterized.""" raise NotImplementedError + @deprecated_functionality @abstractmethod def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType] diff --git a/qiskit/pulse/library/waveform.py b/qiskit/pulse/library/waveform.py index 471e96891b6b..a53b4ff2f467 100644 --- a/qiskit/pulse/library/waveform.py +++ b/qiskit/pulse/library/waveform.py @@ -18,6 +18,7 @@ from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.exceptions import PulseError from qiskit.pulse.library.pulse import Pulse +from qiskit.pulse.utils import deprecated_functionality class Waveform(Pulse): @@ -95,6 +96,7 @@ def _clip(self, samples: np.ndarray, epsilon: float = 1e-7) -> np.ndarray: return samples def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" return False @property @@ -102,6 +104,7 @@ def parameters(self) -> Dict[str, Any]: """Return a dictionary containing the pulse's parameters.""" return dict() + @deprecated_functionality def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType] ) -> 'Waveform': diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py new file mode 100644 index 000000000000..3983e41e248d --- /dev/null +++ b/qiskit/pulse/parameter_manager.py @@ -0,0 +1,416 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +# pylint: disable=invalid-name + +""""Management of pulse program parameters. + +Background +========== + +In contrast to ``QuantumCircuit``, in pulse programs, parameter objects can be stored in +multiple places at different layers, for example + +- program variables: ``ScheduleBlock.alignment_context._context_params`` + +- instruction operands: ``ShiftPhase.phase``, ... + +- operand parameters: ``pulse.parameters``, ``channel.index`` ... + +This complexity is due to the tight coupling of the program to an underlying device Hamiltonian, +i.e. the variance of physical parameters between qubits and their couplings. +If we want to define a program that can be used with arbitrary qubits, +we should be able to parametrize every control parameter in the program. + +Implementation +============== + +Managing parameters in each object within a program, i.e. the ``ParameterTable`` model, +makes the framework quite complicated. With the ``ParameterManager`` class within this module, +the parameter assignment operation is performed by a visitor instance. + +The visitor pattern is a way of separating data processing from the object on which it operates. +This removes the overhead of parameter management from each piece of the program. +The computational complexity of the parameter assignment operation may be increased +from the parameter table model of ~O(1), however, usually, this calculation occurs +only once before the program is executed. Thus this doesn't hurt user experience during +pulse programming. On the contrary, it removes parameter table object and associated logic +from each object, yielding smaller object creation cost and higher performance +as the data amount scales. + +Note that we don't need to write any parameter management logic for each object, +and thus this parameter framework gives greater scalability to the pulse module. +""" +from copy import deepcopy, copy +from typing import List, Dict, Set, Any, Union + +from qiskit.circuit.parameter import Parameter +from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType +from qiskit.pulse import instructions, channels +from qiskit.pulse.exceptions import PulseError +from qiskit.pulse.library import ParametricPulse, Waveform +from qiskit.pulse.schedule import Schedule, ScheduleBlock +from qiskit.pulse.transforms.alignments import AlignmentKind +from qiskit.pulse.utils import format_parameter_value + + +class NodeVisitor: + """A node visitor base class that walks instruction data in a pulse program and calls + visitor functions for every node. + + Though this class implementation is based on Python AST, each node doesn't have + a dedicated node class due to the lack of an abstract syntax tree for pulse programs in + Qiskit. Instead of parsing pulse programs, this visitor class finds the associated visitor + function based on class name of the instruction node, i.e. ``Play``, ``Call``, etc... + The `.visit` method recursively checks superclass of given node since some parametrized + components such as ``DriveChannel`` may share a common superclass with other subclasses. + In this example, we can just define ``visit_Channel`` method instead of defining + the same visitor function for every subclasses. + + Some instructions may have special logic or data structure to store parameter objects, + and visitor functions for these nodes should be individually defined. + + Because pulse programs can be nested into another pulse program, + the visitor function should be able to recursively call proper visitor functions. + If visitor function is not defined for a given node, ``generic_visit`` + method is called. Usually, this method is provided for operating on object defined + outside of the Qiskit Pulse module. + """ + def visit(self, node: Any): + """Visit a node.""" + visitor = self._get_visitor(type(node)) + return visitor(node) + + def _get_visitor(self, node_class): + """A helper function to recursively investigate superclass visitor method.""" + if node_class == object: + return self.generic_visit + + try: + return getattr(self, f'visit_{node_class.__name__}') + except AttributeError: + # check super class + return self._get_visitor(node_class.__base__) + + def visit_ScheduleBlock(self, node: ScheduleBlock): + """Visit ``ScheduleBlock``. Recursively visit context blocks and overwrite. + + .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. + """ + raise NotImplementedError + + def visit_Schedule(self, node: Schedule): + """Visit ``Schedule``. Recursively visit schedule children and overwrite.""" + raise NotImplementedError + + def generic_visit(self, node: Any): + """Called if no explicit visitor function exists for a node.""" + raise NotImplementedError + + +class ParameterSetter(NodeVisitor): + """Node visitor for parameter binding. + + This visitor is initialized with a dictionary of parameters to be assigned, + and assign values to operands of nodes found. + """ + def __init__(self, param_map: Dict[ParameterExpression, ParameterValueType]): + self._param_map = param_map + + # Top layer: Assign parameters to programs + + def visit_ScheduleBlock(self, node: ScheduleBlock): + """Visit ``ScheduleBlock``. Recursively visit context blocks and overwrite. + + .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. + """ + # accessing to protected member + node._blocks = [self.visit(block) for block in node.instructions] + node._alignment_context = self.visit_AlignmentKind(node.alignment_context) + + self._update_parameter_manager(node) + return node + + def visit_Schedule(self, node: Schedule): + """Visit ``Schedule``. Recursively visit schedule children and overwrite.""" + # accessing to private member + # TODO: consider updating Schedule to handle this more gracefully + node._Schedule__children = [(t0, self.visit(sched)) for t0, sched in node.instructions] + node._renew_timeslots() + + self._update_parameter_manager(node) + return node + + def visit_AlignmentKind(self, node: AlignmentKind): + """Assign parameters to block's ``AlignmentKind`` specification.""" + new_parameters = tuple(self.visit(param) for param in node._context_params) + node._context_params = new_parameters + + return node + + # Mid layer: Assign parameters to instructions + + def visit_Call(self, node: instructions.Call): + """Assign parameters to ``Call`` instruction. + + .. note:: ``Call`` instruction has a special parameter handling logic. + This instruction separately keeps program, i.e. parametrized schedule, + and bound parameters until execution. The parameter assignment operation doesn't + immediately override its operand data. + """ + if node.is_parameterized(): + new_table = copy(node.arguments) + + for parameter, value in new_table.items(): + if isinstance(value, ParameterExpression): + new_table[parameter] = self._assign_parameter_expression(value) + node.arguments = new_table + + return node + + def visit_Instruction(self, node: instructions.Instruction): + """Assign parameters to general pulse instruction. + + .. note:: All parametrized object should be stored in the operands. + Otherwise parameter cannot be detected. + """ + if node.is_parameterized(): + node._operands = tuple(self.visit(op) for op in node.operands) + + return node + + # Lower layer: Assign parameters to operands + + def visit_Channel(self, node: channels.Channel): + """Assign parameters to ``Channel`` object.""" + if node.is_parameterized(): + new_index = self._assign_parameter_expression(node.index) + + # validate + if not isinstance(new_index, ParameterExpression): + if not isinstance(new_index, int) or new_index < 0: + raise PulseError('Channel index must be a nonnegative integer') + + # return new instance to prevent accidentally override timeslots without evaluation + return node.__class__(index=new_index) + + return node + + def visit_ParametricPulse(self, node: ParametricPulse): + """Assign parameters to ``ParametricPulse`` object.""" + if node.is_parameterized(): + new_parameters = {} + for op, op_value in node.parameters.items(): + if isinstance(op_value, ParameterExpression): + op_value = self._assign_parameter_expression(op_value) + new_parameters[op] = op_value + + return node.__class__(**new_parameters, name=node.name) + + return node + + def visit_Waveform(self, node: Waveform): + """Assign parameters to ``Waveform`` object. + + .. node:: No parameter can be assigned to ``Waveform`` object. + """ + return node + + def generic_visit(self, node: Any): + """Assign parameters to object that doesn't belong to Qiskit Pulse module.""" + if isinstance(node, ParameterExpression): + return self._assign_parameter_expression(node) + else: + return node + + def _assign_parameter_expression(self, param_expr: ParameterExpression): + """A helper function to assign parameter value to parameter expression.""" + new_value = copy(param_expr) + for parameter in param_expr.parameters: + if parameter in self._param_map: + new_value = new_value.assign(parameter, self._param_map[parameter]) + + return format_parameter_value(new_value) + + def _update_parameter_manager(self, node: Union[Schedule, ScheduleBlock]): + """A helper function to update parameter manager of pulse program.""" + new_parameters = set() + + for parameter in node.parameters: + if parameter in self._param_map: + value = self._param_map[parameter] + if isinstance(value, ParameterExpression): + for new_parameter in value.parameters: + new_parameters.add(new_parameter) + else: + new_parameters.add(parameter) + + if hasattr(node, '_parameter_manager'): + node._parameter_manager._parameters = new_parameters + else: + raise PulseError(f'Node type {node.__class__.__name__} has no parameter manager.') + + +class ParameterGetter(NodeVisitor): + """Node visitor for parameter finding. + + This visitor initializes empty parameter array, and recursively visits nodes + and add parameters found to the array. + """ + def __init__(self): + self.parameters = set() + + # Top layer: Get parameters from programs + + def visit_ScheduleBlock(self, node: ScheduleBlock): + """Visit ``ScheduleBlock``. Recursively visit context blocks and search parameters. + + .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. + """ + for parameter in node.parameters: + self.parameters.add(parameter) + + def visit_Schedule(self, node: Schedule): + """Visit ``Schedule``. Recursively visit schedule children and search parameters.""" + for parameter in node.parameters: + self.parameters.add(parameter) + + def visit_AlignmentKind(self, node: AlignmentKind): + """Get parameters from block's ``AlignmentKind`` specification.""" + for param in node._context_params: + self.visit(param) + + # Mid layer: Get parameters from instructions + + def visit_Call(self, node: instructions.Call): + """Get parameters from ``Call`` instruction. + + .. note:: ``Call`` instruction has a special parameter handling logic. + This instruction separately keeps parameters and program. + """ + for parameter in node.parameters: + self.parameters.add(parameter) + + def visit_Instruction(self, node: instructions.Instruction): + """Get parameters from general pulse instruction. + + .. note:: All parametrized object should be stored in the operands. + Otherwise parameter cannot be detected. + """ + for op in node.operands: + self.visit(op) + + # Lower layer: Get parameters from operands + + def visit_Channel(self, node: channels.Channel): + """Get parameters from ``Channel`` object.""" + if isinstance(node.index, ParameterExpression): + self._add_parameters(node.index) + + def visit_ParametricPulse(self, node: ParametricPulse): + """Get parameters from ``ParametricPulse`` object.""" + for op_value in node.parameters.values(): + if isinstance(op_value, ParameterExpression): + self._add_parameters(op_value) + + def visit_Waveform(self, node: Waveform): + """Get parameters from ``Waveform`` object. + + .. node:: No parameter can be assigned to ``Waveform`` object. + """ + pass + + def generic_visit(self, node: Any): + """Get parameters from object that doesn't belong to Qiskit Pulse module.""" + if isinstance(node, ParameterExpression): + self._add_parameters(node) + + def _add_parameters(self, param_expr: ParameterExpression): + """A helper function to get parameters from parameter expression.""" + for parameter in param_expr.parameters: + self.parameters.add(parameter) + + +class ParameterManager: + """Helper class to manage parameter objects associated with arbitrary pulse programs. + + This object is implicitly initialized with the parameter object storage + that stores parameter objects added to the parent pulse program. + + Parameter assignment logic is implemented based on the visitor pattern. + Instruction data and its location are not directly associated with this object. + """ + def __init__(self): + """Create new parameter table for pulse programs.""" + self._parameters = set() + + @property + def parameters(self) -> Set: + """Parameters which determine the schedule behavior.""" + return self._parameters + + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return bool(self.parameters) + + def get_parameters(self, parameter_name: str) -> List[Parameter]: + """Get parameter object bound to this schedule by string name. + + Because different ``Parameter`` objects can have the same name, + this method returns a list of ``Parameter`` s for the provided name. + + Args: + parameter_name: Name of parameter. + + Returns: + Parameter objects that have corresponding name. + """ + return [param for param in self.parameters if param.name == parameter_name] + + def assign_parameters(self, + pulse_program: Any, + value_dict: Dict[ParameterExpression, ParameterValueType], + inplace: bool = True + ) -> Any: + """Modify and return program data with parameters assigned according to the input. + + Args: + pulse_program: Arbitrary pulse program associated with this manager instance. + value_dict: A mapping from Parameters to either numeric values or another + Parameter expression. + inplace: Set ``True`` to overwrite existing program data. + + Returns: + Updated program data. + """ + if inplace: + source = pulse_program + else: + source = deepcopy(pulse_program) + + valid_map = {par: val for par, val in value_dict.items() if par in self.parameters} + if valid_map: + visitor = ParameterSetter(param_map=valid_map) + return visitor.visit(source) + return source + + def update_parameter_table(self, new_node: Any): + """A helper function to update parameter table with given data node. + + Args: + new_node: A new data node to be added. + """ + visitor = ParameterGetter() + visitor.visit(new_node) + + for parameter in visitor.parameters: + self._parameters.add(parameter) diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index fcbdea638fa6..36f1cb868a63 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=cyclic-import, missing-return-doc, missing-return-type-doc + """The Schedule is one of the most fundamental objects to this pulse-level programming module. A ``Schedule`` is a representation of a *program* in Pulse. Each schedule tracks the time of each instruction occuring in parallel over multiple signal *channels*. @@ -17,11 +19,11 @@ import abc import copy +import functools import itertools import multiprocessing as mp import sys import warnings -from collections import defaultdict from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional, Any import numpy as np @@ -29,34 +31,81 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.channels import Channel -from qiskit.pulse.exceptions import PulseError -# pylint: disable=cyclic-import, unused-import +from qiskit.pulse.exceptions import PulseError, UnassignedDurationError from qiskit.pulse.instructions import Instruction -from qiskit.pulse.utils import instruction_duration_validation +from qiskit.pulse.utils import instruction_duration_validation, deprecated_functionality from qiskit.utils.multiprocessing import is_main_process -# pylint: disable=missing-return-doc 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]]] +TimeSlots = Dict[Channel, List[Interval]] """List of timeslots occupied by instructions for each channel.""" ScheduleComponent = Union['Schedule', Instruction] """An element that composes a pulse schedule.""" +BlockComponent = Union['ScheduleBlock', Instruction] +"""An element that composes a pulse schedule block.""" + -class Schedule(abc.ABC): +class Schedule: """A quantum program *schedule* with exact time constraints for its instructions, operating over all input signal *channels* and supporting special syntaxes for building. - """ - # Counter for the number of instances in this class. - instances_counter = itertools.count() + Pulse program representation for the original Qiskit Pulse model [1]. + Instructions are not allowed to overlap in time + on the same channel. This overlap constraint is immediately + evaluated when a new instruction is added to the ``Schedule`` object. + + It is necessary to specify the absolute start time and duration + for each instruction so as to deterministically fix its execution time. + + The ``Schedule`` program supports some syntax sugar for easier programming. + + - Appending an instruction to the end of a channel + + .. code-block:: python + + sched = Schedule() + sched += Play(Gaussian(160, 0.1, 40), DriveChannel(0)) + + - Appending an instruction shifted in time by a given amount + + .. code-block:: python + + sched = Schedule() + sched += Play(Gaussian(160, 0.1, 40), DriveChannel(0)) << 30 + + - Merge two schedules + + .. code-block:: python + + sched1 = Schedule() + sched1 += Play(Gaussian(160, 0.1, 40), DriveChannel(0)) + + sched2 = Schedule() + sched2 += Play(Gaussian(160, 0.1, 40), DriveChannel(1)) + sched2 = sched1 | sched2 + + A ``PulseError`` is immediately raised when the overlap constraint is violated. + + In the schedule representation, we cannot parametrize the duration of instructions. + Thus we need to create a new schedule object for each duration. + To parametrize an instruction's duration, the :class:`~qiskit.pulse.ScheduleBlock` + representation may be used instead. + + References: + [1]: https://arxiv.org/abs/2004.06755 + + """ # Prefix to use for auto naming. prefix = 'sched' + # Counter to count instance number. + instances_counter = itertools.count() + def __init__(self, *schedules: Union[ScheduleComponent, Tuple[int, ScheduleComponent]], name: Optional[str] = None, @@ -74,18 +123,25 @@ def __init__(self, Raises: TypeError: if metadata is not a dict. """ + from qiskit.pulse.parameter_manager import ParameterManager + if name is None: name = self.prefix + str(next(self.instances_counter)) if sys.platform != "win32" and not is_main_process(): name += '-{}'.format(mp.current_process().pid) self._name = name + self._parameter_manager = ParameterManager() + + if not isinstance(metadata, dict) and metadata is not None: + raise TypeError("Only a dictionary or None is accepted for schedule metadata") + self._metadata = metadata or dict() + self._duration = 0 # These attributes are populated by ``_mutable_insert`` self._timeslots = {} self.__children = [] - self._parameter_table = defaultdict(list) for sched_pair in schedules: try: time, sched = sched_pair @@ -93,15 +149,32 @@ def __init__(self, # recreate as sequence starting at 0. time, sched = 0, sched_pair self._mutable_insert(time, sched) - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for schedule metadata") - self._metadata = metadata @property def name(self) -> str: """Name of this Schedule""" return self._name + @property + def metadata(self) -> Dict[str, Any]: + """The user provided metadata associated with the schedule. + + User provided ``dict`` of metadata for the schedule. + The metadata contents do not affect the semantics of the program + but are used to influence the execution of the schedule. It is expected + to be passed between all transforms of the schedule and that providers + will associate any schedule metadata with the results it returns from the + execution of that schedule. + """ + return self._metadata + + @metadata.setter + def metadata(self, metadata): + """Update the schedule metadata""" + if not isinstance(metadata, dict) and metadata is not None: + raise TypeError("Only a dictionary or None is accepted for schedule metadata") + self._metadata = metadata or dict() + @property def timeslots(self) -> TimeSlots: """Time keeping attribute.""" @@ -140,13 +213,8 @@ def _children(self) -> Tuple[Tuple[int, ScheduleComponent], ...]: return tuple(self.__children) @property - def instructions(self): - """Get the time-ordered instructions from self. - - ReturnType: - Tuple[Tuple[int, Instruction], ...] - """ - + def instructions(self) -> Tuple[Tuple[int, Instruction]]: + """Get the time-ordered instructions from self.""" def key(time_inst_pair): inst = time_inst_pair[1] return (time_inst_pair[0], inst.duration, @@ -155,26 +223,11 @@ def key(time_inst_pair): return tuple(sorted(self._instructions(), key=key)) @property - def metadata(self): - """The user provided metadata associated with the schedule - - The metadata for the schedule is a user provided ``dict`` of metadata - for the schedule. It will not be used to influence the execution or - operation of the schedule, but it is expected to be passed between - all transforms of the schedule and that providers will associate any - schedule metadata with the results it returns from execution of that - schedule. - """ - return self._metadata - - @metadata.setter - def metadata(self, metadata): - """Update the schedule metadata""" - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for schedule metadata") - self._metadata = metadata + def parameters(self) -> Set: + """Parameters which determine the schedule behavior.""" + return self._parameter_manager.parameters - def ch_duration(self, *channels: List[Channel]) -> int: + def ch_duration(self, *channels: Channel) -> int: """Return the time of the end of the last instruction over the supplied channels. Args: @@ -182,7 +235,7 @@ def ch_duration(self, *channels: List[Channel]) -> int: """ return self.ch_stop_time(*channels) - def ch_start_time(self, *channels: List[Channel]) -> int: + def ch_start_time(self, *channels: Channel) -> int: """Return the time of the start of the first instruction over the supplied channels. Args: @@ -195,7 +248,7 @@ def ch_start_time(self, *channels: List[Channel]) -> int: # If there are no instructions over channels return 0 - def ch_stop_time(self, *channels: List[Channel]) -> int: + def ch_stop_time(self, *channels: Channel) -> int: """Return maximum start time over supplied channels. Args: @@ -251,7 +304,7 @@ def _immutable_shift(self, """ if name is None: name = self.name - return Schedule((time, self), name=name) + return Schedule((time, self), name=name, metadata=self.metadata.copy()) def _mutable_shift(self, time: int @@ -312,7 +365,7 @@ def _mutable_insert(self, """ self._add_timeslots(start_time, schedule) self.__children.append((start_time, schedule)) - self._update_parameter_table(schedule) + self._parameter_manager.update_parameter_table(schedule) return self def _immutable_insert(self, @@ -328,7 +381,7 @@ def _immutable_insert(self, """ if name is None: name = self.name - new_sched = Schedule(name=name) + new_sched = Schedule(name=name, metadata=self.metadata.copy()) new_sched._mutable_insert(0, self) new_sched._mutable_insert(start_time, schedule) return new_sched @@ -364,11 +417,12 @@ def flatten(self) -> 'Schedule': return flatten(self) - def filter(self, *filter_funcs: List[Callable], + def filter(self, *filter_funcs: Callable, channels: Optional[Iterable[Channel]] = None, - instruction_types=None, + instruction_types: Union[Iterable[abc.ABCMeta], abc.ABCMeta] = None, time_ranges: Optional[Iterable[Tuple[int, int]]] = None, - intervals: Optional[Iterable[Interval]] = None) -> 'Schedule': + intervals: Optional[Iterable[Interval]] = None, + check_subroutine: bool = True) -> 'Schedule': """Return a new ``Schedule`` with only the instructions from this ``Schedule`` which pass though the provided filters; i.e. an instruction will be retained iff every function in ``filter_funcs`` returns ``True``, the instruction occurs on a channel type contained in @@ -380,28 +434,31 @@ def filter(self, *filter_funcs: List[Callable], Args: filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) - tuple and return a bool. + tuple and return a bool. channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types (Optional[Iterable[Type[qiskit.pulse.Instruction]]]): For example, - ``[PulseInstruction, AcquireInstruction]``. + instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. time_ranges: For example, ``[(0, 5), (6, 10)]``. intervals: For example, ``[(0, 5), (6, 10)]``. + check_subroutine: Set `True` to individually filter instructions inside of a subroutine + defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. """ - composed_filter = self._construct_filter(*filter_funcs, - channels=channels, - instruction_types=instruction_types, - time_ranges=time_ranges, - intervals=intervals) - return self._apply_filter(composed_filter, - new_sched_name="{name}".format(name=self.name)) + from qiskit.pulse.filters import composite_filter, filter_instructions - def exclude(self, *filter_funcs: List[Callable], + filters = composite_filter(channels, instruction_types, time_ranges, intervals) + filters.extend(filter_funcs) + + return filter_instructions(self, filters=filters, negate=False, + recurse_subroutines=check_subroutine) + + def exclude(self, *filter_funcs: Callable, channels: Optional[Iterable[Channel]] = None, - instruction_types=None, + instruction_types: Union[Iterable[abc.ABCMeta], abc.ABCMeta] = None, time_ranges: Optional[Iterable[Tuple[int, int]]] = None, - intervals: Optional[Iterable[Interval]] = None) -> 'Schedule': - """Return a Schedule with only the instructions from this Schedule *failing* at least one - of the provided filters. This method is the complement of ``self.filter``, so that:: + intervals: Optional[Iterable[Interval]] = None, + check_subroutine: bool = True) -> 'Schedule': + """Return a ``Schedule`` with only the instructions from this Schedule *failing* + at least one of the provided filters. + This method is the complement of py:meth:`~self.filter`, so that:: self.filter(args) | self.exclude(args) == self @@ -409,114 +466,19 @@ def exclude(self, *filter_funcs: List[Callable], filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) tuple and return a bool. channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types (Optional[Iterable[Type[qiskit.pulse.Instruction]]]): For example, - ``[PulseInstruction, AcquireInstruction]``. + instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. time_ranges: For example, ``[(0, 5), (6, 10)]``. intervals: For example, ``[(0, 5), (6, 10)]``. + check_subroutine: Set `True` to individually filter instructions inside of a subroutine + defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. """ - composed_filter = self._construct_filter(*filter_funcs, - channels=channels, - instruction_types=instruction_types, - time_ranges=time_ranges, - intervals=intervals) - return self._apply_filter(lambda x: not composed_filter(x), - new_sched_name="{name}".format(name=self.name)) - - def _apply_filter(self, filter_func: Callable, new_sched_name: str) -> 'Schedule': - """Return a Schedule containing only the instructions from this Schedule for which - ``filter_func`` returns ``True``. - - Args: - filter_func: Function of the form (int, Union['Schedule', Instruction]) -> bool. - new_sched_name: Name of the returned ``Schedule``. - """ - from qiskit.pulse.transforms import flatten - - subschedules = flatten(self)._children - valid_subschedules = [sched for sched in subschedules if filter_func(sched)] - return Schedule(*valid_subschedules, name=new_sched_name) - - def _construct_filter(self, *filter_funcs: List[Callable], - channels: Optional[Iterable[Channel]] = None, - instruction_types: Optional[Iterable[Instruction]] = None, - time_ranges: Optional[Iterable[Tuple[int, int]]] = None, - intervals: Optional[Iterable[Interval]] = None) -> Callable: - """Returns a boolean-valued function with input type ``(int, ScheduleComponent)`` that - returns ``True`` iff the input satisfies all of the criteria specified by the arguments; - i.e. iff every function in ``filter_funcs`` returns ``True``, the instruction occurs on a - channel type contained in ``channels``, the instruction type is contained in - ``instruction_types``, and the period over which the instruction operates is fully - contained in one specified in ``time_ranges`` or ``intervals``. - - Args: - filter_funcs: A list of Callables which take a (int, ScheduleComponent) tuple and - return a bool - channels: For example, ``[DriveChannel(0), AcquireChannel(0)]`` or ``DriveChannel(0)`` - instruction_types: For example, ``[PulseInstruction, AcquireInstruction]`` - or ``DelayInstruction`` - time_ranges: For example, ``[(0, 5), (6, 10)]`` or ``(0, 5)`` - intervals: For example, ``[Interval(0, 5), Interval(6, 10)]`` or ``Interval(0, 5)`` - """ - - def if_scalar_cast_to_list(to_list): - try: - iter(to_list) - except TypeError: - to_list = [to_list] - return to_list - - def only_channels(channels: Union[Set[Channel], Channel]) -> Callable: - channels = if_scalar_cast_to_list(channels) - - def channel_filter(time_inst) -> bool: - """Filter channel. - - Args: - time_inst (Tuple[int, Instruction]): Time - """ - return any(chan in channels for chan in time_inst[1].channels) - return channel_filter - - def only_instruction_types(types: Union[Iterable[abc.ABCMeta], abc.ABCMeta]) -> Callable: - types = if_scalar_cast_to_list(types) - - def instruction_filter(time_inst) -> bool: - """Filter instruction. - - Args: - time_inst (Tuple[int, Instruction]): Time - """ - return isinstance(time_inst[1], tuple(types)) - return instruction_filter - - def only_intervals(ranges: Union[Iterable[Interval], Interval]) -> Callable: - ranges = if_scalar_cast_to_list(ranges) - - def interval_filter(time_inst) -> bool: - """Filter interval. - Args: - time_inst (Tuple[int, Instruction]): Time - """ - for i in ranges: - inst_start = time_inst[0] - inst_stop = inst_start + time_inst[1].duration - if i[0] <= inst_start and inst_stop <= i[1]: - return True - return False + from qiskit.pulse.filters import composite_filter, filter_instructions - return interval_filter + filters = composite_filter(channels, instruction_types, time_ranges, intervals) + filters.extend(filter_funcs) - filter_func_list = list(filter_funcs) - if channels is not None: - filter_func_list.append(only_channels(channels)) - if instruction_types is not None: - filter_func_list.append(only_instruction_types(instruction_types)) - if time_ranges is not None: - filter_func_list.append(only_intervals(time_ranges)) - if intervals is not None: - filter_func_list.append(only_intervals(intervals)) - # return function returning true iff all filters are passed - return lambda x: all(filter_func(x) for filter_func in filter_func_list) + return filter_instructions(self, filters=filters, negate=True, + recurse_subroutines=check_subroutine) def _add_timeslots(self, time: int, @@ -621,35 +583,41 @@ def _replace_timeslots(self, self._remove_timeslots(time, old) self._add_timeslots(time, new) + def _renew_timeslots(self): + """Regenerate timeslots based on current instructions.""" + self._timeslots.clear() + for t0, inst in self.instructions: + self._add_timeslots(t0, inst) + def replace(self, old: ScheduleComponent, new: ScheduleComponent, inplace: bool = False, ) -> 'Schedule': - """Return a schedule with the ``old`` instruction replaced with a ``new`` + """Return a ``Schedule`` with the ``old`` instruction replaced with a ``new`` instruction. The replacement matching is based on an instruction equality check. .. jupyter-kernel:: python3 - :id: replace + :id: replace .. jupyter-execute:: - from qiskit import pulse + from qiskit import pulse - d0 = pulse.DriveChannel(0) + d0 = pulse.DriveChannel(0) - sched = pulse.Schedule() + sched = pulse.Schedule() - old = pulse.Play(pulse.Constant(100, 1.0), d0) - new = pulse.Play(pulse.Constant(100, 0.1), d0) + old = pulse.Play(pulse.Constant(100, 1.0), d0) + new = pulse.Play(pulse.Constant(100, 0.1), d0) - sched += old + sched += old - sched = sched.replace(old, new) + sched = sched.replace(old, new) - assert sched == pulse.Schedule(new) + assert sched == pulse.Schedule(new) Only matches at the top-level of the schedule tree. If you wish to perform this replacement over all instructions in the schedule tree. @@ -657,110 +625,77 @@ def replace(self, .. jupyter-execute:: - sched = pulse.Schedule() + sched = pulse.Schedule() - sched += pulse.Schedule(old) + sched += pulse.Schedule(old) - sched = sched.flatten() + sched = sched.flatten() - sched = sched.replace(old, new) + sched = sched.replace(old, new) - assert sched == pulse.Schedule(new) + assert sched == pulse.Schedule(new) Args: - old: Instruction to replace. - new: Instruction to replace with. - inplace: Replace instruction by mutably modifying this ``Schedule``. + old: Instruction to replace. + new: Instruction to replace with. + inplace: Replace instruction by mutably modifying this ``Schedule``. Returns: - The modified schedule with ``old`` replaced by ``new``. + The modified schedule with ``old`` replaced by ``new``. Raises: PulseError: If the ``Schedule`` after replacements will has a timing overlap. """ + from qiskit.pulse.parameter_manager import ParameterManager + new_children = [] + new_parameters = ParameterManager() + for time, child in self._children: if child == old: new_children.append((time, new)) - if inplace: - self._replace_timeslots(time, old, new) + new_parameters.update_parameter_table(new) else: new_children.append((time, child)) + new_parameters.update_parameter_table(child) if inplace: self.__children = new_children - self._parameter_table.clear() - for _, child in new_children: - self._update_parameter_table(child) + self._parameter_manager = new_parameters + self._renew_timeslots() return self else: try: - return Schedule(*new_children) + return Schedule(*new_children, name=self.name, metadata=self.metadata.copy()) except PulseError as err: raise PulseError( 'Replacement of {old} with {new} results in ' 'overlapping instructions.'.format( old=old, new=new)) from err - @property - def parameters(self) -> Set: - """Parameters which determine the schedule behavior.""" - return set(self._parameter_table.keys()) - def is_parameterized(self) -> bool: """Return True iff the instruction is parameterized.""" - return bool(self.parameters) + return self._parameter_manager.is_parameterized() def assign_parameters(self, value_dict: Dict[ParameterExpression, ParameterValueType], + inplace: bool = True ) -> 'Schedule': """Assign the parameters in this schedule according to the input. Args: value_dict: A mapping from Parameters to either numeric values or another Parameter expression. + inplace: Set ``True`` to override this instance with new parameter. Returns: - Schedule with updated parameters (a new one if not inplace, otherwise self). + Schedule with updated parameters. """ - for parameter in self.parameters: - if parameter not in value_dict: - continue - - value = value_dict[parameter] - for inst in self._parameter_table[parameter]: - inst.assign_parameters({parameter: value}) - - entry = self._parameter_table.pop(parameter) - if isinstance(value, ParameterExpression): - for new_parameter in value.parameters: - if new_parameter in self._parameter_table: - new_entry = set(entry + self._parameter_table[new_parameter]) - self._parameter_table[new_parameter] = list(new_entry) - else: - self._parameter_table[new_parameter] = entry - - # Update timeslots according to new channel keys - for chan in copy.copy(self._timeslots): - if isinstance(chan.index, ParameterExpression): - chan_timeslots = self._timeslots.pop(chan) - - # Find the channel's new assignment - new_channel = chan - for param, value in value_dict.items(): - if param in new_channel.parameters: - new_channel = new_channel.assign(param, value) - - # Merge with existing channel - if new_channel in self._timeslots: - sched = Schedule() - sched._timeslots = {new_channel: chan_timeslots} - self._add_timeslots(0, sched) - # Or add back under the new name - else: - self._timeslots[new_channel] = chan_timeslots - - return self + return self._parameter_manager.assign_parameters( + pulse_program=self, + value_dict=value_dict, + inplace=inplace + ) def get_parameters(self, parameter_name: str) -> List[Parameter]: @@ -775,159 +710,26 @@ def get_parameters(self, Returns: Parameter objects that have corresponding name. """ - return [param for param in self.parameters if param.name == parameter_name] - - def _update_parameter_table(self, schedule: 'Schedule'): - """ + return self._parameter_manager.get_parameters(parameter_name) - Args: - schedule: - """ - # TODO need to fix cyclic import - from qiskit.pulse.transforms import flatten + def __len__(self) -> int: + """Return number of instructions in the schedule.""" + return len(self.instructions) - schedule = flatten(schedule) - for _, inst in schedule.instructions: - for param in inst.parameters: - self._parameter_table[param].append(inst) - - def draw(self, - dt: Any = None, # deprecated - style: Optional[Dict[str, Any]] = None, - filename: Any = None, # deprecated - interp_method: Any = None, # deprecated - scale: Any = None, # deprecated - channel_scales: Any = None, # deprecated - plot_all: Any = None, # deprecated - plot_range: Any = None, # deprecated - interactive: Any = None, # deprecated - table: Any = None, # deprecated - label: Any = None, # deprecated - framechange: Any = None, # deprecated - channels: Any = None, # deprecated - show_framechange_channels: Any = None, # deprecated - draw_title: Any = None, # deprecated - backend=None, # importing backend causes cyclic import - time_range: Optional[Tuple[int, int]] = None, - time_unit: str = 'dt', - disable_channels: Optional[List[Channel]] = None, - show_snapshot: bool = True, - show_framechange: bool = True, - show_waveform_info: bool = True, - show_barrier: bool = True, - plotter: str = 'mpl2d', - axis: Optional[Any] = None): - """Plot the schedule. + def __add__(self, other: ScheduleComponent) -> 'Schedule': + """Return a new schedule with ``other`` inserted within ``self`` at ``start_time``.""" + return self.append(other) - Args: - style: Stylesheet options. This can be dictionary or preset stylesheet classes. See - :py:class:~`qiskit.visualization.pulse_v2.stylesheets.IQXStandard`, - :py:class:~`qiskit.visualization.pulse_v2.stylesheets.IQXSimple`, and - :py:class:~`qiskit.visualization.pulse_v2.stylesheets.IQXDebugging` for details of - preset stylesheets. - backend (Optional[BaseBackend]): Backend object to play the input pulse program. - If provided, the plotter may use to make the visualization hardware aware. - time_range: Set horizontal axis limit. Tuple `(tmin, tmax)`. - time_unit: The unit of specified time range either `dt` or `ns`. - The unit of `ns` is available only when `backend` object is provided. - disable_channels: A control property to show specific pulse channel. - Pulse channel instances provided as a list are not shown in the output image. - show_snapshot: Show snapshot instructions. - show_framechange: Show frame change instructions. The frame change represents - instructions that modulate phase or frequency of pulse channels. - show_waveform_info: Show additional information about waveforms such as their name. - show_barrier: Show barrier lines. - plotter: Name of plotter API to generate an output image. - One of following APIs should be specified:: - - mpl2d: Matplotlib API for 2D image generation. - Matplotlib API to generate 2D image. Charts are placed along y axis with - vertical offset. This API takes matplotlib.axes.Axes as ``axis`` input. - - ``axis`` and ``style`` kwargs may depend on the plotter. - axis: Arbitrary object passed to the plotter. If this object is provided, - the plotters use a given ``axis`` instead of internally initializing - a figure object. This object format depends on the plotter. - See plotter argument for details. - dt: Deprecated. This argument is used by the legacy pulse drawer. - filename: Deprecated. This argument is used by the legacy pulse drawer. - To save output image, you can call ``.savefig`` method with - returned Matplotlib Figure object. - interp_method: Deprecated. This argument is used by the legacy pulse drawer. - scale: Deprecated. This argument is used by the legacy pulse drawer. - channel_scales: Deprecated. This argument is used by the legacy pulse drawer. - plot_all: Deprecated. This argument is used by the legacy pulse drawer. - plot_range: Deprecated. This argument is used by the legacy pulse drawer. - interactive: Deprecated. This argument is used by the legacy pulse drawer. - table: Deprecated. This argument is used by the legacy pulse drawer. - label: Deprecated. This argument is used by the legacy pulse drawer. - framechange: Deprecated. This argument is used by the legacy pulse drawer. - channels: Deprecated. This argument is used by the legacy pulse drawer. - show_framechange_channels: Deprecated. This argument is used by the legacy pulse drawer. - draw_title: Deprecated. This argument is used by the legacy pulse drawer. + def __or__(self, other: ScheduleComponent) -> 'Schedule': + """Return a new schedule which is the union of `self` and `other`.""" + return self.insert(0, other) - Returns: - Visualization output data. - The returned data type depends on the ``plotter``. - If matplotlib family is specified, this will be a ``matplotlib.pyplot.Figure`` data. - """ - # pylint: disable=cyclic-import, missing-return-type-doc - from qiskit.visualization import pulse_drawer_v2, SchedStyle - - legacy_args = {'dt': dt, - 'filename': filename, - 'interp_method': interp_method, - 'scale': scale, - 'channel_scales': channel_scales, - 'plot_all': plot_all, - 'plot_range': plot_range, - 'interactive': interactive, - 'table': table, - 'label': label, - 'framechange': framechange, - 'channels': channels, - 'show_framechange_channels': show_framechange_channels, - 'draw_title': draw_title} - - active_legacy_args = [] - for name, legacy_arg in legacy_args.items(): - if legacy_arg is not None: - active_legacy_args.append(name) - - if active_legacy_args: - warnings.warn('Legacy pulse drawer is deprecated. ' - 'Specified arguments {dep_args} are deprecated. ' - 'Please check the API document of new pulse drawer ' - '`qiskit.visualization.pulse_drawer_v2`.' - ''.format(dep_args=', '.join(active_legacy_args)), - DeprecationWarning) - - if filename: - warnings.warn('File saving is delegated to the plotter software in new drawer. ' - 'If you specify matplotlib plotter family to `plotter` argument, ' - 'you can call `savefig` method with the returned Figure object.', - DeprecationWarning) - - if isinstance(style, SchedStyle): - style = None - warnings.warn('Legacy stylesheet is specified. This is ignored in the new drawer. ' - 'Please check the API documentation for this method.') - - return pulse_drawer_v2(program=self, - style=style, - backend=backend, - time_range=time_range, - time_unit=time_unit, - disable_channels=disable_channels, - show_snapshot=show_snapshot, - show_framechange=show_framechange, - show_waveform_info=show_waveform_info, - show_barrier=show_barrier, - plotter=plotter, - axis=axis) + def __lshift__(self, time: int) -> 'Schedule': + """Return a new schedule which is shifted forward by ``time``.""" + return self.shift(time) def __eq__(self, other: ScheduleComponent) -> bool: - """Test if two ScheduleComponents are equal. + """Test if two Schedule are equal. Equality is checked by verifying there is an equal instruction at every time in ``other`` for every instruction in this ``Schedule``. @@ -937,54 +739,586 @@ def __eq__(self, other: ScheduleComponent) -> bool: This does not check for logical equivalency. Ie., ```python - >>> (Delay(10)(DriveChannel(0)) + Delay(10)(DriveChannel(0)) == - Delay(20)(DriveChannel(0))) + >>> Delay(10, DriveChannel(0)) + Delay(10, DriveChannel(0)) + == Delay(20, DriveChannel(0)) False ``` """ - channels = set(self.channels) - other_channels = set(other.channels) + # 0. type check, we consider Instruction is a subtype of schedule + if not isinstance(other, (type(self), Instruction)): + return False - # first check channels are the same - if channels != other_channels: + # 1. channel check + if set(self.channels) != set(other.channels): return False - # then verify same number of instructions in each - instructions = self.instructions - other_instructions = other.instructions - if len(instructions) != len(other_instructions): + # 2. size check + if len(self.instructions) != len(other.instructions): return False - # finally check each instruction in `other` is in this schedule - for idx, inst in enumerate(other_instructions): - # check assumes `Schedule.instructions` is sorted consistently - if instructions[idx] != inst: + # 3. instruction check + return all(self_inst == other_inst for self_inst, other_inst + in zip(self.instructions, other.instructions)) + + def __repr__(self) -> str: + name = format(self._name) if self._name else "" + instructions = ", ".join([repr(instr) for instr in self.instructions[:50]]) + if len(self.instructions) > 25: + instructions += ", ..." + return '{}({}, name="{}")'.format( + self.__class__.__name__, + instructions, + name + ) + + +def _require_schedule_conversion(function: Callable) -> Callable: + """A method decorator to convert schedule block to pulse schedule. + + This conversation is performed for backward compatibility only if all durations are assigned. + """ + @functools.wraps(function) + def wrapper(self, *args, **kwargs): + from qiskit.pulse.transforms import block_to_schedule + if self.is_schedulable(): + return function(block_to_schedule(self), *args, **kwargs) + raise UnassignedDurationError('This method requires all durations to be assigned with ' + 'some integer value. Please check `.parameters` to find ' + 'unassigned parameter objects.') + return wrapper + + +class ScheduleBlock: + """A ``ScheduleBlock`` is a time-ordered sequence of instructions and transform macro to + manage their relative timing. The relative position of the instructions is managed by + the ``context_alignment``. This allows ``ScheduleBlock`` to support instructions with + a parametric duration and allows the lazy scheduling of instructions, + i.e. allocating the instruction time just before execution. + + ``ScheduleBlock`` s should be initialized with one of the following alignment contexts: + + - :class:`~qiskit.pulse.transforms.AlignLeft`: Align instructions in the + `as-soon-as-possible` manner. Instructions are scheduled at the earliest + possible time on the channel. + + - :class:`~qiskit.pulse.transforms.AlignRight`: Align instructions in the + `as-late-as-possible` manner. Instructions are scheduled at the latest + possible time on the channel. + + - :class:`~qiskit.pulse.transforms.AlignSequential`: Align instructions sequentially + even though they are allocated in different channels. + + - :class:`~qiskit.pulse.transforms.AlignEquispaced`: Align instructions with + equal interval within a specified duration. Instructions on different channels + are aligned sequentially. + + - :class:`~qiskit.pulse.transforms.AlignFunc`: Align instructions with + arbitrary position within the given duration. The position is specified by + a callback function taking a pulse index ``j`` and returning a + fractional coordinate in [0, 1]. + + The ``ScheduleBlock`` defaults to the ``AlignLeft`` alignment. + The timing overlap constraint of instructions is not immediately evaluated, + and thus we can assign a parameter object to the instruction duration. + Instructions are implicitly scheduled at optimum time when the program is executed. + + Note that ``ScheduleBlock`` can contain :class:`~qiskit.pulse.instructions.Instruction`s + and other ``ScheduleBlock``s to build an experimental program, but ``Schedule`` is not + supported. This should be added as a :class:`~qiskit.pulse.instructions.Call` instruction. + This conversion is automatically performed with the pulse builder. + + By using ``ScheduleBlock`` representation we can fully parametrize pulse waveform. + For example, Rabi schedule generator can be defined as + + .. code-block:: python + + duration = Parameter('rabi_dur') + amp = Parameter('rabi_amp') + + block = ScheduleBlock() + rabi_pulse = pulse.Gaussian(duration=duration, amp=amp, sigma=duration/4) + + block += Play(rabi_pulse, pulse.DriveChannel(0)) + block += Call(measure_schedule) + + Note that such waveform cannot be appended to the ``Schedule`` representation. + + In the block representation, the interval between two instructions can be + managed with the ``Delay`` instruction. Because the schedule block lacks an instruction + start time ``t0``, we cannot ``insert`` or ``shift`` the target instruction. + In addition, stored instructions are not interchangable because the schedule block is + sensitive to the relative position of instructions. + Apart from these differences, the block representation can provide compatible + functionality with ``Schedule`` representation. + """ + # Prefix to use for auto naming. + prefix = 'block' + + # Counter to count instance number. + instances_counter = itertools.count() + + def __init__(self, + name: Optional[str] = None, + metadata: Optional[dict] = None, + alignment_context=None): + """Create an empty schedule block. + + Args: + name: Name of this schedule. Defaults to an autogenerated string if not provided. + metadata: Arbitrary key value metadata to associate with the schedule. This gets + stored as free-form data in a dict in the + :attr:`~qiskit.pulse.ScheduleBlock.metadata` attribute. It will not be directly + used in the schedule. + alignment_context (AlignmentKind): ``AlignmentKind`` instance that manages + scheduling of instructions in this block. + Raises: + TypeError: if metadata is not a dict. + """ + from qiskit.pulse.parameter_manager import ParameterManager + from qiskit.pulse.transforms import AlignLeft + + if name is None: + name = self.prefix + str(next(self.instances_counter)) + if sys.platform != "win32" and not is_main_process(): + name += '-{}'.format(mp.current_process().pid) + + self._name = name + self._parameter_manager = ParameterManager() + + if not isinstance(metadata, dict) and metadata is not None: + raise TypeError("Only a dictionary or None is accepted for schedule metadata") + self._metadata = metadata or dict() + + self._alignment_context = alignment_context or AlignLeft() + self._blocks = list() + + # get parameters from context + self._parameter_manager.update_parameter_table(self._alignment_context) + + @property + def name(self) -> str: + """Name of this Schedule""" + return self._name + + @property + def metadata(self) -> Dict[str, Any]: + """The user provided metadata associated with the schedule. + + User provided ``dict`` of metadata for the schedule. + The metadata contents do not affect the semantics of the program + but are used to influence the execution of the schedule. It is expected + to be passed between all transforms of the schedule and that providers + will associate any schedule metadata with the results it returns from the + execution of that schedule. + """ + return self._metadata + + @metadata.setter + def metadata(self, metadata): + """Update the schedule metadata""" + if not isinstance(metadata, dict) and metadata is not None: + raise TypeError("Only a dictionary or None is accepted for schedule metadata") + self._metadata = metadata or dict() + + @property + def alignment_context(self): + """Return alignment instance that allocates block component to generate schedule.""" + return self._alignment_context + + def is_schedulable(self) -> bool: + """Return ``True`` if all durations are assigned.""" + # check context assignment + for context_param in self.alignment_context._context_params: + if isinstance(context_param, ParameterExpression): return False + # check duration assignment + for block in self.instructions: + if isinstance(block, ScheduleBlock): + if not block.is_schedulable(): + return False + else: + if not isinstance(block.duration, int): + return False return True - def __add__(self, other: ScheduleComponent) -> 'Schedule': - """Return a new schedule with ``other`` inserted within ``self`` at ``start_time``.""" - return self.append(other) + @property + @deprecated_functionality + @_require_schedule_conversion + def timeslots(self) -> TimeSlots: + """Time keeping attribute.""" + return self.timeslots - def __or__(self, other: ScheduleComponent) -> 'Schedule': - """Return a new schedule which is the union of `self` and `other`.""" - return self.insert(0, other) + @property + @_require_schedule_conversion + def duration(self) -> int: + """Duration of this schedule block.""" + return self.duration - def __lshift__(self, time: int) -> 'Schedule': - """Return a new schedule which is shifted forward by ``time``.""" - return self.shift(time) + @property + @deprecated_functionality + @_require_schedule_conversion + def start_time(self) -> int: + """Starting time of this schedule block.""" + return self.ch_start_time(*self.channels) + + @property + @deprecated_functionality + @_require_schedule_conversion + def stop_time(self) -> int: + """Stopping time of this schedule block.""" + return self.duration + + @property + def channels(self) -> Tuple[Channel]: + """Returns channels that this schedule clock uses.""" + chans = set() + for block in self.instructions: + for chan in block.channels: + chans.add(chan) + return tuple(chans) + + @property + def instructions(self) -> Tuple[BlockComponent]: + """Get the time-ordered instructions from self.""" + return tuple(self._blocks) + + @property + def parameters(self) -> Set: + """Parameters which determine the schedule behavior.""" + return self._parameter_manager.parameters + + @_require_schedule_conversion + def ch_duration(self, *channels: Channel) -> int: + """Return the time of the end of the last instruction over the supplied channels. + + Args: + *channels: Channels within ``self`` to include. + """ + return self.ch_duration(*channels) + + @deprecated_functionality + @_require_schedule_conversion + def ch_start_time(self, *channels: Channel) -> int: + """Return the time of the start of the first instruction over the supplied channels. + + Args: + *channels: Channels within ``self`` to include. + """ + return self.ch_start_time(*channels) + + @deprecated_functionality + @_require_schedule_conversion + def ch_stop_time(self, *channels: Channel) -> int: + """Return maximum start time over supplied channels. + + Args: + *channels: Channels within ``self`` to include. + """ + return self.ch_stop_time(*channels) + + @deprecated_functionality + def shift(self, + time: int, + name: Optional[str] = None, + inplace: bool = True): + """This method will be removed. Temporarily added for backward compatibility. + + .. note:: This method is not supported and being deprecated. + + Args: + time: Time to shift by. + name: Name of the new schedule. Defaults to the name of self. + inplace: Perform operation inplace on this schedule. Otherwise + return a new ``Schedule``. + + Raises: + PulseError: When this method is called. This method is not supported. + """ + raise PulseError('Method ``ScheduleBlock.shift`` is not supported as this program ' + 'representation does not have the notion of instruction ' + 'time. Apply ``qiskit.pulse.transforms.block_to_schedule`` function to ' + 'this program to obtain the ``Schedule`` representation supporting ' + 'this method.') + + @deprecated_functionality + def insert(self, + start_time: int, + block: ScheduleComponent, + name: Optional[str] = None, + inplace: bool = True): + """This method will be removed. Temporarily added for backward compatibility. + + .. note:: This method is not supported and being deprecated. + + Args: + start_time: Time to insert the schedule. + block: Schedule to insert. + name: Name of the new schedule. Defaults to the name of self. + inplace: Perform operation inplace on this schedule. Otherwise + return a new ``Schedule``. + + Raises: + PulseError: When this method is called. This method is not supported. + """ + raise PulseError('Method ``ScheduleBlock.insert`` is not supported as this program ' + 'representation does not have the notion of instruction ' + 'time. Apply ``qiskit.pulse.transforms.block_to_schedule`` function to ' + 'this program to obtain the ``Schedule`` representation supporting ' + 'this method.') + + def append(self, block: BlockComponent, + name: Optional[str] = None, + inplace: bool = True) -> 'ScheduleBlock': + """Return a new schedule block with ``block`` appended to the context block. + The execution time is automatically assigned when the block is converted into schedule. + + Args: + block: ScheduleBlock to be appended. + name: Name of the new ``Schedule``. Defaults to name of ``self``. + inplace: Perform operation inplace on this schedule. Otherwise + return a new ``Schedule``. + + Returns: + Schedule block with appended schedule. + + Raises: + PulseError: When invalid schedule type is specified. + """ + if not isinstance(block, (ScheduleBlock, Instruction)): + raise PulseError(f'Appended `schedule` {block.__class__.__name__} is invalid type. ' + 'Only `Instruction` and `ScheduleBlock` can be accepted.') + + if not inplace: + ret_block = copy.deepcopy(self) + ret_block._name = name or self.name + ret_block.append(block, inplace=True) + return ret_block + else: + self._blocks.append(block) + self._parameter_manager.update_parameter_table(block) + + return self + + def filter(self, *filter_funcs: List[Callable], + channels: Optional[Iterable[Channel]] = None, + instruction_types: Union[Iterable[abc.ABCMeta], abc.ABCMeta] = None, + time_ranges: Optional[Iterable[Tuple[int, int]]] = None, + intervals: Optional[Iterable[Interval]] = None, + check_subroutine: bool = True): + """Return a new ``Schedule`` with only the instructions from this ``ScheduleBlock`` + which pass though the provided filters; i.e. an instruction will be retained iff + every function in ``filter_funcs`` returns ``True``, the instruction occurs on + a channel type contained in ``channels``, the instruction type is contained + in ``instruction_types``, and the period over which the instruction operates + is *fully* contained in one specified in ``time_ranges`` or ``intervals``. + + If no arguments are provided, ``self`` is returned. + + .. note:: This method is currently not supported. Support will be soon added + please create an issue if you believe this must be prioritized. + + Args: + filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) + tuple and return a bool. + channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. + instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. + time_ranges: For example, ``[(0, 5), (6, 10)]``. + intervals: For example, ``[(0, 5), (6, 10)]``. + check_subroutine: Set `True` to individually filter instructions inside of a subroutine + defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. + + Returns: + ``Schedule`` consisting of instructions that matches with filtering condition. + + Raises: + PulseError: When this method is called. This method will be supported soon. + """ + raise PulseError('Method ``ScheduleBlock.filter`` is not supported as this program ' + 'representation does not have the notion of an explicit instruction ' + 'time. Apply ``qiskit.pulse.transforms.block_to_schedule`` function to ' + 'this program to obtain the ``Schedule`` representation supporting ' + 'this method.') + + def exclude(self, *filter_funcs: List[Callable], + channels: Optional[Iterable[Channel]] = None, + instruction_types: Union[Iterable[abc.ABCMeta], abc.ABCMeta] = None, + time_ranges: Optional[Iterable[Tuple[int, int]]] = None, + intervals: Optional[Iterable[Interval]] = None, + check_subroutine: bool = True): + """Return a ``Schedule`` with only the instructions from this Schedule *failing* + at least one of the provided filters. + This method is the complement of py:meth:`~self.filter`, so that:: + + self.filter(args) | self.exclude(args) == self + + .. note:: This method is currently not supported. Support will be soon added + please create an issue if you believe this must be prioritized. + + Args: + filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) + tuple and return a bool. + channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. + instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. + time_ranges: For example, ``[(0, 5), (6, 10)]``. + intervals: For example, ``[(0, 5), (6, 10)]``. + check_subroutine: Set `True` to individually filter instructions inside of a subroutine + defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. + + Returns: + ``Schedule`` consisting of instructions that are not matche with filtering condition. + + Raises: + PulseError: When this method is called. This method will be supported soon. + """ + raise PulseError('Method ``ScheduleBlock.exclude`` is not supported as this program ' + 'representation does not have the notion of instruction ' + 'time. Apply ``qiskit.pulse.transforms.block_to_schedule`` function to ' + 'this program to obtain the ``Schedule`` representation supporting ' + 'this method.') + + def replace(self, + old: BlockComponent, + new: BlockComponent, + inplace: bool = True, + ) -> 'ScheduleBlock': + """Return a ``ScheduleBlock`` with the ``old`` component replaced with a ``new`` + component. + + Args: + old: Schedule block component to replace. + new: Schedule block component to replace with. + inplace: Replace instruction by mutably modifying this ``ScheduleBlock``. + + Returns: + The modified schedule block with ``old`` replaced by ``new``. + """ + from qiskit.pulse.parameter_manager import ParameterManager + + new_blocks = [] + new_parameters = ParameterManager() + + for block in self.instructions: + if block == old: + new_blocks.append(new) + new_parameters.update_parameter_table(new) + else: + if isinstance(block, ScheduleBlock): + new_blocks.append(block.replace(old, new, inplace)) + else: + new_blocks.append(block) + new_parameters.update_parameter_table(block) + + if inplace: + self._blocks = new_blocks + self._parameter_manager = new_parameters + return self + else: + ret_block = copy.deepcopy(self) + ret_block._blocks = new_blocks + ret_block._parameter_manager = new_parameters + return ret_block + + def is_parameterized(self) -> bool: + """Return True iff the instruction is parameterized.""" + return self._parameter_manager.is_parameterized() + + def assign_parameters(self, + value_dict: Dict[ParameterExpression, ParameterValueType], + inplace: bool = True + ) -> 'ScheduleBlock': + """Assign the parameters in this schedule according to the input. + + Args: + value_dict: A mapping from Parameters to either numeric values or another + Parameter expression. + inplace: Set ``True`` to override this instance with new parameter. + + Returns: + Schedule with updated parameters. + """ + return self._parameter_manager.assign_parameters( + pulse_program=self, + value_dict=value_dict, + inplace=inplace + ) + + def get_parameters(self, + parameter_name: str) -> List[Parameter]: + """Get parameter object bound to this schedule by string name. + + Because different ``Parameter`` objects can have the same name, + this method returns a list of ``Parameter`` s for the provided name. + + Args: + parameter_name: Name of parameter. + + Returns: + Parameter objects that have corresponding name. + """ + return self._parameter_manager.get_parameters(parameter_name) def __len__(self) -> int: """Return number of instructions in the schedule.""" return len(self.instructions) - def __repr__(self): + def __eq__(self, other: 'ScheduleBlock') -> bool: + """Test if two ScheduleBlocks are equal. + + Equality is checked by verifying there is an equal instruction at every time + in ``other`` for every instruction in this ``ScheduleBlock``. This check is + performed by converting the instruction representation into directed acyclic graph, + in which execution order of every instruction is evaluated correctly across all channels. + Also ``self`` and ``other`` should have the same alignment context. + + .. warning:: + + This does not check for logical equivalency. Ie., + + ```python + >>> Delay(10, DriveChannel(0)) + Delay(10, DriveChannel(0)) + == Delay(20, DriveChannel(0)) + False + ``` + """ + # 0. type check + if not isinstance(other, type(self)): + return False + + # 1. transformation check + if self.alignment_context != other.alignment_context: + return False + + # 2. channel check + if set(self.channels) != set(other.channels): + return False + + # 3. size check + if len(self) != len(other): + return False + + # 4. instruction check + import retworkx as rx + from qiskit.pulse.transforms import block_to_dag + + return rx.is_isomorphic_node_match(block_to_dag(self), block_to_dag(other), + lambda x, y: x == y) + + def __repr__(self) -> str: name = format(self._name) if self._name else "" instructions = ", ".join([repr(instr) for instr in self.instructions[:50]]) if len(self.instructions) > 25: instructions += ", ..." - return 'Schedule({}, name="{}")'.format(instructions, name) + return '{}({}, name="{}", transform={})'.format( + self.__class__.__name__, + instructions, + name, + repr(self.alignment_context) + ) + + def __add__(self, other: BlockComponent) -> 'ScheduleBlock': + """Return a new schedule with ``other`` inserted within ``self`` at ``start_time``.""" + return self.append(other) class ParameterizedSchedule: @@ -1073,6 +1407,162 @@ def __call__(self, *args: Union[int, float, complex, ParameterExpression], return self.bind_parameters(*args, **kwargs) +def _common_method(*classes): + """A function decorator to attach the function to specified classes as a method. + + .. note:: For developer: A method attached through this decorator may hurt readability + of the codebase, because the method may not be detected by a code editor. + Thus, this decorator should be used to a limited extent, i.e. huge helper method. + By using this decorator wisely, we can reduce code maintenance overhead without + losing readability of the codebase. + """ + def decorator(method): + @functools.wraps(method) + def wrapper(*args, **kwargs): + return method(*args, **kwargs) + for cls in classes: + setattr(cls, method.__name__, wrapper) + return method + return decorator + + +@_common_method(Schedule, ScheduleBlock) +def draw(self, + dt: Any = None, # deprecated + style: Optional[Dict[str, Any]] = None, + filename: Any = None, # deprecated + interp_method: Any = None, # deprecated + scale: Any = None, # deprecated + channel_scales: Any = None, # deprecated + plot_all: Any = None, # deprecated + plot_range: Any = None, # deprecated + interactive: Any = None, # deprecated + table: Any = None, # deprecated + label: Any = None, # deprecated + framechange: Any = None, # deprecated + channels: Any = None, # deprecated + show_framechange_channels: Any = None, # deprecated + draw_title: Any = None, # deprecated + backend=None, # importing backend causes cyclic import + time_range: Optional[Tuple[int, int]] = None, + time_unit: str = 'dt', + disable_channels: Optional[List[Channel]] = None, + show_snapshot: bool = True, + show_framechange: bool = True, + show_waveform_info: bool = True, + show_barrier: bool = True, + plotter: str = 'mpl2d', + axis: Optional[Any] = None): + """Plot the schedule. + + Args: + style: Stylesheet options. This can be dictionary or preset stylesheet classes. See + :py:class:~`qiskit.visualization.pulse_v2.stylesheets.IQXStandard`, + :py:class:~`qiskit.visualization.pulse_v2.stylesheets.IQXSimple`, and + :py:class:~`qiskit.visualization.pulse_v2.stylesheets.IQXDebugging` for details of + preset stylesheets. + backend (Optional[BaseBackend]): Backend object to play the input pulse program. + If provided, the plotter may use to make the visualization hardware aware. + time_range: Set horizontal axis limit. Tuple `(tmin, tmax)`. + time_unit: The unit of specified time range either `dt` or `ns`. + The unit of `ns` is available only when `backend` object is provided. + disable_channels: A control property to show specific pulse channel. + Pulse channel instances provided as a list are not shown in the output image. + show_snapshot: Show snapshot instructions. + show_framechange: Show frame change instructions. The frame change represents + instructions that modulate phase or frequency of pulse channels. + show_waveform_info: Show additional information about waveforms such as their name. + show_barrier: Show barrier lines. + plotter: Name of plotter API to generate an output image. + One of following APIs should be specified:: + + mpl2d: Matplotlib API for 2D image generation. + Matplotlib API to generate 2D image. Charts are placed along y axis with + vertical offset. This API takes matplotlib.axes.Axes as ``axis`` input. + + ``axis`` and ``style`` kwargs may depend on the plotter. + axis: Arbitrary object passed to the plotter. If this object is provided, + the plotters use a given ``axis`` instead of internally initializing + a figure object. This object format depends on the plotter. + See plotter argument for details. + dt: Deprecated. This argument is used by the legacy pulse drawer. + filename: Deprecated. This argument is used by the legacy pulse drawer. + To save output image, you can call ``.savefig`` method with + returned Matplotlib Figure object. + interp_method: Deprecated. This argument is used by the legacy pulse drawer. + scale: Deprecated. This argument is used by the legacy pulse drawer. + channel_scales: Deprecated. This argument is used by the legacy pulse drawer. + plot_all: Deprecated. This argument is used by the legacy pulse drawer. + plot_range: Deprecated. This argument is used by the legacy pulse drawer. + interactive: Deprecated. This argument is used by the legacy pulse drawer. + table: Deprecated. This argument is used by the legacy pulse drawer. + label: Deprecated. This argument is used by the legacy pulse drawer. + framechange: Deprecated. This argument is used by the legacy pulse drawer. + channels: Deprecated. This argument is used by the legacy pulse drawer. + show_framechange_channels: Deprecated. This argument is used by the legacy pulse drawer. + draw_title: Deprecated. This argument is used by the legacy pulse drawer. + + Returns: + Visualization output data. + The returned data type depends on the ``plotter``. + If matplotlib family is specified, this will be a ``matplotlib.pyplot.Figure`` data. + """ + # pylint: disable=cyclic-import, missing-return-type-doc + from qiskit.visualization import pulse_drawer_v2, SchedStyle + + legacy_args = {'dt': dt, + 'filename': filename, + 'interp_method': interp_method, + 'scale': scale, + 'channel_scales': channel_scales, + 'plot_all': plot_all, + 'plot_range': plot_range, + 'interactive': interactive, + 'table': table, + 'label': label, + 'framechange': framechange, + 'channels': channels, + 'show_framechange_channels': show_framechange_channels, + 'draw_title': draw_title} + + active_legacy_args = [] + for name, legacy_arg in legacy_args.items(): + if legacy_arg is not None: + active_legacy_args.append(name) + + if active_legacy_args: + warnings.warn('Legacy pulse drawer is deprecated. ' + 'Specified arguments {dep_args} are deprecated. ' + 'Please check the API document of new pulse drawer ' + '`qiskit.visualization.pulse_drawer_v2`.' + ''.format(dep_args=', '.join(active_legacy_args)), + DeprecationWarning) + + if filename: + warnings.warn('File saving is delegated to the plotter software in new drawer. ' + 'If you specify matplotlib plotter family to `plotter` argument, ' + 'you can call `savefig` method with the returned Figure object.', + DeprecationWarning) + + if isinstance(style, SchedStyle): + style = None + warnings.warn('Legacy stylesheet is specified. This is ignored in the new drawer. ' + 'Please check the API documentation for this method.') + + return pulse_drawer_v2(program=self, + style=style, + backend=backend, + time_range=time_range, + time_unit=time_unit, + disable_channels=disable_channels, + show_snapshot=show_snapshot, + show_framechange=show_framechange, + show_waveform_info=show_waveform_info, + show_barrier=show_barrier, + plotter=plotter, + axis=axis) + + def _interval_index(intervals: List[Interval], interval: Interval) -> int: """Find the index of an interval. diff --git a/qiskit/pulse/transforms/__init__.py b/qiskit/pulse/transforms/__init__.py new file mode 100644 index 000000000000..1189bc31953a --- /dev/null +++ b/qiskit/pulse/transforms/__init__.py @@ -0,0 +1,96 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. +""" +=================================================== +Pulse Transforms (:mod:`qiskit.pulse.transforms`) +=================================================== + +The pulse transforms provide transformation routines to reallocate and optimize +pulse programs for backends. + +Alignments +========== + +The alignment transforms define alignment policies of instructions in ``ScheduleBlock``. +These transformations are called to create ``Schedule``s from ``ScheduleBlock``s. + +.. autosummary:: + :toctree: ../stubs/ + + AlignEquispaced + AlignFunc + AlignLeft + AlignRight + AlignSequential + pad + + +Canonicalization +================ + +The canonicalization transforms convert schedules to a form amenable for execution on +Openpulse backends. + +.. autosummary:: + :toctree: ../stubs/ + + add_implicit_acquires + align_measures + block_to_schedule + compress_pulses + flatten + inline_subroutines + remove_directives + remove_trivial_barriers + + +DAG +=== + +The DAG transforms create DAG representation of input program. This can be used for +optimization of instructions and equality checks. + +.. autosummary:: + :toctree: ../stubs/ + + block_to_dag + +""" + +from qiskit.pulse.transforms.alignments import ( + AlignEquispaced, + AlignFunc, + AlignLeft, + AlignRight, + AlignSequential, + align_equispaced, + align_func, + align_left, + align_right, + align_sequential, + pad +) + +from qiskit.pulse.transforms.canonicalization import ( + add_implicit_acquires, + align_measures, + block_to_schedule, + compress_pulses, + flatten, + inline_subroutines, + remove_directives, + remove_trivial_barriers, +) + +from qiskit.pulse.transforms.dag import ( + block_to_dag +) diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py new file mode 100644 index 000000000000..b9787df1e857 --- /dev/null +++ b/qiskit/pulse/transforms/alignments.py @@ -0,0 +1,479 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. +"""A collection of passes to reallocate the timeslots of instructions according to context.""" + +import abc +from typing import Optional, Iterable, Callable, Dict, Any, Union + +import numpy as np + +from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.pulse import channels as chans, instructions +from qiskit.pulse.exceptions import PulseError +from qiskit.pulse.schedule import Schedule, ScheduleComponent +from qiskit.pulse.utils import instruction_duration_validation + + +class AlignmentKind(abc.ABC): + """An abstract class for schedule alignment.""" + is_sequential = None + + def __init__(self): + """Create new context.""" + self._context_params = tuple() + + @abc.abstractmethod + def align(self, schedule: Schedule) -> Schedule: + """Reallocate instructions according to the policy. + + Only top-level sub-schedules are aligned. If sub-schedules are nested, + nested schedules are not recursively aligned. + + Args: + schedule: Schedule to align. + + Returns: + Schedule with reallocated instructions. + """ + pass + + def to_dict(self) -> Dict[str, Any]: + """Returns dictionary to represent this alignment.""" + return {'alignment': self.__class__.__name__} + + def __eq__(self, other): + """Check equality of two transforms.""" + return isinstance(other, type(self)) and self.to_dict() == other.to_dict() + + def __repr__(self): + name = self.__class__.__name__ + opts = self.to_dict() + opts.pop('alignment') + opts_str = ', '.join(f'{key}={val}' for key, val in opts.items()) + return f'{name}({opts_str})' + + +class AlignLeft(AlignmentKind): + """Align instructions in as-soon-as-possible manner. + + Instructions are placed at earliest available timeslots. + """ + is_sequential = False + + def align(self, schedule: Schedule) -> Schedule: + """Reallocate instructions according to the policy. + + Only top-level sub-schedules are aligned. If sub-schedules are nested, + nested schedules are not recursively aligned. + + Args: + schedule: Schedule to align. + + Returns: + Schedule with reallocated instructions. + """ + aligned = Schedule() + for _, child in schedule._children: + self._push_left_append(aligned, child) + return aligned + + @staticmethod + def _push_left_append(this: Schedule, other: ScheduleComponent) -> Schedule: + """Return ``this`` with ``other`` inserted at the maximum time over + all channels shared between ```this`` and ``other``. + + Args: + this: Input schedule to which ``other`` will be inserted. + other: Other schedule to insert. + + Returns: + Push left appended schedule. + """ + this_channels = set(this.channels) + other_channels = set(other.channels) + shared_channels = list(this_channels & other_channels) + ch_slacks = [this.stop_time - this.ch_stop_time(channel) + other.ch_start_time(channel) + for channel in shared_channels] + + if ch_slacks: + slack_chan = shared_channels[np.argmin(ch_slacks)] + shared_insert_time = this.ch_stop_time(slack_chan) - other.ch_start_time(slack_chan) + else: + shared_insert_time = 0 + + # Handle case where channels not common to both might actually start + # after ``this`` has finished. + other_only_insert_time = other.ch_start_time(*(other_channels - this_channels)) + # Choose whichever is greatest. + insert_time = max(shared_insert_time, other_only_insert_time) + return this.insert(insert_time, other, inplace=True) + + +class AlignRight(AlignmentKind): + """Align instructions in as-late-as-possible manner. + + Instructions are placed at latest available timeslots. + """ + is_sequential = False + + def align(self, schedule: Schedule) -> Schedule: + """Reallocate instructions according to the policy. + + Only top-level sub-schedules are aligned. If sub-schedules are nested, + nested schedules are not recursively aligned. + + Args: + schedule: Schedule to align. + + Returns: + Schedule with reallocated instructions. + """ + aligned = Schedule() + for _, child in reversed(schedule._children): + aligned = self._push_right_prepend(aligned, child) + return aligned + + @staticmethod + def _push_right_prepend(this: ScheduleComponent, other: ScheduleComponent) -> Schedule: + """Return ``this`` with ``other`` inserted at the latest possible time + such that ``other`` ends before it overlaps with any of ``this``. + + If required ``this`` is shifted to start late enough so that there is room + to insert ``other``. + + Args: + this: Input schedule to which ``other`` will be inserted. + other: Other schedule to insert. + + Returns: + Push right prepended schedule. + """ + this_channels = set(this.channels) + other_channels = set(other.channels) + shared_channels = list(this_channels & other_channels) + ch_slacks = [this.ch_start_time(channel) - other.ch_stop_time(channel) + for channel in shared_channels] + + if ch_slacks: + insert_time = min(ch_slacks) + other.start_time + else: + insert_time = this.stop_time - other.stop_time + other.start_time + + if insert_time < 0: + this.shift(-insert_time, inplace=True) + this.insert(0, other, inplace=True) + else: + this.insert(insert_time, other, inplace=True) + + return this + + +class AlignSequential(AlignmentKind): + """Align instructions sequentially. + + Instructions played on different channels are also arranged in a sequence. + No buffer time is inserted in between instructions. + """ + is_sequential = True + + def align(self, schedule: Schedule) -> Schedule: + """Reallocate instructions according to the policy. + + Only top-level sub-schedules are aligned. If sub-schedules are nested, + nested schedules are not recursively aligned. + + Args: + schedule: Schedule to align. + + Returns: + Schedule with reallocated instructions. + """ + aligned = Schedule() + for _, child in schedule._children: + aligned.insert(aligned.duration, child, inplace=True) + return aligned + + +class AlignEquispaced(AlignmentKind): + """Align instructions with equispaced interval within a specified duration. + + Instructions played on different channels are also arranged in a sequence. + This alignment is convenient to create dynamical decoupling sequences such as PDD. + """ + is_sequential = True + + def __init__(self, duration: Union[int, ParameterExpression]): + """Create new equispaced context. + + Args: + duration: Duration of this context. This should be larger than the schedule duration. + If the specified duration is shorter than the schedule duration, + no alignment is performed and the input schedule is just returned. + This duration can be parametrized. + """ + super().__init__() + + self._context_params = (duration, ) + + def align(self, schedule: Schedule) -> Schedule: + """Reallocate instructions according to the policy. + + Only top-level sub-schedules are aligned. If sub-schedules are nested, + nested schedules are not recursively aligned. + + Args: + schedule: Schedule to align. + + Returns: + Schedule with reallocated instructions. + """ + duration = self._context_params[0] + instruction_duration_validation(duration) + + total_duration = sum([child.duration for _, child in schedule._children]) + if duration < total_duration: + return schedule + + total_delay = duration - total_duration + + if len(schedule._children) > 1: + # Calculate the interval in between sub-schedules. + # If the duration cannot be divided by the number of sub-schedules, + # the modulo is appended and prepended to the input schedule. + interval, mod = np.divmod(total_delay, len(schedule._children) - 1) + else: + interval = 0 + mod = total_delay + + # Calculate pre schedule delay + delay, mod = np.divmod(mod, 2) + + aligned = Schedule() + # Insert sub-schedules with interval + _t0 = int(aligned.stop_time + delay + mod) + for _, child in schedule._children: + aligned.insert(_t0, child, inplace=True) + _t0 = int(aligned.stop_time + interval) + + return pad(aligned, aligned.channels, until=duration, inplace=True) + + def to_dict(self) -> Dict[str, Any]: + """Returns dictionary to represent this alignment.""" + return {'alignment': self.__class__.__name__, + 'duration': self._context_params[0]} + + +class AlignFunc(AlignmentKind): + """Allocate instructions at position specified by callback function. + + The position is specified for each instruction of index ``j`` as a + fractional coordinate in [0, 1] within the specified duration. + + Instructions played on different channels are also arranged in a sequence. + This alignment is convenient to create dynamical decoupling sequences such as UDD. + + For example, UDD sequence with 10 pulses can be specified with following function. + + .. code-block:: python + + def udd10_pos(j): + return np.sin(np.pi*j/(2*10 + 2))**2 + """ + is_sequential = True + + def __init__(self, duration: Union[int, ParameterExpression], func: Callable): + """Create new equispaced context. + + Args: + duration: Duration of this context. This should be larger than the schedule duration. + If the specified duration is shorter than the schedule duration, + no alignment is performed and the input schedule is just returned. + This duration can be parametrized. + func: A function that takes an index of sub-schedule and returns the + fractional coordinate of of that sub-schedule. The returned value should be + defined within [0, 1]. The pulse index starts from 1. + """ + super().__init__() + + self._context_params = (duration, ) + self._func = func + + def align(self, schedule: Schedule) -> Schedule: + """Reallocate instructions according to the policy. + + Only top-level sub-schedules are aligned. If sub-schedules are nested, + nested schedules are not recursively aligned. + + Args: + schedule: Schedule to align. + + Returns: + Schedule with reallocated instructions. + """ + duration = self._context_params[0] + instruction_duration_validation(duration) + + if duration < schedule.duration: + return schedule + + aligned = Schedule() + for ind, (_, child) in enumerate(schedule._children): + _t_center = duration * self._func(ind + 1) + _t0 = int(_t_center - 0.5 * child.duration) + if _t0 < 0 or _t0 > duration: + PulseError('Invalid schedule position t=%d is specified at index=%d' % (_t0, ind)) + aligned.insert(_t0, child, inplace=True) + + return pad(aligned, aligned.channels, until=duration, inplace=True) + + def to_dict(self) -> Dict[str, Any]: + """Returns dictionary to represent this alignment. + + .. note:: ``func`` is not presented in this dictionary. Just name. + """ + return {'alignment': self.__class__.__name__, + 'duration': self._context_params[0], + 'func': self._func.__name__} + + +def pad(schedule: Schedule, + channels: Optional[Iterable[chans.Channel]] = None, + until: Optional[int] = None, + inplace: bool = False + ) -> Schedule: + """Pad the input Schedule with ``Delay``s on all unoccupied timeslots until + ``schedule.duration`` or ``until`` if not ``None``. + + Args: + schedule: Schedule to pad. + channels: Channels to pad. Defaults to all channels in + ``schedule`` if not provided. If the supplied channel is not a member + of ``schedule`` it will be added. + until: Time to pad until. Defaults to ``schedule.duration`` if not provided. + inplace: Pad this schedule by mutating rather than returning a new schedule. + + Returns: + The padded schedule. + """ + until = until or schedule.duration + channels = channels or schedule.channels + + for channel in channels: + if channel not in schedule.channels: + schedule |= instructions.Delay(until, channel) + continue + + curr_time = 0 + # Use the copy of timeslots. When a delay is inserted before the current interval, + # current timeslot is pointed twice and the program crashes with the wrong pointer index. + timeslots = schedule.timeslots[channel].copy() + # TODO: Replace with method of getting instructions on a channel + for interval in timeslots: + if curr_time >= until: + break + if interval[0] != curr_time: + end_time = min(interval[0], until) + schedule = schedule.insert( + curr_time, + instructions.Delay(end_time - curr_time, channel), + inplace=inplace) + curr_time = interval[1] + if curr_time < until: + schedule = schedule.insert( + curr_time, + instructions.Delay(until - curr_time, channel), + inplace=inplace) + + return schedule + + +def align_left(schedule: Schedule) -> Schedule: + """Align a list of pulse instructions on the left. + + Args: + schedule: Input schedule of which top-level sub-schedules will be rescheduled. + + Returns: + New schedule with input `schedule`` child schedules and instructions + left aligned. + """ + context = AlignLeft() + return context.align(schedule) + + +def align_right(schedule: Schedule) -> Schedule: + """Align a list of pulse instructions on the right. + + Args: + schedule: Input schedule of which top-level sub-schedules will be rescheduled. + + Returns: + New schedule with input `schedule`` child schedules and instructions + right aligned. + """ + context = AlignRight() + return context.align(schedule) + + +def align_sequential(schedule: Schedule) -> Schedule: + """Schedule all top-level nodes in parallel. + + Args: + schedule: Input schedule of which top-level sub-schedules will be rescheduled. + + Returns: + New schedule with input `schedule`` child schedules and instructions + applied sequentially across channels + """ + context = AlignSequential() + return context.align(schedule) + + +def align_equispaced(schedule: Schedule, duration: int) -> Schedule: + """Schedule a list of pulse instructions with equivalent interval. + + Args: + schedule: Input schedule of which top-level sub-schedules will be rescheduled. + duration: Duration of context. This should be larger than the schedule duration. + + Returns: + New schedule with input `schedule`` child schedules and instructions + aligned with equivalent interval. + + Notes: + This context is convenient for writing PDD or Hahn echo sequence for example. + """ + context = AlignEquispaced(duration=duration) + return context.align(schedule) + + +def align_func(schedule: Schedule, duration: int, func: Callable[[int], float]) -> Schedule: + """Schedule a list of pulse instructions with schedule position defined by the + numerical expression. + + Args: + schedule: Input schedule of which top-level sub-schedules will be rescheduled. + duration: Duration of context. This should be larger than the schedule duration. + func: A function that takes an index of sub-schedule and returns the + fractional coordinate of of that sub-schedule. + The returned value should be defined within [0, 1]. + The pulse index starts from 1. + + Returns: + New schedule with input `schedule`` child schedules and instructions + aligned with equivalent interval. + + Notes: + This context is convenient for writing UDD sequence for example. + """ + context = AlignFunc(duration=duration, func=func) + return context.align(schedule) diff --git a/qiskit/pulse/transforms.py b/qiskit/pulse/transforms/canonicalization.py similarity index 54% rename from qiskit/pulse/transforms.py rename to qiskit/pulse/transforms/canonicalization.py index d032237dbe07..43aab220da0c 100644 --- a/qiskit/pulse/transforms.py +++ b/qiskit/pulse/transforms/canonicalization.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019. +# (C) Copyright IBM 2021. # # 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 @@ -9,26 +9,203 @@ # 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. +"""Basic rescheduling functions which take schedule or instructions and return new schedules.""" -"""Basic rescheduling functions which take schedules or instructions -(and possibly some arguments) and return new schedules. -""" import warnings from collections import defaultdict -from copy import deepcopy -from typing import Callable from typing import List, Optional, Iterable, Union import numpy as np from qiskit.pulse import channels as chans, exceptions, instructions from qiskit.pulse.exceptions import PulseError +from qiskit.pulse.exceptions import UnassignedDurationError from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.pulse.instructions import directives -from qiskit.pulse.schedule import Schedule, ScheduleComponent +from qiskit.pulse.schedule import Schedule, ScheduleBlock, ScheduleComponent -def align_measures(schedules: Iterable[Union['Schedule', instructions.Instruction]], +def block_to_schedule(block: ScheduleBlock) -> Schedule: + """Convert ``ScheduleBlock`` to ``Schedule``. + + Args: + block: A ``ScheduleBlock`` to convert. + + Returns: + Scheduled pulse program. + + Raises: + UnassignedDurationError: When any instruction duration is not assigned. + """ + if not block.is_schedulable(): + raise UnassignedDurationError( + 'All instruction durations should be assigned before creating `Schedule`.' + 'Please check `.parameters` to find unassigned parameter objects.') + + schedule = Schedule(name=block.name, metadata=block.metadata) + for op_data in block.instructions: + if isinstance(op_data, ScheduleBlock): + context_schedule = block_to_schedule(op_data) + schedule.append(context_schedule, inplace=True) + else: + schedule.append(op_data, inplace=True) + + # transform with defined policy + return block.alignment_context.align(schedule) + + +def compress_pulses(schedules: List[Schedule]) -> List[Schedule]: + """Optimization pass to replace identical pulses. + + Args: + schedules: Schedules to compress. + + Returns: + Compressed schedules. + """ + existing_pulses = [] + new_schedules = [] + + for schedule in schedules: + new_schedule = Schedule(name=schedule.name, metadata=schedule.metadata) + + for time, inst in schedule.instructions: + if isinstance(inst, instructions.Play): + if inst.pulse in existing_pulses: + idx = existing_pulses.index(inst.pulse) + identical_pulse = existing_pulses[idx] + new_schedule.insert(time, + instructions.Play(identical_pulse, + inst.channel, + inst.name), + inplace=True) + else: + existing_pulses.append(inst.pulse) + new_schedule.insert(time, inst, inplace=True) + else: + new_schedule.insert(time, inst, inplace=True) + + new_schedules.append(new_schedule) + + return new_schedules + + +def flatten(program: Schedule) -> Schedule: + """Flatten (inline) any called nodes into a Schedule tree with no nested children. + + Args: + program: Pulse program to remove nested structure. + + Returns: + Flatten pulse program. + + Raises: + PulseError: When invalid data format is given. + """ + if isinstance(program, Schedule): + return Schedule(*program.instructions, name=program.name, metadata=program.metadata) + else: + raise PulseError(f'Invalid input program {program.__class__.__name__} is specified.') + + +def inline_subroutines(program: Union[Schedule, ScheduleBlock]) -> Union[Schedule, ScheduleBlock]: + """Recursively remove call instructions and inline the respective subroutine instructions. + + Assigned parameter values, which are stored in the parameter table, are also applied. + The subroutine is copied before the parameter assignment to avoid mutation problem. + + Args: + program: A program which may contain the subroutine, i.e. ``Call`` instruction. + + Returns: + A schedule without subroutine. + + Raises: + PulseError: When input program is not valid data format. + """ + if isinstance(program, Schedule): + return _inline_schedule(program) + elif isinstance(program, ScheduleBlock): + return _inline_block(program) + else: + raise PulseError(f'Invalid program {program.__class__.__name__} is specified.') + + +def _inline_schedule(schedule: Schedule) -> Schedule: + """A helper function to inline subroutine of schedule. + + .. note:: If subroutine is ``ScheduleBlock`` it is converted into Schedule to get ``t0``. + """ + ret_schedule = Schedule(name=schedule.name, + metadata=schedule.metadata) + for t0, inst in schedule.instructions: + if isinstance(inst, instructions.Call): + # bind parameter + subroutine = inst.assigned_subroutine() + # convert into schedule if block is given + if isinstance(subroutine, ScheduleBlock): + subroutine = block_to_schedule(subroutine) + # recursively inline the program + inline_schedule = _inline_schedule(subroutine) + ret_schedule.insert(t0, inline_schedule, inplace=True) + else: + ret_schedule.insert(t0, inst, inplace=True) + return ret_schedule + + +def _inline_block(block: ScheduleBlock) -> ScheduleBlock: + """A helper function to inline subroutine of schedule block. + + .. note:: If subroutine is ``Schedule`` the function raises an error. + """ + ret_block = ScheduleBlock(alignment_context=block.alignment_context, + name=block.name, + metadata=block.metadata) + for inst in block.instructions: + if isinstance(inst, instructions.Call): + # bind parameter + subroutine = inst.assigned_subroutine() + if isinstance(subroutine, Schedule): + raise PulseError(f'A subroutine {subroutine.name} is a pulse Schedule. ' + 'This program cannot be inserted into ScheduleBlock because ' + 't0 associated with instruction will be lost.') + # recursively inline the program + inline_block = _inline_block(subroutine) + ret_block.append(inline_block, inplace=True) + else: + ret_block.append(inst, inplace=True) + return ret_block + + +def remove_directives(schedule: Schedule) -> Schedule: + """Remove directives. + + Args: + schedule: A schedule to remove compiler directives. + + Returns: + A schedule without directives. + """ + return schedule.exclude(instruction_types=[directives.Directive]) + + +def remove_trivial_barriers(schedule: Schedule) -> Schedule: + """Remove trivial barriers with 0 or 1 channels. + + Args: + schedule: A schedule to remove trivial barriers. + + Returns: + schedule: A schedule without trivial barriers + """ + def filter_func(inst): + return (isinstance(inst[1], directives.RelativeBarrier) and + len(inst[1].channels) < 2) + + return schedule.exclude(filter_func) + + +def align_measures(schedules: Iterable[ScheduleComponent], inst_map: Optional[InstructionScheduleMap] = None, cal_gate: str = 'u3', max_calibration_duration: Optional[int] = None, @@ -180,7 +357,7 @@ def get_max_calibration_duration(inst_map, cal_gate): return new_schedules -def add_implicit_acquires(schedule: Union['Schedule', instructions.Instruction], +def add_implicit_acquires(schedule: ScheduleComponent, meas_map: List[List[int]] ) -> Schedule: """Return a new schedule with implicit acquires from the measurement mapping replaced by @@ -228,344 +405,3 @@ def add_implicit_acquires(schedule: Union['Schedule', instructions.Instruction], new_schedule.insert(time, inst, inplace=True) return new_schedule - - -def pad(schedule: Schedule, - channels: Optional[Iterable[chans.Channel]] = None, - until: Optional[int] = None, - inplace: bool = False - ) -> Schedule: - r"""Pad the input Schedule with ``Delay``\s on all unoccupied timeslots until - ``schedule.duration`` or ``until`` if not ``None``. - - Args: - schedule: Schedule to pad. - channels: Channels to pad. Defaults to all channels in - ``schedule`` if not provided. If the supplied channel is not a member - of ``schedule`` it will be added. - until: Time to pad until. Defaults to ``schedule.duration`` if not provided. - inplace: Pad this schedule by mutating rather than returning a new schedule. - - Returns: - The padded schedule. - """ - until = until or schedule.duration - channels = channels or schedule.channels - - for channel in channels: - if channel not in schedule.channels: - schedule |= instructions.Delay(until, channel) - continue - - curr_time = 0 - # Use the copy of timeslots. When a delay is inserted before the current interval, - # current timeslot is pointed twice and the program crashes with the wrong pointer index. - timeslots = schedule.timeslots[channel].copy() - # TODO: Replace with method of getting instructions on a channel - for interval in timeslots: - if curr_time >= until: - break - if interval[0] != curr_time: - end_time = min(interval[0], until) - schedule = schedule.insert( - curr_time, - instructions.Delay(end_time - curr_time, channel), - inplace=inplace) - curr_time = interval[1] - if curr_time < until: - schedule = schedule.insert( - curr_time, - instructions.Delay(until - curr_time, channel), - inplace=inplace) - - return schedule - - -def compress_pulses(schedules: List[Schedule]) -> List[Schedule]: - """Optimization pass to replace identical pulses. - - Args: - schedules: Schedules to compress. - - Returns: - Compressed schedules. - """ - - existing_pulses = [] - new_schedules = [] - - for schedule in schedules: - new_schedule = Schedule(name=schedule.name, metadata=schedule.metadata) - - for time, inst in schedule.instructions: - if isinstance(inst, instructions.Play): - if inst.pulse in existing_pulses: - idx = existing_pulses.index(inst.pulse) - identical_pulse = existing_pulses[idx] - new_schedule.insert(time, - instructions.Play(identical_pulse, - inst.channel, - inst.name), - inplace=True) - else: - existing_pulses.append(inst.pulse) - new_schedule.insert(time, inst, inplace=True) - else: - new_schedule.insert(time, inst, inplace=True) - - new_schedules.append(new_schedule) - - return new_schedules - - -def _push_left_append(this: Schedule, - other: Union['Schedule', instructions.Instruction], - ) -> Schedule: - r"""Return ``this`` with ``other`` inserted at the maximum time over - all channels shared between ```this`` and ``other``. - - Args: - this: Input schedule to which ``other`` will be inserted. - other: Other schedule to insert. - - Returns: - Push left appended schedule. - """ - this_channels = set(this.channels) - other_channels = set(other.channels) - shared_channels = list(this_channels & other_channels) - ch_slacks = [this.stop_time - this.ch_stop_time(channel) + other.ch_start_time(channel) - for channel in shared_channels] - - if ch_slacks: - slack_chan = shared_channels[np.argmin(ch_slacks)] - shared_insert_time = this.ch_stop_time(slack_chan) - other.ch_start_time(slack_chan) - else: - shared_insert_time = 0 - - # Handle case where channels not common to both might actually start - # after ``this`` has finished. - other_only_insert_time = other.ch_start_time(*(other_channels - this_channels)) - # Choose whichever is greatest. - insert_time = max(shared_insert_time, other_only_insert_time) - return this.insert(insert_time, other, inplace=True) - - -def align_left(schedule: Schedule) -> Schedule: - """Align a list of pulse instructions on the left. - - Args: - schedule: Input schedule of which top-level ``child`` nodes will be - rescheduled. - - Returns: - New schedule with input `schedule`` child schedules and instructions - left aligned. - """ - aligned = Schedule() - for _, child in schedule._children: - _push_left_append(aligned, child) - return aligned - - -def _push_right_prepend(this: Union['Schedule', instructions.Instruction], - other: Union['Schedule', instructions.Instruction], - ) -> Schedule: - r"""Return ``this`` with ``other`` inserted at the latest possible time - such that ``other`` ends before it overlaps with any of ``this``. - - If required ``this`` is shifted to start late enough so that there is room - to insert ``other``. - - Args: - this: Input schedule to which ``other`` will be inserted. - other: Other schedule to insert. - - Returns: - Push right prepended schedule. - """ - this_channels = set(this.channels) - other_channels = set(other.channels) - shared_channels = list(this_channels & other_channels) - ch_slacks = [this.ch_start_time(channel) - other.ch_stop_time(channel) - for channel in shared_channels] - - if ch_slacks: - insert_time = min(ch_slacks) + other.start_time - else: - insert_time = this.stop_time - other.stop_time + other.start_time - - if insert_time < 0: - this.shift(-insert_time, inplace=True) - this.insert(0, other, inplace=True) - else: - this.insert(insert_time, other, inplace=True) - - return this - - -def align_right(schedule: Schedule) -> Schedule: - """Align a list of pulse instructions on the right. - - Args: - schedule: Input schedule of which top-level ``child`` nodes will be - rescheduled. - - Returns: - New schedule with input `schedule`` child schedules and instructions - right aligned. - """ - aligned = Schedule() - for _, child in reversed(schedule._children): - aligned = _push_right_prepend(aligned, child) - return aligned - - -def align_sequential(schedule: Schedule) -> Schedule: - """Schedule all top-level nodes in parallel. - - Args: - schedule: Input schedule of which top-level ``child`` nodes will be - rescheduled. - - Returns: - New schedule with input `schedule`` child schedules and instructions - applied sequentially across channels - """ - aligned = Schedule() - for _, child in schedule._children: - aligned.insert(aligned.duration, child, inplace=True) - return aligned - - -def align_equispaced(schedule: Schedule, - duration: int) -> Schedule: - """Schedule a list of pulse instructions with equivalent interval. - - Args: - schedule: Input schedule of which top-level ``child`` nodes will be - rescheduled. - duration: Duration of context. This should be larger than the schedule duration. - - Returns: - New schedule with input `schedule`` child schedules and instructions - aligned with equivalent interval. - - Notes: - This context is convenient for writing PDD or Hahn echo sequence for example. - """ - total_duration = sum([child.duration for _, child in schedule._children]) - if duration and duration < total_duration: - return schedule - - total_delay = duration - total_duration - - if len(schedule._children) > 1: - # Calculate the interval in between sub-schedules. - # If the duration cannot be divided by the number of sub-schedules, - # the modulo is appended and prepended to the input schedule. - interval, mod = np.divmod(total_delay, len(schedule._children) - 1) - else: - interval = 0 - mod = total_delay - - # Calculate pre schedule delay - delay, mod = np.divmod(mod, 2) - - aligned = Schedule() - # Insert sub-schedules with interval - _t0 = int(aligned.stop_time + delay + mod) - for _, child in schedule._children: - aligned.insert(_t0, child, inplace=True) - _t0 = int(aligned.stop_time + interval) - - return pad(aligned, aligned.channels, until=duration, inplace=True) - - -def align_func(schedule: Schedule, - duration: int, - func: Callable[[int], float]) -> Schedule: - """Schedule a list of pulse instructions with schedule position defined by the - numerical expression. - - Args: - schedule: Input schedule of which top-level ``child`` nodes will be - rescheduled. - duration: Duration of context. This should be larger than the schedule duration. - func: A function that takes an index of sub-schedule and returns the - fractional coordinate of of that sub-schedule. - The returned value should be defined within [0, 1]. - The pulse index starts from 1. - - Returns: - New schedule with input `schedule`` child schedules and instructions - aligned with equivalent interval. - - Notes: - This context is convenient for writing UDD sequence for example. - """ - if duration < schedule.duration: - return schedule - - aligned = Schedule() - for ind, (_, child) in enumerate(schedule._children): - _t_center = duration * func(ind + 1) - _t0 = int(_t_center - 0.5 * child.duration) - if _t0 < 0 or _t0 > duration: - PulseError('Invalid schedule position t=%d is specified at index=%d' % (_t0, ind)) - aligned.insert(_t0, child, inplace=True) - - return pad(aligned, aligned.channels, until=duration, inplace=True) - - -def flatten(program: ScheduleComponent) -> ScheduleComponent: - """Flatten (inline) any called nodes into a Schedule tree with no nested children.""" - if isinstance(program, instructions.Instruction): - return program - else: - return Schedule(*program.instructions, - name=program.name, - metadata=program.metadata) - - -def inline_subroutines(program: Schedule) -> Schedule: - """Recursively remove call instructions and inline the respective subroutine instructions. - - Assigned parameter values, which are stored in the parameter table, are also applied. - The subroutine is copied before the parameter assignment to avoid mutation problem. - - Args: - program: A program which may contain the subroutine, i.e. ``Call`` instruction. - - Returns: - A schedule without subroutine. - """ - schedule = Schedule(name=program.name, metadata=program.metadata) - for t0, inst in program.instructions: - if isinstance(inst, instructions.Call): - # bind parameter - if bool(inst.arguments): - subroutine = deepcopy(inst.subroutine) - subroutine.assign_parameters(value_dict=inst.arguments) - else: - subroutine = inst.subroutine - # recursively inline the program - inline_schedule = inline_subroutines(subroutine) - schedule.insert(t0, inline_schedule, inplace=True) - else: - schedule.insert(t0, inst, inplace=True) - return schedule - - -def remove_directives(schedule: Schedule) -> Schedule: - """Remove directives.""" - return schedule.exclude(instruction_types=[directives.Directive]) - - -def remove_trivial_barriers(schedule: Schedule) -> Schedule: - """Remove trivial barriers with 0 or 1 channels.""" - def filter_func(inst): - return (isinstance(inst[1], directives.RelativeBarrier) and - len(inst[1].channels) < 2) - - return schedule.exclude(filter_func) diff --git a/qiskit/pulse/transforms/dag.py b/qiskit/pulse/transforms/dag.py new file mode 100644 index 000000000000..96b4b7337b31 --- /dev/null +++ b/qiskit/pulse/transforms/dag.py @@ -0,0 +1,99 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. +"""A collection of functions to convert ScheduleBlock to DAG representation.""" + +import retworkx as rx + +from qiskit.pulse.schedule import ScheduleBlock + + +def block_to_dag(block: ScheduleBlock) -> rx.PyDAG: + """Convert schedule block instruction into DAG. + + ``ScheduleBlock`` can be represented as a DAG as needed. + For example, equality of two programs are efficiently checked on DAG representation. + + .. code-block:: python + + with pulse.build() as sched1: + with pulse.align_left(): + pulse.play(my_gaussian0, pulse.DriveChannel(0)) + pulse.shift_phase(1.57, pulse.DriveChannel(2)) + pulse.play(my_gaussian1, pulse.DriveChannel(1)) + + with pulse.build() as sched2: + with pulse.align_left(): + pulse.shift_phase(1.57, pulse.DriveChannel(2)) + pulse.play(my_gaussian1, pulse.DriveChannel(1)) + pulse.play(my_gaussian0, pulse.DriveChannel(0)) + + Here the ``sched1 `` and ``sched2`` are different implementations of the same program, + but it is difficult to confirm on the list representation. + + Another example is instruction optimization. + + .. code-block:: python + + with pulse.build() as sched: + with pulse.align_left(): + pulse.shift_phase(1.57, pulse.DriveChannel(1)) + pulse.play(my_gaussian0, pulse.DriveChannel(0)) + pulse.shift_phase(-1.57, pulse.DriveChannel(1)) + + In above program two ``shift_phase`` instructions can be cancelled out because + they are consecutive on the same drive channel. + This can be easily found on the DAG representation. + + Args: + block: A schedule block to be converted. + + Returns: + Instructions in DAG representation. + """ + if block.alignment_context.is_sequential: + return _sequential_allocation(block) + else: + return _parallel_allocation(block) + + +def _sequential_allocation(block: ScheduleBlock) -> rx.PyDAG: + """A helper function to create a DAG of a sequential alignment context.""" + dag_instructions = rx.PyDAG() + + prev_node = None + edges = [] + for inst in block.instructions: + current_node = dag_instructions.add_node(inst) + if prev_node is not None: + edges.append((prev_node, current_node)) + prev_node = current_node + dag_instructions.add_edges_from_no_data(edges) + + return dag_instructions + + +def _parallel_allocation(block: ScheduleBlock) -> rx.PyDAG: + """A helper function to create a DAG of a parallel alignment context.""" + dag_instructions = rx.PyDAG() + + slots = dict() + edges = [] + for inst in block.instructions: + current_node = dag_instructions.add_node(inst) + for chan in inst.channels: + prev_node = slots.pop(chan, None) + if prev_node is not None: + edges.append((prev_node, current_node)) + slots[chan] = current_node + dag_instructions.add_edges_from_no_data(edges) + + return dag_instructions diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py index 30cc4feb84be..731adc64683a 100644 --- a/qiskit/pulse/utils.py +++ b/qiskit/pulse/utils.py @@ -12,6 +12,7 @@ """Module for common pulse programming utilities.""" import functools +import warnings from typing import List, Dict, Union import numpy as np @@ -95,3 +96,17 @@ def instruction_duration_validation(duration: int): raise QiskitError( 'Instruction duration must be a non-negative integer, ' 'got {} instead.'.format(duration)) + + +def deprecated_functionality(func): + """A decorator that raises deprecation warning without showing alternative method.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn(f'Calling {func.__name__} is being deprecated and will be removed soon. ' + 'No alternative method will be provided with this change. ' + 'If there is any practical usage of this functionality, please write ' + 'an issue in Qiskit/qiskit-terra repository.', + category=DeprecationWarning, + stacklevel=2) + return func(*args, **kwargs) + return wrapper diff --git a/qiskit/visualization/pulse_v2/events.py b/qiskit/visualization/pulse_v2/events.py index de361c79e92a..fb66174bafd2 100644 --- a/qiskit/visualization/pulse_v2/events.py +++ b/qiskit/visualization/pulse_v2/events.py @@ -19,9 +19,10 @@ The `ChannelEvents` class is expected to be called by other programs (not by end-users). The `ChannelEvents` class instance is created with the class method ``load_program``: - ```python + +.. code-block:: python + event = ChannelEvents.load_program(sched, DriveChannel(0)) - ``` The `ChannelEvents` is created for a specific pulse channel and loosely assorts pulse instructions within the channel with different visualization purposes. @@ -31,13 +32,14 @@ Instructions that have finite duration are grouped as waveforms. The grouped instructions are returned as an iterator by the corresponding method call: - ```python + +.. code-block:: python + for t0, frame, instruction in event.get_waveforms(): ... for t0, frame_change, instructions in event.get_frame_changes(): ... - ``` The class method ``get_waveforms`` returns the iterator of waveform type instructions with the ``PhaseFreqTuple`` (frame) at the time when instruction is issued. @@ -56,7 +58,9 @@ the set type instruction will be converted into the relevant shift amount for visualization. Note that these instructions are not interchangeable and the order should be kept. For example: - ```python + +.. code-block:: python + sched1 = Schedule() sched1 = sched1.insert(0, ShiftPhase(-1.57, DriveChannel(0)) sched1 = sched1.insert(0, SetPhase(3.14, DriveChannel(0)) @@ -64,7 +68,7 @@ sched2 = Schedule() sched2 = sched2.insert(0, SetPhase(3.14, DriveChannel(0)) sched2 = sched2.insert(0, ShiftPhase(-1.57, DriveChannel(0)) - ``` + In this example, ``sched1`` and ``sched2`` will have different frames. On the drawer canvas, the total frame change amount of +3.14 should be shown for ``sched1``, while ``sched2`` is +1.57. Since the `SetPhase` and the `ShiftPhase` instruction behave @@ -185,7 +189,7 @@ def get_waveforms(self) -> Iterator[PulseInstruction]: # Check if pulse has unbound parameters if isinstance(inst, pulse.Play): - is_opaque = inst.is_parameterized() + is_opaque = inst.pulse.is_parameterized() yield PulseInstruction(t0, self._dt, frame, inst, is_opaque) diff --git a/qiskit/visualization/pulse_v2/interface.py b/qiskit/visualization/pulse_v2/interface.py index 6e254fce94e6..80082db4a37f 100644 --- a/qiskit/visualization/pulse_v2/interface.py +++ b/qiskit/visualization/pulse_v2/interface.py @@ -24,13 +24,14 @@ from typing import Union, Optional, Dict, Any, Tuple, List from qiskit.providers import BaseBackend -from qiskit.pulse import Waveform, ParametricPulse, Schedule +from qiskit.pulse import Waveform, ParametricPulse, Schedule, ScheduleBlock +from qiskit.pulse.transforms import block_to_schedule from qiskit.pulse.channels import Channel from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.pulse_v2 import core, device_info, stylesheet, types -def draw(program: Union[Waveform, ParametricPulse, Schedule], +def draw(program: Union[Waveform, ParametricPulse, Schedule, ScheduleBlock], style: Optional[Dict[str, Any]] = None, backend: Optional[BaseBackend] = None, time_range: Optional[Tuple[int, int]] = None, @@ -372,6 +373,9 @@ def draw(program: Union[Waveform, ParametricPulse, Schedule], ImportError: When required visualization package is not installed. VisualizationError: When invalid plotter API or invalid time range is specified. """ + if isinstance(program, ScheduleBlock): + program = block_to_schedule(program) + temp_style = stylesheet.QiskitPulseStyle() temp_style.update(style or stylesheet.IQXStandard()) diff --git a/releasenotes/notes/add-schedule-block-c37527f3205b7b62.yaml b/releasenotes/notes/add-schedule-block-c37527f3205b7b62.yaml new file mode 100644 index 000000000000..1f624217b62d --- /dev/null +++ b/releasenotes/notes/add-schedule-block-c37527f3205b7b62.yaml @@ -0,0 +1,48 @@ +--- +features: + - | + A new pulse program representation :py:class:`~qiskit.pulse.ScheduleBlock` has been added. + This representation is best suited for the pulse builder syntax and is based on + relative instruction ordering. + + This representation takes ``alignment_context`` instead of specifying starting time ``t0`` for + each instruction. The start time of instruction is implicitly allocated with + the specified transformation and relative position of instructions. + + This representation allows lazy instruction scheduling, meaning we can assign + arbitrary parameters to the duration of instructions. + + For example, + + .. code-block:: python + + from qiskit.pulse import ScheduleBlock, DriveChannel, Gaussian + from qiskit.pulse.instructions import Play, Call + from qiskit.pulse.transforms import AlignRight + from qiskit.circuit import Parameter + + dur = Parameter('rabi_duration') + + block = ScheduleBlock(alignment_context=AlignRight()) + block += Play(Gaussian(dur, 0.1, dur/4), DriveChannel(0)) + block += Call(measure_sched) # subroutine defined elsewhere + + this code defines an experiment scanning a Gaussian pulse's duration followed by + a measurement ``measure_sched``, i.e. a Rabi experiment. + You can reuse the ``block`` object for every scanned duration + by assigning a target duration value. +deprecations: + - | + :meth:`assign_parameters` is being deprecated from :py:class:`~qiskit.pulse.channels.Channel`, + :py:class:`~qiskit.pulse.library.Pulse`, :py:class:`~qiskit.pulse.instructions.Instruction`, + and their subclasses. :meth:`parameters` is also being deprecated from + :py:class:`~qiskit.pulse.channels.Channel` and + :py:class:`~qiskit.pulse.instructions.Instruction`. +upgrade: + - | + In pulse programs, the parameter framework is replaced with the visitor pattern. + This removes the parameter management logic from each object consisting a program, + yielding performance improvements of object construction with pulse programming. + Parameter management logic is newly implemented in + :py:mod:`~qiskit.pulse.parameter_manager`. This upgrade may influence code developers who + are providing custom pulse instruction based on Qiskit Pulse. diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py new file mode 100644 index 000000000000..b55f4984619b --- /dev/null +++ b/test/python/pulse/test_block.py @@ -0,0 +1,775 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +# pylint: disable=invalid-name + +"""Test cases for the pulse schedule block.""" + +from qiskit import pulse, circuit +from qiskit.pulse import transforms +from qiskit.pulse.exceptions import PulseError +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeOpenPulse2Q + + +class BaseTestBlock(QiskitTestCase): + """ScheduleBlock tests.""" + + def setUp(self): + super().setUp() + + self.backend = FakeOpenPulse2Q() + + self.test_waveform0 = pulse.Constant(100, 0.1) + self.test_waveform1 = pulse.Constant(200, 0.1) + + self.d0 = pulse.DriveChannel(0) + self.d1 = pulse.DriveChannel(1) + + self.left_context = transforms.AlignLeft() + self.right_context = transforms.AlignRight() + self.sequential_context = transforms.AlignSequential() + self.equispaced_context = transforms.AlignEquispaced(duration=1000) + + def _align_func(j): + return {1: 0.1, 2: 0.25, 3: 0.7, 4: 0.85}.get(j) + self.func_context = transforms.AlignFunc(duration=1000, func=_align_func) + + def assertScheduleEqual(self, target, reference): + """Check if two block are equal schedule representation.""" + self.assertEqual(transforms.block_to_schedule(target), reference) + + +class TestTransformation(BaseTestBlock): + """Test conversion of ScheduleBlock to Schedule.""" + + def test_left_alignment(self): + """Test left alignment context.""" + block = pulse.ScheduleBlock(alignment_context=self.left_context) + block = block.append(pulse.Play(self.test_waveform0, self.d0)) + block = block.append(pulse.Play(self.test_waveform1, self.d1)) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) + + self.assertScheduleEqual(block, ref_sched) + + def test_right_alignment(self): + """Test right alignment context.""" + block = pulse.ScheduleBlock(alignment_context=self.right_context) + block = block.append(pulse.Play(self.test_waveform0, self.d0)) + block = block.append(pulse.Play(self.test_waveform1, self.d1)) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) + + self.assertScheduleEqual(block, ref_sched) + + def test_sequential_alignment(self): + """Test sequential alignment context.""" + block = pulse.ScheduleBlock(alignment_context=self.sequential_context) + block = block.append(pulse.Play(self.test_waveform0, self.d0)) + block = block.append(pulse.Play(self.test_waveform1, self.d1)) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform1, self.d1)) + + self.assertScheduleEqual(block, ref_sched) + + def test_equispace_alignment(self): + """Test equispace alignment context.""" + block = pulse.ScheduleBlock(alignment_context=self.equispaced_context) + for _ in range(4): + block = block.append(pulse.Play(self.test_waveform0, self.d0)) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(100, pulse.Delay(200, self.d0)) + ref_sched = ref_sched.insert(300, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(400, pulse.Delay(200, self.d0)) + ref_sched = ref_sched.insert(600, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(700, pulse.Delay(200, self.d0)) + ref_sched = ref_sched.insert(900, pulse.Play(self.test_waveform0, self.d0)) + + self.assertScheduleEqual(block, ref_sched) + + def test_func_alignment(self): + """Test func alignment context.""" + block = pulse.ScheduleBlock(alignment_context=self.func_context) + for _ in range(4): + block = block.append(pulse.Play(self.test_waveform0, self.d0)) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Delay(50, self.d0)) + ref_sched = ref_sched.insert(50, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(150, pulse.Delay(50, self.d0)) + ref_sched = ref_sched.insert(200, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(300, pulse.Delay(350, self.d0)) + ref_sched = ref_sched.insert(650, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(750, pulse.Delay(50, self.d0)) + ref_sched = ref_sched.insert(800, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(900, pulse.Delay(100, self.d0)) + + self.assertScheduleEqual(block, ref_sched) + + def test_nested_alignment(self): + """Test nested block scheduling.""" + block_sub = pulse.ScheduleBlock(alignment_context=self.right_context) + block_sub = block_sub.append(pulse.Play(self.test_waveform0, self.d0)) + block_sub = block_sub.append(pulse.Play(self.test_waveform1, self.d1)) + + block_main = pulse.ScheduleBlock(alignment_context=self.sequential_context) + block_main = block_main.append(block_sub) + block_main = block_main.append(pulse.Delay(10, self.d0)) + block_main = block_main.append(block_sub) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) + ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(200, pulse.Delay(10, self.d0)) + ref_sched = ref_sched.insert(210, pulse.Play(self.test_waveform1, self.d1)) + ref_sched = ref_sched.insert(310, pulse.Play(self.test_waveform0, self.d0)) + + self.assertScheduleEqual(block_main, ref_sched) + + +class TestBlockOperation(BaseTestBlock): + """Test fundamental operation on schedule block. + + Because ScheduleBlock adapts to the lazy scheduling, no uniitest for + overlap constraints is necessary. Test scheme becomes simpler than the schedule. + + Some tests have dependency on schedule conversion. + This operation should be tested in `test.python.pulse.test_block.TestTransformation`. + """ + def setUp(self): + super().setUp() + + self.test_blocks = [ + pulse.Play(self.test_waveform0, self.d0), + pulse.Play(self.test_waveform1, self.d1), + pulse.Delay(50, self.d0), + pulse.Play(self.test_waveform1, self.d0) + ] + + def test_append_an_instruction_to_empty_block(self): + """Test append instructions to an empty block.""" + block = pulse.ScheduleBlock() + block = block.append(pulse.Play(self.test_waveform0, self.d0)) + + self.assertEqual(block.instructions[0], pulse.Play(self.test_waveform0, self.d0)) + + def test_append_an_instruction_to_empty_block_sugar(self): + """Test append instructions to an empty block with syntax sugar.""" + block = pulse.ScheduleBlock() + block += pulse.Play(self.test_waveform0, self.d0) + + self.assertEqual(block.instructions[0], pulse.Play(self.test_waveform0, self.d0)) + + def test_append_an_instruction_to_empty_block_inplace(self): + """Test append instructions to an empty block with inplace.""" + block = pulse.ScheduleBlock() + block.append(pulse.Play(self.test_waveform0, self.d0), inplace=True) + + self.assertEqual(block.instructions[0], pulse.Play(self.test_waveform0, self.d0)) + + def test_append_a_block_to_empty_block(self): + """Test append another ScheduleBlock to empty block.""" + block = pulse.ScheduleBlock() + block.append(pulse.Play(self.test_waveform0, self.d0), inplace=True) + + block_main = pulse.ScheduleBlock() + block_main = block_main.append(block) + + self.assertEqual(block_main.instructions[0], block) + + def test_append_an_instruction_to_block(self): + """Test append instructions to a non-empty block.""" + block = pulse.ScheduleBlock() + block = block.append(pulse.Delay(100, self.d0)) + + block = block.append(pulse.Delay(100, self.d0)) + + self.assertEqual(len(block.instructions), 2) + + def test_append_an_instruction_to_block_inplace(self): + """Test append instructions to a non-empty block with inplace.""" + block = pulse.ScheduleBlock() + block = block.append(pulse.Delay(100, self.d0)) + + block.append(pulse.Delay(100, self.d0), inplace=True) + + self.assertEqual(len(block.instructions), 2) + + def test_duration(self): + """Test if correct duration is returned with implicit scheduling.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.duration, 350) + + def test_timeslots(self): + """Test if correct timeslot is returned with implicit scheduling.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + ref_slots = { + self.d0: [(0, 100), (100, 150), (150, 350)], + self.d1: [(0, 200)] + } + + self.assertDictEqual(block.timeslots, ref_slots) + + def test_start_time(self): + """Test if correct schedule start time is returned with implicit scheduling.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.start_time, 0) + + def test_stop_time(self): + """Test if correct schedule stop time is returned with implicit scheduling.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.stop_time, 350) + + def test_channels(self): + """Test if all channels are returned.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(len(block.channels), 2) + + def test_instructions(self): + """Test if all instructions are returned.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.instructions, tuple(self.test_blocks)) + + def test_channel_duraction(self): + """Test if correct durations is calculated for each channel.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.ch_duration(self.d0), 350) + self.assertEqual(block.ch_duration(self.d1), 200) + + def test_channel_start_time(self): + """Test if correct start time is calculated for each channel.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.ch_start_time(self.d0), 0) + self.assertEqual(block.ch_start_time(self.d1), 0) + + def test_channel_stop_time(self): + """Test if correct stop time is calculated for each channel.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + self.assertEqual(block.ch_stop_time(self.d0), 350) + self.assertEqual(block.ch_stop_time(self.d1), 200) + + def test_cannot_insert(self): + """Test insert is not supported.""" + block = pulse.ScheduleBlock() + + with self.assertRaises(PulseError): + block.insert(0, pulse.Delay(10, self.d0)) + + def test_cannot_shift(self): + """Test shift is not supported.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + with self.assertRaises(PulseError): + block.shift(10, inplace=True) + + def test_cannot_append_schedule(self): + """Test schedule cannot be appended. Schedule should be input as Call instruction.""" + block = pulse.ScheduleBlock() + + sched = pulse.Schedule() + sched += pulse.Delay(10, self.d0) + + with self.assertRaises(PulseError): + block.append(sched) + + def test_replace(self): + """Test replacing specific instruction.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + replaced = pulse.Play(pulse.Constant(300, 0.1), self.d1) + target = pulse.Delay(50, self.d0) + + block_replaced = block.replace(target, replaced, inplace=False) + + # original schedule is not destroyed + self.assertListEqual(list(block.instructions), self.test_blocks) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) + ref_sched = ref_sched.insert(200, replaced) + ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform1, self.d0)) + + self.assertScheduleEqual(block_replaced, ref_sched) + + def test_replace_inplace(self): + """Test replacing specific instruction with inplace.""" + block = pulse.ScheduleBlock() + for inst in self.test_blocks: + block.append(inst) + + replaced = pulse.Play(pulse.Constant(300, 0.1), self.d1) + target = pulse.Delay(50, self.d0) + + block.replace(target, replaced, inplace=True) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) + ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) + ref_sched = ref_sched.insert(200, replaced) + ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform1, self.d0)) + + self.assertScheduleEqual(block, ref_sched) + + def test_replace_block_by_instruction(self): + """Test replacing block with instruction.""" + sub_block1 = pulse.ScheduleBlock() + sub_block1 = sub_block1.append(pulse.Delay(50, self.d0)) + sub_block1 = sub_block1.append(pulse.Play(self.test_waveform0, self.d0)) + + sub_block2 = pulse.ScheduleBlock() + sub_block2 = sub_block2.append(pulse.Delay(50, self.d0)) + sub_block2 = sub_block2.append(pulse.Play(self.test_waveform1, self.d1)) + + main_block = pulse.ScheduleBlock() + main_block = main_block.append(pulse.Delay(50, self.d0)) + main_block = main_block.append(pulse.Play(self.test_waveform0, self.d0)) + main_block = main_block.append(sub_block1) + main_block = main_block.append(sub_block2) + main_block = main_block.append(pulse.Play(self.test_waveform0, self.d1)) + + replaced = main_block.replace(sub_block1, pulse.Delay(100, self.d0)) + + ref_blocks = [ + pulse.Delay(50, self.d0), + pulse.Play(self.test_waveform0, self.d0), + pulse.Delay(100, self.d0), + sub_block2, + pulse.Play(self.test_waveform0, self.d1) + ] + + self.assertListEqual(list(replaced.instructions), ref_blocks) + + def test_replace_instruction_by_block(self): + """Test replacing instruction with block.""" + sub_block1 = pulse.ScheduleBlock() + sub_block1 = sub_block1.append(pulse.Delay(50, self.d0)) + sub_block1 = sub_block1.append(pulse.Play(self.test_waveform0, self.d0)) + + sub_block2 = pulse.ScheduleBlock() + sub_block2 = sub_block2.append(pulse.Delay(50, self.d0)) + sub_block2 = sub_block2.append(pulse.Play(self.test_waveform1, self.d1)) + + main_block = pulse.ScheduleBlock() + main_block = main_block.append(pulse.Delay(50, self.d0)) + main_block = main_block.append(pulse.Play(self.test_waveform0, self.d0)) + main_block = main_block.append(pulse.Delay(100, self.d0)) + main_block = main_block.append(sub_block2) + main_block = main_block.append(pulse.Play(self.test_waveform0, self.d1)) + + replaced = main_block.replace(pulse.Delay(100, self.d0), sub_block1) + + ref_blocks = [ + pulse.Delay(50, self.d0), + pulse.Play(self.test_waveform0, self.d0), + sub_block1, + sub_block2, + pulse.Play(self.test_waveform0, self.d1) + ] + + self.assertListEqual(list(replaced.instructions), ref_blocks) + + def test_len(self): + """Test __len__ method""" + block = pulse.ScheduleBlock() + self.assertEqual(len(block), 0) + + for j in range(1, 10): + block = block.append(pulse.Delay(10, self.d0)) + self.assertEqual(len(block), j) + + +class TestBlockEquality(BaseTestBlock): + """Test equality of blocks. + + Equality of instruction ordering is compared on DAG representation. + This should be tested for each transform. + """ + def test_different_channels(self): + """Test equality is False if different channels.""" + block1 = pulse.ScheduleBlock() + block1 += pulse.Delay(10, self.d0) + + block2 = pulse.ScheduleBlock() + block2 += pulse.Delay(10, self.d1) + + self.assertNotEqual(block1, block2) + + def test_different_transform(self): + """Test equality is False if different transforms.""" + block1 = pulse.ScheduleBlock(alignment_context=self.left_context) + block1 += pulse.Delay(10, self.d0) + + block2 = pulse.ScheduleBlock(alignment_context=self.right_context) + block2 += pulse.Delay(10, self.d0) + + self.assertNotEqual(block1, block2) + + def test_different_transform_opts(self): + """Test equality is False if different transform options.""" + context1 = transforms.AlignEquispaced(duration=100) + context2 = transforms.AlignEquispaced(duration=500) + + block1 = pulse.ScheduleBlock(alignment_context=context1) + block1 += pulse.Delay(10, self.d0) + + block2 = pulse.ScheduleBlock(alignment_context=context2) + block2 += pulse.Delay(10, self.d0) + + self.assertNotEqual(block1, block2) + + def test_instruction_out_of_order_left(self): + """Test equality is True if two blocks have instructions in different order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.left_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.left_context) + block2 += pulse.Play(self.test_waveform0, self.d1) + block2 += pulse.Play(self.test_waveform0, self.d0) + + self.assertEqual(block1, block2) + + def test_instruction_in_order_left(self): + """Test equality is True if two blocks have instructions in same order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.left_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.left_context) + block2 += pulse.Play(self.test_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, self.d1) + + self.assertEqual(block1, block2) + + def test_instruction_out_of_order_right(self): + """Test equality is True if two blocks have instructions in different order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.right_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.right_context) + block2 += pulse.Play(self.test_waveform0, self.d1) + block2 += pulse.Play(self.test_waveform0, self.d0) + + self.assertEqual(block1, block2) + + def test_instruction_in_order_right(self): + """Test equality is True if two blocks have instructions in same order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.right_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.right_context) + block2 += pulse.Play(self.test_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, self.d1) + + self.assertEqual(block1, block2) + + def test_instruction_out_of_order_sequential(self): + """Test equality is False if two blocks have instructions in different order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.sequential_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.sequential_context) + block2 += pulse.Play(self.test_waveform0, self.d1) + block2 += pulse.Play(self.test_waveform0, self.d0) + + self.assertNotEqual(block1, block2) + + def test_instruction_in_order_sequential(self): + """Test equality is True if two blocks have instructions in same order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.sequential_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.sequential_context) + block2 += pulse.Play(self.test_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, self.d1) + + self.assertEqual(block1, block2) + + def test_instruction_out_of_order_equispaced(self): + """Test equality is False if two blocks have instructions in different order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) + block2 += pulse.Play(self.test_waveform0, self.d1) + block2 += pulse.Play(self.test_waveform0, self.d0) + + self.assertNotEqual(block1, block2) + + def test_instruction_in_order_equispaced(self): + """Test equality is True if two blocks have instructions in same order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) + block2 += pulse.Play(self.test_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, self.d1) + + self.assertEqual(block1, block2) + + def test_instruction_out_of_order_func(self): + """Test equality is False if two blocks have instructions in different order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.func_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.func_context) + block2 += pulse.Play(self.test_waveform0, self.d1) + block2 += pulse.Play(self.test_waveform0, self.d0) + + self.assertNotEqual(block1, block2) + + def test_instruction_in_order_func(self): + """Test equality is True if two blocks have instructions in same order.""" + block1 = pulse.ScheduleBlock(alignment_context=self.func_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform0, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.func_context) + block2 += pulse.Play(self.test_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, self.d1) + + self.assertEqual(block1, block2) + + def test_instrution_in_oder_but_different_node(self): + """Test equality is False if two blocks have different instructions.""" + block1 = pulse.ScheduleBlock(alignment_context=self.left_context) + block1 += pulse.Play(self.test_waveform0, self.d0) + block1 += pulse.Play(self.test_waveform1, self.d1) + + block2 = pulse.ScheduleBlock(alignment_context=self.left_context) + block2 += pulse.Play(self.test_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, self.d1) + + self.assertNotEqual(block1, block2) + + def test_instruction_out_of_order_complex_equal(self): + """Test complex schedule equality can be correctly evaluated.""" + block1_a = pulse.ScheduleBlock(alignment_context=self.left_context) + block1_a += pulse.Delay(10, self.d0) + block1_a += pulse.Play(self.test_waveform1, self.d1) + block1_a += pulse.Play(self.test_waveform0, self.d0) + + block1_b = pulse.ScheduleBlock(alignment_context=self.left_context) + block1_b += pulse.Play(self.test_waveform1, self.d1) + block1_b += pulse.Delay(10, self.d0) + block1_b += pulse.Play(self.test_waveform0, self.d0) + + block2_a = pulse.ScheduleBlock(alignment_context=self.right_context) + block2_a += block1_a + block2_a += block1_b + block2_a += block1_a + + block2_b = pulse.ScheduleBlock(alignment_context=self.right_context) + block2_b += block1_a + block2_b += block1_a + block2_b += block1_b + + self.assertEqual(block2_a, block2_b) + + def test_instruction_out_of_order_complex_not_equal(self): + """Test complex schedule equality can be correctly evaluated.""" + block1_a = pulse.ScheduleBlock(alignment_context=self.left_context) + block1_a += pulse.Play(self.test_waveform0, self.d0) + block1_a += pulse.Play(self.test_waveform1, self.d1) + block1_a += pulse.Delay(10, self.d0) + + block1_b = pulse.ScheduleBlock(alignment_context=self.left_context) + block1_b += pulse.Play(self.test_waveform1, self.d1) + block1_b += pulse.Delay(10, self.d0) + block1_b += pulse.Play(self.test_waveform0, self.d0) + + block2_a = pulse.ScheduleBlock(alignment_context=self.right_context) + block2_a += block1_a + block2_a += block1_b + block2_a += block1_a + + block2_b = pulse.ScheduleBlock(alignment_context=self.right_context) + block2_b += block1_a + block2_b += block1_a + block2_b += block1_b + + self.assertNotEqual(block2_a, block2_b) + + +class TestParametrizedBlockOperation(BaseTestBlock): + """Test fundamental operation with parametrization.""" + def setUp(self): + super().setUp() + + self.amp0 = circuit.Parameter('amp0') + self.amp1 = circuit.Parameter('amp1') + self.dur0 = circuit.Parameter('dur0') + self.dur1 = circuit.Parameter('dur1') + + self.test_par_waveform0 = pulse.Constant(self.dur0, self.amp0) + self.test_par_waveform1 = pulse.Constant(self.dur1, self.amp1) + + def test_report_parameter_assignment(self): + """Test duration assignment check.""" + block = pulse.ScheduleBlock() + block += pulse.Play(self.test_par_waveform0, self.d0) + + # check parameter evaluation mechanism + self.assertTrue(block.is_parameterized()) + self.assertFalse(block.is_schedulable()) + + # assign duration + block = block.assign_parameters({self.dur0: 200}) + self.assertTrue(block.is_parameterized()) + self.assertTrue(block.is_schedulable()) + + def test_cannot_get_duration_if_not_assigned(self): + """Test raise error when duration is not assigned.""" + block = pulse.ScheduleBlock() + block += pulse.Play(self.test_par_waveform0, self.d0) + + with self.assertRaises(PulseError): + # pylint: disable=pointless-statement + block.duration + + def test_get_assigend_duration(self): + """Test duration is correctly evaluated.""" + block = pulse.ScheduleBlock() + block += pulse.Play(self.test_par_waveform0, self.d0) + block += pulse.Play(self.test_waveform0, self.d0) + + block = block.assign_parameters({self.dur0: 300}) + + self.assertEqual(block.duration, 400) + + def test_nested_parametrized_instructions(self): + """Test parameters of nested schedule can be assigned.""" + test_waveform = pulse.Constant(100, self.amp0) + + param_sched = pulse.Schedule(pulse.Play(test_waveform, self.d0)) + call_inst = pulse.instructions.Call(param_sched) + + sub_block = pulse.ScheduleBlock() + sub_block += call_inst + + block = pulse.ScheduleBlock() + block += sub_block + + self.assertTrue(block.is_parameterized()) + + # assign durations + block = block.assign_parameters({self.amp0: 0.1}) + self.assertFalse(block.is_parameterized()) + + def test_equality_of_parametrized_channels(self): + """Test check equality of blocks involving parametrized channels.""" + par_ch = circuit.Parameter('ch') + + block1 = pulse.ScheduleBlock(alignment_context=self.left_context) + block1 += pulse.Play(self.test_waveform0, pulse.DriveChannel(par_ch)) + block1 += pulse.Play(self.test_par_waveform0, self.d0) + + block2 = pulse.ScheduleBlock(alignment_context=self.left_context) + block2 += pulse.Play(self.test_par_waveform0, self.d0) + block2 += pulse.Play(self.test_waveform0, pulse.DriveChannel(par_ch)) + + self.assertEqual(block1, block2) + + block1_assigned = block1.assign_parameters({par_ch: 1}) + block2_assigned = block2.assign_parameters({par_ch: 1}) + self.assertEqual(block1_assigned, block2_assigned) + + def test_replace_parametrized_instruction(self): + """Test parametrized instruction can updated with parameter table.""" + block = pulse.ScheduleBlock() + block += pulse.Play(self.test_par_waveform0, self.d0) + block += pulse.Delay(100, self.d0) + block += pulse.Play(self.test_waveform0, self.d0) + + replaced = block.replace(pulse.Play(self.test_par_waveform0, self.d0), + pulse.Play(self.test_par_waveform1, self.d0)) + self.assertTrue(replaced.is_parameterized()) + + # check assign parameters + replaced_assigned = replaced.assign_parameters({self.dur1: 100, self.amp1: 0.1}) + self.assertFalse(replaced_assigned.is_parameterized()) + + def test_parametrized_context(self): + """Test parametrize context parameter.""" + duration = circuit.Parameter('dur') + param_context = transforms.AlignEquispaced(duration=duration) + + block = pulse.ScheduleBlock(alignment_context=param_context) + block += pulse.Delay(10, self.d0) + block += pulse.Delay(10, self.d0) + block += pulse.Delay(10, self.d0) + block += pulse.Delay(10, self.d0) + self.assertTrue(block.is_parameterized()) + self.assertFalse(block.is_schedulable()) + + block.assign_parameters({duration: 100}, inplace=True) + self.assertFalse(block.is_parameterized()) + self.assertTrue(block.is_schedulable()) + + ref_sched = pulse.Schedule() + ref_sched = ref_sched.insert(0, pulse.Delay(10, self.d0)) + ref_sched = ref_sched.insert(10, pulse.Delay(20, self.d0)) + ref_sched = ref_sched.insert(30, pulse.Delay(10, self.d0)) + ref_sched = ref_sched.insert(40, pulse.Delay(20, self.d0)) + ref_sched = ref_sched.insert(60, pulse.Delay(10, self.d0)) + ref_sched = ref_sched.insert(70, pulse.Delay(20, self.d0)) + ref_sched = ref_sched.insert(90, pulse.Delay(10, self.d0)) + + self.assertScheduleEqual(block, ref_sched) diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index 5ca615e14b94..cb2e9a2c4eb7 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -16,6 +16,7 @@ from qiskit import pulse, circuit from qiskit.pulse import channels, configuration, instructions, library, exceptions +from qiskit.pulse.transforms import inline_subroutines from qiskit.test import QiskitTestCase @@ -267,15 +268,52 @@ def test_parameterized_call(self): self.assertTrue(call.is_parameterized()) self.assertEqual(len(call.parameters), 2) - def test_assign_parameters(self): - """Test assigning parameter doesn't immediately update program.""" - call = instructions.Call(subroutine=self.function) - call.assign_parameters({self.param1: 0.1, self.param2: 0.2}) + def test_assign_parameters_to_call(self): + """Test create schedule by calling subroutine and assign parameters to it.""" + init_dict = {self.param1: 0.1, self.param2: 0.5} + + with pulse.build() as test_sched: + pulse.call(self.function) + + test_sched = test_sched.assign_parameters(value_dict=init_dict) + test_sched = inline_subroutines(test_sched) + + with pulse.build() as ref_sched: + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + + self.assertEqual(test_sched, ref_sched) + + def test_call_initialize_with_parameter(self): + """Test call instruction with parameterized subroutine with initial dict.""" + init_dict = {self.param1: 0.1, self.param2: 0.5} + call = instructions.Call(subroutine=self.function, value_dict=init_dict) + + with pulse.build() as ref_sched: + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + + self.assertEqual(call.assigned_subroutine(), ref_sched) + + def test_call_subroutine_with_different_parameters(self): + """Test call subroutines with different parameters in the same schedule.""" + init_dict1 = {self.param1: 0.1, self.param2: 0.5} + init_dict2 = {self.param1: 0.3, self.param2: 0.7} + + with pulse.build() as test_sched: + pulse.call(self.function, value_dict=init_dict1) + pulse.call(self.function, value_dict=init_dict2) - self.assertFalse(call.is_parameterized()) + test_sched = inline_subroutines(test_sched) - subroutine = call.subroutine - self.assertTrue(subroutine.is_parameterized()) + with pulse.build() as ref_sched: + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.3, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.7, 40), pulse.DriveChannel(0)) + pulse.play(pulse.Gaussian(160, 0.3, 40), pulse.DriveChannel(0)) - arguments = call.arguments - self.assertDictEqual(arguments, {self.param1: 0.1, self.param2: 0.2}) + self.assertEqual(test_sched, ref_sched) diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py new file mode 100644 index 000000000000..6e2d6c26c0c3 --- /dev/null +++ b/test/python/pulse/test_parameter_manager.py @@ -0,0 +1,338 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +# pylint: disable=invalid-name + +"""Test cases for parameter manager.""" + +from copy import deepcopy + +from qiskit import pulse +from qiskit.circuit import Parameter +from qiskit.pulse.parameter_manager import ParameterGetter, ParameterSetter +from qiskit.pulse.transforms import AlignEquispaced, AlignLeft, inline_subroutines +from qiskit.test import QiskitTestCase + + +class ParameterTestBase(QiskitTestCase): + """A base class for parameter manager unittest, providing test schedule.""" + + def setUp(self): + """Just some useful, reusable Parameters, constants, schedules.""" + super().setUp() + + self.amp1_1 = Parameter('amp1_1') + self.amp1_2 = Parameter('amp1_2') + self.amp2 = Parameter('amp2') + self.amp3 = Parameter('amp3') + + self.dur1 = Parameter('dur1') + self.dur2 = Parameter('dur2') + self.dur3 = Parameter('dur3') + + self.parametric_waveform1 = pulse.Gaussian( + duration=self.dur1, + amp=self.amp1_1 + self.amp1_2, + sigma=self.dur1/4 + ) + + self.parametric_waveform2 = pulse.Gaussian( + duration=self.dur2, + amp=self.amp2, + sigma=self.dur2/5 + ) + + self.parametric_waveform3 = pulse.Gaussian( + duration=self.dur3, + amp=self.amp3, + sigma=self.dur3/6 + ) + + self.ch1 = Parameter('ch1') + self.ch2 = Parameter('ch2') + self.ch3 = Parameter('ch3') + + self.d1 = pulse.DriveChannel(self.ch1) + self.d2 = pulse.DriveChannel(self.ch2) + self.d3 = pulse.DriveChannel(self.ch3) + + self.phi1 = Parameter('phi1') + self.phi2 = Parameter('phi2') + self.phi3 = Parameter('phi3') + + self.meas_dur = Parameter('meas_dur') + self.mem1 = Parameter('s1') + self.reg1 = Parameter('m1') + + self.context_dur = Parameter('context_dur') + + # schedule under test + subroutine = pulse.ScheduleBlock( + alignment_context=AlignLeft() + ) + subroutine += pulse.ShiftPhase(self.phi1, self.d1) + subroutine += pulse.Play(self.parametric_waveform1, self.d1) + + sched = pulse.Schedule() + sched += pulse.ShiftPhase(self.phi3, self.d3) + + long_schedule = pulse.ScheduleBlock( + alignment_context=AlignEquispaced(self.context_dur), + name='long_schedule' + ) + + long_schedule += subroutine + long_schedule += pulse.ShiftPhase(self.phi2, self.d2) + long_schedule += pulse.Play(self.parametric_waveform2, self.d2) + long_schedule += pulse.Call(sched) + long_schedule += pulse.Play(self.parametric_waveform3, self.d3) + + long_schedule += pulse.Acquire(self.meas_dur, + pulse.AcquireChannel(self.ch1), + mem_slot=pulse.MemorySlot(self.mem1), + reg_slot=pulse.RegisterSlot(self.reg1)) + + self.test_sched = long_schedule + + +class TestParameterGetter(ParameterTestBase): + """Test getting parameters.""" + + def test_get_parameter_from_channel(self): + """Test get parameters from channel.""" + test_obj = pulse.DriveChannel(self.ch1 + self.ch2) + + visitor = ParameterGetter() + visitor.visit(test_obj) + + ref_params = {self.ch1, self.ch2} + + self.assertSetEqual(visitor.parameters, ref_params) + + def test_get_parameter_from_pulse(self): + """Test get parameters from pulse instruction.""" + test_obj = self.parametric_waveform1 + + visitor = ParameterGetter() + visitor.visit(test_obj) + + ref_params = {self.amp1_1, self.amp1_2, self.dur1} + + self.assertSetEqual(visitor.parameters, ref_params) + + def test_get_parameter_from_inst(self): + """Test get parameters from instruction.""" + test_obj = pulse.ShiftPhase(self.phi1 + self.phi2, pulse.DriveChannel(0)) + + visitor = ParameterGetter() + visitor.visit(test_obj) + + ref_params = {self.phi1, self.phi2} + + self.assertSetEqual(visitor.parameters, ref_params) + + def test_get_parameter_from_call(self): + """Test get parameters from instruction.""" + sched = pulse.Schedule() + sched += pulse.ShiftPhase(self.phi1, self.d1) + + test_obj = pulse.Call(subroutine=sched) + + visitor = ParameterGetter() + visitor.visit(test_obj) + + ref_params = {self.phi1, self.ch1} + + self.assertSetEqual(visitor.parameters, ref_params) + + def test_get_parameter_from_alignment_context(self): + """Test get parameters from alignment context.""" + test_obj = AlignEquispaced(duration=self.context_dur + self.dur1) + + visitor = ParameterGetter() + visitor.visit(test_obj) + + ref_params = {self.context_dur, self.dur1} + + self.assertSetEqual(visitor.parameters, ref_params) + + def test_get_parameter_from_complex_schedule(self): + """Test get parameters from complicated schedule.""" + test_block = deepcopy(self.test_sched) + + visitor = ParameterGetter() + visitor.visit(test_block) + + self.assertEqual(len(visitor.parameters), 17) + + +class TestParameterSetter(ParameterTestBase): + """Test setting parameters.""" + + def test_set_parameter_to_channel(self): + """Test get parameters from channel.""" + test_obj = pulse.DriveChannel(self.ch1 + self.ch2) + + value_dict = {self.ch1: 1, self.ch2: 2} + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_obj) + + ref_obj = pulse.DriveChannel(3) + + self.assertEqual(assigned, ref_obj) + + def test_set_parameter_to_pulse(self): + """Test get parameters from pulse instruction.""" + test_obj = self.parametric_waveform1 + + value_dict = {self.amp1_1: 0.1, self.amp1_2: 0.2, self.dur1: 160} + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_obj) + + ref_obj = pulse.Gaussian(duration=160, amp=0.3, sigma=40) + + self.assertEqual(assigned, ref_obj) + + def test_set_parameter_to_inst(self): + """Test get parameters from instruction.""" + test_obj = pulse.ShiftPhase(self.phi1 + self.phi2, pulse.DriveChannel(0)) + + value_dict = {self.phi1: 0.123, self.phi2: 0.456} + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_obj) + + ref_obj = pulse.ShiftPhase(0.579, pulse.DriveChannel(0)) + + self.assertEqual(assigned, ref_obj) + + def test_set_parameter_to_call(self): + """Test get parameters from instruction.""" + sched = pulse.Schedule() + sched += pulse.ShiftPhase(self.phi1, self.d1) + + test_obj = pulse.Call(subroutine=sched) + + value_dict = {self.phi1: 1.57, self.ch1: 2} + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_obj) + + ref_sched = pulse.Schedule() + ref_sched += pulse.ShiftPhase(1.57, pulse.DriveChannel(2)) + + ref_obj = pulse.Call(subroutine=ref_sched) + + self.assertEqual(assigned, ref_obj) + + def test_set_parameter_to_alignment_context(self): + """Test get parameters from alignment context.""" + test_obj = AlignEquispaced(duration=self.context_dur + self.dur1) + + value_dict = {self.context_dur: 1000, self.dur1: 100} + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_obj) + + ref_obj = AlignEquispaced(duration=1100) + + self.assertEqual(assigned, ref_obj) + + def test_nested_assigment_partial_bind(self): + """Test nested schedule with call instruction. + Inline the schedule and partially bind parameters.""" + context = AlignEquispaced(duration=self.context_dur) + subroutine = pulse.ScheduleBlock(alignment_context=context) + subroutine += pulse.Play(self.parametric_waveform1, self.d1) + + nested_block = pulse.ScheduleBlock() + nested_block += pulse.Call(subroutine=subroutine) + + test_obj = pulse.ScheduleBlock() + test_obj += nested_block + + test_obj = inline_subroutines(test_obj) + + value_dict = {self.context_dur: 1000, self.dur1: 200, self.ch1: 1} + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_obj) + + ref_context = AlignEquispaced(duration=1000) + ref_subroutine = pulse.ScheduleBlock(alignment_context=ref_context) + ref_subroutine += pulse.Play(pulse.Gaussian(200, self.amp1_1 + self.amp1_2, 25), + pulse.DriveChannel(1)) + + ref_nested_block = pulse.ScheduleBlock() + ref_nested_block += ref_subroutine + + ref_obj = pulse.ScheduleBlock() + ref_obj += nested_block + + self.assertEqual(assigned, ref_obj) + + def test_set_parameter_to_complex_schedule(self): + """Test get parameters from complicated schedule.""" + test_block = deepcopy(self.test_sched) + + value_dict = { + self.amp1_1: 0.1, + self.amp1_2: 0.2, + self.amp2: 0.3, + self.amp3: 0.4, + self.dur1: 100, + self.dur2: 125, + self.dur3: 150, + self.ch1: 0, + self.ch2: 2, + self.ch3: 4, + self.phi1: 1., + self.phi2: 2., + self.phi3: 3., + self.meas_dur: 300, + self.mem1: 3, + self.reg1: 0, + self.context_dur: 1000 + } + + visitor = ParameterSetter(param_map=value_dict) + assigned = visitor.visit(test_block) + + # create ref schedule + subroutine = pulse.ScheduleBlock( + alignment_context=AlignLeft() + ) + subroutine += pulse.ShiftPhase(1., pulse.DriveChannel(0)) + subroutine += pulse.Play(pulse.Gaussian(100, 0.3, 25), pulse.DriveChannel(0)) + + sched = pulse.Schedule() + sched += pulse.ShiftPhase(3., pulse.DriveChannel(4)) + + ref_obj = pulse.ScheduleBlock( + alignment_context=AlignEquispaced(1000), + name='long_schedule' + ) + + ref_obj += subroutine + ref_obj += pulse.ShiftPhase(2., pulse.DriveChannel(2)) + ref_obj += pulse.Play(pulse.Gaussian(125, 0.3, 25), pulse.DriveChannel(2)) + ref_obj += pulse.Call(sched) + ref_obj += pulse.Play(pulse.Gaussian(150, 0.4, 25), pulse.DriveChannel(4)) + + ref_obj += pulse.Acquire(300, + pulse.AcquireChannel(0), + pulse.MemorySlot(3), + pulse.RegisterSlot(0)) + + self.assertEqual(assigned, ref_obj) diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py index bf7193cf2462..7d9629faa117 100644 --- a/test/python/pulse/test_schedule.py +++ b/test/python/pulse/test_schedule.py @@ -75,15 +75,6 @@ def test_append_an_instruction_to_empty_schedule(self): self.assertEqual(0, sched.start_time) self.assertEqual(3, sched.stop_time) - def test_deprecated_style(self): - """Test append instructions to an empty schedule.""" - lp0 = self.linear(duration=3, slope=0.2, intercept=0.1) - - sched = Schedule() - sched = sched.append(Play(lp0, self.config.drive(0))) - self.assertEqual(0, sched.start_time) - self.assertEqual(3, sched.stop_time) - def test_append_instructions_applying_to_different_channels(self): """Test append instructions to schedule.""" lp0 = self.linear(duration=3, slope=0.2, intercept=0.1) diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index 35f30850da64..f01b4d3fcbe9 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -819,7 +819,11 @@ class _TestDirective(directives.Directive): def __init__(self, *channels): """Test directive""" - super().__init__(tuple(channels), 0, tuple(channels)) + super().__init__(operands=tuple(channels)) + + @property + def channels(self): + return self.operands class TestRemoveDirectives(QiskitTestCase):