diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index 2df00258b4c8..961dfb086a20 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -17,9 +17,10 @@ from typing import Any, Dict, List, Tuple from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule, Delay, Acquire +from qiskit.pulse import Schedule, Acquire, Delay, Play +from qiskit.pulse.pulse_lib import ParametricPulse, SamplePulse from qiskit.pulse.commands import (Command, PulseInstruction, AcquireInstruction, - DelayInstruction, SamplePulse, ParametricInstruction) + DelayInstruction, ParametricInstruction) from qiskit.qobj import (PulseQobj, QobjHeader, QobjExperimentHeader, PulseQobjInstruction, PulseQobjExperimentConfig, PulseQobjExperiment, PulseQobjConfig, PulseLibraryItem) @@ -170,7 +171,14 @@ def _assemble_instructions( acquire_instruction_map = defaultdict(list) for time, instruction in schedule.instructions: - if isinstance(instruction, ParametricInstruction): + if isinstance(instruction, Play) and isinstance(instruction.pulse, ParametricPulse): + pulse_shape = ParametricPulseShapes(type(instruction.pulse)).name + if pulse_shape not in run_config.parametric_pulses: + instruction = Play(instruction.pulse.get_sample_pulse(), + instruction.channel, + name=instruction.name) + + if isinstance(instruction, ParametricInstruction): # deprecated pulse_shape = ParametricPulseShapes(type(instruction.command)).name if pulse_shape not in run_config.parametric_pulses: # Convert to SamplePulse if the backend does not support it @@ -178,7 +186,16 @@ def _assemble_instructions( instruction.channels[0], name=instruction.name) - if isinstance(instruction, PulseInstruction): + if isinstance(instruction, Play) and isinstance(instruction.pulse, SamplePulse): + name = instruction.pulse.name + if instruction.pulse != user_pulselib.get(name): + name = "{0}-{1:x}".format(name, hash(instruction.pulse.samples.tostring())) + instruction = Play(SamplePulse(name=name, samples=instruction.pulse.samples), + channel=instruction.channel, + name=instruction.name) + user_pulselib[name] = instruction.pulse + + if isinstance(instruction, PulseInstruction): # deprecated name = instruction.command.name if name in user_pulselib and instruction.command != user_pulselib[name]: name = "{0}-{1:x}".format(name, hash(instruction.command.samples.tostring())) diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 026cce5963f9..bc156dd2b49a 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -132,13 +132,12 @@ from .channels import (DriveChannel, MeasureChannel, AcquireChannel, ControlChannel, RegisterSlot, MemorySlot) -from .commands import (AcquireInstruction, FrameChange, - PersistentValue, SamplePulse, ParametricPulse, - ParametricInstruction, Gaussian, - GaussianSquare, Drag, ConstantPulse, functional_pulse) +from .commands import AcquireInstruction, FrameChange, PersistentValue from .configuration import LoConfig, LoRange, Kernel, Discriminator from .exceptions import PulseError from .instruction_schedule_map import InstructionScheduleMap -from .instructions import Acquire, Instruction, Delay, ShiftPhase, Snapshot, SetFrequency +from .instructions import Acquire, Instruction, Delay, Play, ShiftPhase, Snapshot, SetFrequency from .interfaces import ScheduleComponent +from .pulse_lib import SamplePulse, Gaussian, GaussianSquare, Drag, ConstantPulse, ParametricPulse +from .pulse_lib.samplers.decorators import functional_pulse from .schedule import Schedule diff --git a/qiskit/pulse/commands/delay.py b/qiskit/pulse/commands/delay.py index e4355abc927d..b90a4239af11 100644 --- a/qiskit/pulse/commands/delay.py +++ b/qiskit/pulse/commands/delay.py @@ -15,6 +15,7 @@ """Delay instruction. Deprecated path.""" import warnings +from ..channels import Channel from ..instructions import Delay from ..instructions import Instruction @@ -22,7 +23,7 @@ class DelayInstruction(Instruction): """Deprecated.""" - def __init__(self, command: Delay, channel: Delay, name: str = None): + def __init__(self, command: Delay, channel: Channel, name: str = None): """Create a delay instruction from a delay command. Args: diff --git a/qiskit/pulse/commands/parametric_pulses.py b/qiskit/pulse/commands/parametric_pulses.py index 767964797da3..b28add4d9835 100644 --- a/qiskit/pulse/commands/parametric_pulses.py +++ b/qiskit/pulse/commands/parametric_pulses.py @@ -12,395 +12,22 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -Parametric pulse commands module. These are pulse commands which are described by a specified -parameterization. +"""Deprecated path to parametric pulses.""" +import warnings -If a backend supports parametric pulses, it will have the attribute -`backend.configuration().parametric_pulses`, which is a list of supported pulse shapes, such as -`['gaussian', 'gaussian_square', 'drag']`. A Pulse Schedule, using parametric pulses, which is -assembled for a backend which supports those pulses, will result in a Qobj which is dramatically -smaller than one which uses SamplePulses. +# pylint: disable=unused-import -This module can easily be extended to describe more pulse shapes. The new class should: - - have a descriptive name - - be a well known and/or well described formula (include the formula in the class docstring) - - take some parameters (at least `duration`) and validate them, if necessary - - implement a `get_sample_pulse` method which returns a corresponding SamplePulse in the - case that it is assembled for a backend which does not support it. +from qiskit.pulse.pulse_lib import ParametricPulse, Gaussian, GaussianSquare, Drag, ConstantPulse +from qiskit.pulse.channels import Channel -The new pulse must then be registered by the assembler in -`qiskit/qobj/converters/pulse_instruction.py:ParametricPulseShapes` -by following the existing pattern: - class ParametricPulseShapes(Enum): - gaussian = commands.Gaussian - ... - new_supported_pulse_name = commands.YourPulseCommandClass -""" -from abc import abstractmethod -from typing import Any, Callable, Dict, Optional -import math -import numpy as np - -from qiskit.pulse.pulse_lib import gaussian, gaussian_square, drag, constant, continuous -from .sample_pulse import SamplePulse -from ..instructions import Instruction -from ..channels import PulseChannel -from ..exceptions import PulseError - -from .pulse_command import PulseCommand - -# pylint: disable=missing-docstring - - -class ParametricPulse(PulseCommand): - """The abstract superclass for parametric pulses.""" - - @abstractmethod - def __init__(self, duration: int): - """Create a parametric pulse and validate the input parameters. - - Args: - duration: Pulse length in terms of the the sampling period `dt`. - """ - super().__init__(duration=duration) - self.validate_parameters() - - @abstractmethod - def get_sample_pulse(self) -> SamplePulse: - """Return a SamplePulse with samples filled according to the formula that the pulse - represents and the parameter values it contains. - """ - raise NotImplementedError - - @abstractmethod - def validate_parameters(self) -> None: - """ - Validate parameters. - - Raises: - PulseError: If the parameters passed are not valid. - """ - raise NotImplementedError - - @property - @abstractmethod - def parameters(self) -> Dict[str, Any]: - """Return a dictionary containing the pulse's parameters.""" - pass - - def to_instruction(self, channel: PulseChannel, - name: Optional[str] = None) -> 'ParametricInstruction': - # pylint: disable=arguments-differ - return ParametricInstruction(self, channel, name=name) - - def draw(self, dt: float = 1, - style=None, - filename: Optional[str] = None, - interp_method: Optional[Callable] = None, - scale: float = 1, interactive: bool = False, - scaling: float = None): - """Plot the pulse. - - Args: - dt: Time interval of samples. - style (Optional[PulseStyle]): A style sheet to configure plot appearance - filename: Name required to save pulse image - interp_method: A function for interpolation - scale: Relative visual scaling of waveform amplitudes - interactive: When set true show the circuit in a new window - (this depends on the matplotlib backend being used supporting this) - scaling: Deprecated, see `scale` - - Returns: - matplotlib.figure: A matplotlib figure object of the pulse envelope - """ - return self.get_sample_pulse().draw(dt=dt, style=style, filename=filename, - interp_method=interp_method, scale=scale, - interactive=interactive) - - -class Gaussian(ParametricPulse): - """ - A truncated pulse envelope shaped according to the Gaussian function whose mean is centered at - the center of the pulse (duration / 2): - - .. math:: - - f(x) = amp * exp( -(1/2) * (x - duration/2)^2 / sigma^2) ) , 0 <= x < duration - """ - - def __init__(self, - duration: int, - amp: complex, - sigma: float): - """Initialize the gaussian pulse. - - Args: - duration: Pulse length in terms of the the sampling period `dt`. - amp: The amplitude of the Gaussian envelope. - sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically - in the class docstring. - """ - self._amp = complex(amp) - self._sigma = sigma - super().__init__(duration=duration) - - @property - def amp(self): - return self._amp - - @property - def sigma(self): - return self._sigma - - def get_sample_pulse(self) -> SamplePulse: - return gaussian(duration=self.duration, amp=self.amp, - sigma=self.sigma, zero_ends=False) - - def validate_parameters(self) -> None: - if abs(self.amp) > 1.: - raise PulseError("The amplitude norm must be <= 1, " - "found: {}".format(abs(self.amp))) - if self.sigma <= 0: - raise PulseError("Sigma must be greater than 0.") - - @property - def parameters(self) -> Dict[str, Any]: - return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma} - - def __repr__(self): - return '{}(duration={}, amp={}, sigma={})' \ - ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma) - - -class GaussianSquare(ParametricPulse): - """ - A square pulse with a Gaussian shaped risefall on either side: - - .. math:: - - risefall = (duration - width) / 2 - - 0 <= x < risefall - - f(x) = amp * exp( -(1/2) * (x - risefall/2)^2 / sigma^2) ) - - risefall <= x < risefall + width - - f(x) = amp - - risefall + width <= x < duration - - f(x) = amp * exp( -(1/2) * (x - (risefall + width)/2)^2 / sigma^2) ) - """ - - def __init__(self, - duration: int, - amp: complex, - sigma: float, - width: float): - """Initialize the gaussian square pulse. - - Args: - duration: Pulse length in terms of the the sampling period `dt`. - amp: The amplitude of the Gaussian and of the square pulse. - sigma: A measure of how wide or narrow the Gaussian risefall is; see the class - docstring for more details. - width: The duration of the embedded square pulse. - """ - self._amp = complex(amp) - self._sigma = sigma - self._width = width - super().__init__(duration=duration) - - @property - def amp(self): - return self._amp - - @property - def sigma(self): - return self._sigma - - @property - def width(self): - return self._width - - def get_sample_pulse(self) -> SamplePulse: - return gaussian_square(duration=self.duration, amp=self.amp, - width=self.width, sigma=self.sigma, - zero_ends=False) - - def validate_parameters(self) -> None: - if abs(self.amp) > 1.: - raise PulseError("The amplitude norm must be <= 1, " - "found: {}".format(abs(self.amp))) - if self.sigma <= 0: - raise PulseError("Sigma must be greater than 0.") - if self.width < 0 or self.width >= self.duration: - raise PulseError("The pulse width must be at least 0 and less than its duration.") - - @property - def parameters(self) -> Dict[str, Any]: - return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, - "width": self.width} - - def __repr__(self): - return '{}(duration={}, amp={}, sigma={}, width={})' \ - ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.width) - - -class Drag(ParametricPulse): - r"""The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse - with an additional Gaussian derivative component. It is designed to reduce the frequency - spectrum of a normal gaussian pulse near the :math:`|1\rangle` - :math:`|2\rangle` transition, - reducing the chance of leakage to the :math:`|2\rangle` state. - - .. math:: - - f(x) = Gaussian + 1j * beta * d/dx [Gaussian] - = Gaussian + 1j * beta * (-(x - duration/2) / sigma^2) [Gaussian] - - where 'Gaussian' is: - - .. math:: - - Gaussian(x, amp, sigma) = amp * exp( -(1/2) * (x - duration/2)^2 / sigma^2) ) - - References: - 1. |citation1|_ - - .. _citation1: https://link.aps.org/doi/10.1103/PhysRevA.83.012308 - - .. |citation1| replace:: *Gambetta, J. M., Motzoi, F., Merkel, S. T. & Wilhelm, F. K. - Analytic control methods for high-fidelity unitary operations - in a weakly nonlinear oscillator. Phys. Rev. A 83, 012308 (2011).* - - 2. |citation2|_ - - .. _citation2: https://link.aps.org/doi/10.1103/PhysRevLett.103.110501 - - .. |citation2| replace:: *F. Motzoi, J. M. Gambetta, P. Rebentrost, and F. K. Wilhelm - Phys. Rev. Lett. 103, 110501 – Published 8 September 2009.* - """ - - def __init__(self, - duration: int, - amp: complex, - sigma: float, - beta: float): - """Initialize the drag pulse. - - Args: - duration: Pulse length in terms of the the sampling period `dt`. - amp: The amplitude of the Drag envelope. - sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically - in the class docstring. - beta: The correction amplitude. - """ - self._amp = complex(amp) - self._sigma = sigma - self._beta = beta - super().__init__(duration=duration) - - @property - def amp(self): - return self._amp - - @property - def sigma(self): - return self._sigma - - @property - def beta(self): - return self._beta - - def get_sample_pulse(self) -> SamplePulse: - return drag(duration=self.duration, amp=self.amp, sigma=self.sigma, - beta=self.beta, zero_ends=False) - - def validate_parameters(self) -> None: - if abs(self.amp) > 1.: - raise PulseError("The amplitude norm must be <= 1, " - "found: {}".format(abs(self.amp))) - if self.sigma <= 0: - raise PulseError("Sigma must be greater than 0.") - if isinstance(self.beta, complex): - raise PulseError("Beta must be real.") - # Check if beta is too large: the amplitude norm must be <=1 for all points - if self.beta > self.sigma: - # If beta <= sigma, then the maximum amplitude is at duration / 2, which is - # already constrainted by self.amp <= 1 - - # 1. Find the first maxima associated with the beta * d/dx gaussian term - # This eq is derived from solving for the roots of the norm of the drag function. - # There is a second maxima mirrored around the center of the pulse with the same - # norm as the first, so checking the value at the first x maxima is sufficient. - argmax_x = (self.duration / 2 - - (self.sigma / self.beta) * math.sqrt(self.beta ** 2 - self.sigma ** 2)) - if argmax_x < 0: - # If the max point is out of range, either end of the pulse will do - argmax_x = 0 - - # 2. Find the value at that maximum - max_val = continuous.drag(np.array(argmax_x), sigma=self.sigma, - beta=self.beta, amp=self.amp, center=self.duration / 2) - if abs(max_val) > 1.: - raise PulseError("Beta is too large; pulse amplitude norm exceeds 1.") - - @property - def parameters(self) -> Dict[str, Any]: - return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, - "beta": self.beta} - - def __repr__(self): - return '{}(duration={}, amp={}, sigma={}, beta={})' \ - ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.beta) - - -class ConstantPulse(ParametricPulse): - """ - A simple constant pulse, with an amplitude value and a duration: - - .. math:: - - f(x) = amp , 0 <= x < duration - f(x) = 0 , elsewhere - """ - - def __init__(self, - duration: int, - amp: complex): - """ - Initialize the constant-valued pulse. - - Args: - duration: Pulse length in terms of the the sampling period `dt`. - amp: The amplitude of the constant square pulse. - """ - self._amp = complex(amp) - super().__init__(duration=duration) - - @property - def amp(self): - return self._amp - - def get_sample_pulse(self) -> SamplePulse: - return constant(duration=self.duration, amp=self.amp) - - def validate_parameters(self) -> None: - if abs(self.amp) > 1.: - raise PulseError("The amplitude norm must be <= 1, " - "found: {}".format(abs(self.amp))) - - @property - def parameters(self) -> Dict[str, Any]: - return {"duration": self.duration, "amp": self.amp} - - def __repr__(self): - return '{}(duration={}, amp={})'.format(self.__class__.__name__, self.duration, self.amp) - - -class ParametricInstruction(Instruction): +class ParametricInstruction: """Instruction to drive a parametric pulse to an `PulseChannel`.""" + + def __init__(self, command: ParametricPulse, channel: Channel, name: str = None): + warnings.warn("ParametricInstruction is deprecated. Use Play, instead, with a pulse and a " + "channel. For example: ParametricInstruction(Gaussian(amp=amp, sigma=sigma, " + "duration=duration), DriveChannel(0)) -> Play(Gaussian(amp=amp, sigma=sigma," + " duration=duration), DriveChannel(0)).", + DeprecationWarning) + super().__init__(command, channel, name=name) diff --git a/qiskit/pulse/commands/pulse_decorators.py b/qiskit/pulse/commands/pulse_decorators.py index 4de0f5f163f5..af8e237bcf71 100644 --- a/qiskit/pulse/commands/pulse_decorators.py +++ b/qiskit/pulse/commands/pulse_decorators.py @@ -12,37 +12,10 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=missing-return-doc, missing-return-type-doc +# pylint: disable=missing-return-doc, missing-return-type-doc,unused-import """ Pulse decorators. """ -import functools -from typing import Callable - -import numpy as np - -from qiskit.pulse.exceptions import PulseError - -from .sample_pulse import SamplePulse - - -def functional_pulse(func: Callable): - """A decorator for generating SamplePulse from python callable. - - Args: - func: A function describing pulse envelope. - Raises: - PulseError: when invalid function is specified. - """ - @functools.wraps(func) - def to_pulse(duration, *args, name=None, **kwargs): - """Return SamplePulse.""" - if isinstance(duration, (int, np.integer)) and duration > 0: - samples = func(duration, *args, **kwargs) - samples = np.asarray(samples, dtype=np.complex128) - return SamplePulse(samples=samples, name=name) - raise PulseError('The first argument must be an integer value representing duration.') - - return to_pulse +from qiskit.pulse.pulse_lib.samplers.decorators import functional_pulse diff --git a/qiskit/pulse/commands/sample_pulse.py b/qiskit/pulse/commands/sample_pulse.py index 91a23b254f60..1392a2c9400b 100644 --- a/qiskit/pulse/commands/sample_pulse.py +++ b/qiskit/pulse/commands/sample_pulse.py @@ -12,160 +12,22 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -Sample pulse. -""" -from typing import Callable, Union, List, Optional - +"""Sample pulse. Deprecated path.""" import warnings -import numpy as np -from qiskit.pulse.channels import PulseChannel -from qiskit.pulse.exceptions import PulseError +from typing import Optional +from ..channels import PulseChannel +from ..pulse_lib import SamplePulse from ..instructions import Instruction -from .pulse_command import PulseCommand - - -class SamplePulse(PulseCommand): - """Container for functional pulse.""" - - def __init__(self, samples: Union[np.ndarray, List[complex]], name: Optional[str] = None, - epsilon: float = 1e-7): - """Create new sample pulse command. - - Args: - samples: Complex array of pulse envelope - name: Unique name to identify the pulse - epsilon: Pulse sample norm tolerance for clipping. - If any sample's norm exceeds unity by less than or equal to epsilon - it will be clipped to unit norm. If the sample - norm is greater than 1+epsilon an error will be raised - """ - super().__init__(duration=len(samples)) - - samples = np.asarray(samples, dtype=np.complex_) - - self._samples = self._clip(samples, epsilon=epsilon) - self._name = SamplePulse.create_name(name) - - @property - def samples(self): - """Return sample values.""" - return self._samples - - def _clip(self, samples: np.ndarray, epsilon: float = 1e-7): - """If samples are within epsilon of unit norm, clip sample by reducing norm by (1-epsilon). - - If difference is greater than epsilon error is raised. - - Args: - samples: Complex array of pulse envelope - epsilon: Pulse sample norm tolerance for clipping. - If any sample's norm exceeds unity by less than or equal to epsilon - it will be clipped to unit norm. If the sample - norm is greater than 1+epsilon an error will be raised - - Returns: - np.ndarray: Clipped pulse samples - Raises: - PulseError: If there exists a pulse sample with a norm greater than 1+epsilon - """ - samples_norm = np.abs(samples) - to_clip = (samples_norm > 1.) & (samples_norm <= 1. + epsilon) - - if np.any(to_clip): - # first try normalizing by the abs value - clip_where = np.argwhere(to_clip) - clip_angle = np.angle(samples[clip_where]) - clipped_samples = np.exp(1j*clip_angle, dtype=np.complex_) - - # if norm still exceed one subtract epsilon - # required for some platforms - clipped_sample_norms = np.abs(clipped_samples) - to_clip_epsilon = clipped_sample_norms > 1. - if np.any(to_clip_epsilon): - clip_where_epsilon = np.argwhere(to_clip_epsilon) - clipped_samples_epsilon = np.exp( - (1-epsilon)*1j*clip_angle[clip_where_epsilon], dtype=np.complex_) - clipped_samples[clip_where_epsilon] = clipped_samples_epsilon - - # update samples with clipped values - samples[clip_where] = clipped_samples - samples_norm[clip_where] = np.abs(clipped_samples) - - if np.any(samples_norm > 1.): - raise PulseError('Pulse contains sample with norm greater than 1+epsilon.') - - return samples - - def draw(self, dt: float = 1, - style=None, - filename: Optional[str] = None, - interp_method: Optional[Callable] = None, - scale: float = 1, interactive: bool = False, - scaling: float = None): - """Plot the interpolated envelope of pulse. - - Args: - dt: Time interval of samples. - style (Optional[PulseStyle]): A style sheet to configure plot appearance - filename: Name required to save pulse image - interp_method: A function for interpolation - scale: Relative visual scaling of waveform amplitudes - interactive: When set true show the circuit in a new window - (this depends on the matplotlib backend being used supporting this) - scaling: Deprecated, see `scale` - - Returns: - matplotlib.figure: A matplotlib figure object of the pulse envelope - """ - # pylint: disable=invalid-name, cyclic-import - if scaling is not None: - warnings.warn( - 'The parameter "scaling" is being replaced by "scale"', - DeprecationWarning, 3) - scale = scaling - - from qiskit import visualization - - return visualization.pulse_drawer(self, dt=dt, style=style, filename=filename, - interp_method=interp_method, scale=scale, - interactive=interactive) - - def __eq__(self, other: 'SamplePulse'): - """Two SamplePulses are the same if they are of the same type - and have the same name and samples. - - Args: - other: other SamplePulse - - Returns: - bool: are self and other equal - """ - return super().__eq__(other) and (self.samples == other.samples).all() - - def __hash__(self): - return hash((super().__hash__(), self.samples.tostring())) - - def __repr__(self): - opt = np.get_printoptions() - np.set_printoptions(threshold=50) - repr_str = '%s(samples=%s, name="%s")' % (self.__class__.__name__, - repr(self.samples), - self.name) - np.set_printoptions(**opt) - return repr_str - - # pylint: disable=arguments-differ - def to_instruction(self, channel: PulseChannel, - name: Optional[str] = None) -> 'PulseInstruction': - return PulseInstruction(self, channel, name=name) - # pylint: enable=arguments-differ class PulseInstruction(Instruction): """Instruction to drive a pulse to an `PulseChannel`.""" def __init__(self, command: SamplePulse, channel: PulseChannel, name: Optional[str] = None): + warnings.warn("PulseInstruction is deprecated. Use Play, instead, with a pulse and a " + "channel. For example: PulseInstruction(SamplePulse([...]), DriveChannel(0))" + " -> Play(SamplePulse[...], DriveChannel(0)).", + DeprecationWarning) super().__init__(command, channel, name=name) diff --git a/qiskit/pulse/instructions/__init__.py b/qiskit/pulse/instructions/__init__.py index f3677233ddce..2a58d4bfa7a9 100644 --- a/qiskit/pulse/instructions/__init__.py +++ b/qiskit/pulse/instructions/__init__.py @@ -34,4 +34,5 @@ from .instruction import Instruction from .frequency import SetFrequency from .phase import ShiftPhase +from .play import Play from .snapshot import Snapshot diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py new file mode 100644 index 000000000000..74a54f85b788 --- /dev/null +++ b/qiskit/pulse/instructions/play.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""An instruction to transmit a given pulse on a ``PulseChannel`` (i.e., those which support +transmitted pulses, such as ``DriveChannel``). +""" +from typing import List, Optional, Union + +from ..channels import PulseChannel +from .instruction import Instruction + + +class Play(Instruction): + """This instruction is responsible for applying a pulse on a channel. + + The pulse specifies the exact time dynamics of the output signal envelope for a limited + time. The output is modulated by a phase and frequency which are controlled by separate + instructions. The pulse duration must be fixed, and is implicitly given in terms of the + cycle time, dt, of the backend. + """ + + def __init__(self, pulse: 'Pulse', channel: PulseChannel, name: Optional[str] = None): + """Create a new pulse instruction. + + Args: + pulse: A pulse waveform description, such as + :py:class:`~qiskit.pulse.pulse_lib.SamplePulse`. + channel: The channel to which the pulse is applied. + name: Name of the instruction for display purposes. + """ + self._pulse = pulse + self._channel = channel + super().__init__(pulse.duration, channel, name=name if name is not None else pulse.name) + + @property + def operands(self) -> List[Union['Pulse', PulseChannel]]: + """Return a list of instruction operands.""" + return [self.pulse, self.channel] + + @property + def pulse(self) -> 'Pulse': + """A description of the samples that will be played; for instance, exact sample data or + a known function like Gaussian with parameters. + """ + return self._pulse + + @property + def channel(self) -> PulseChannel: + """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is + scheduled on. + """ + return self._channel + + def __repr__(self): + return "{}({}, {}, name={})".format(self.__class__.__name__, self.pulse, self.channel, + self.name) diff --git a/qiskit/pulse/pulse_lib/__init__.py b/qiskit/pulse/pulse_lib/__init__.py index dd0e35a95fa8..e14ed399f7e4 100644 --- a/qiskit/pulse/pulse_lib/__init__.py +++ b/qiskit/pulse/pulse_lib/__init__.py @@ -15,3 +15,6 @@ """Module for builtin ``pulse_lib``.""" from .discrete import * +from .parametric_pulses import ParametricPulse, Gaussian, GaussianSquare, Drag, ConstantPulse +from .pulse import Pulse +from .sample_pulse import SamplePulse diff --git a/qiskit/pulse/pulse_lib/discrete.py b/qiskit/pulse/pulse_lib/discrete.py index 74176dba206f..2249444fed5b 100644 --- a/qiskit/pulse/pulse_lib/discrete.py +++ b/qiskit/pulse/pulse_lib/discrete.py @@ -21,10 +21,9 @@ import warnings from typing import Optional -from qiskit.pulse.exceptions import PulseError -from ..commands.sample_pulse import SamplePulse +from ..exceptions import PulseError +from .sample_pulse import SamplePulse from . import continuous - from . import samplers diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py new file mode 100644 index 000000000000..2de2c9c9a2b7 --- /dev/null +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Parametric waveforms module. These are pulses which are described by a specified +parameterization. + +If a backend supports parametric pulses, it will have the attribute +`backend.configuration().parametric_pulses`, which is a list of supported pulse shapes, such as +`['gaussian', 'gaussian_square', 'drag']`. A Pulse Schedule, using parametric pulses, which is +assembled for a backend which supports those pulses, will result in a Qobj which is dramatically +smaller than one which uses SamplePulses. + +This module can easily be extended to describe more pulse shapes. The new class should: + - have a descriptive name + - be a well known and/or well described formula (include the formula in the class docstring) + - take some parameters (at least `duration`) and validate them, if necessary + - implement a `get_sample_pulse` method which returns a corresponding SamplePulse in the + case that it is assembled for a backend which does not support it. + +The new pulse must then be registered by the assembler in +`qiskit/qobj/converters/pulse_instruction.py:ParametricPulseShapes` +by following the existing pattern: + class ParametricPulseShapes(Enum): + gaussian = commands.Gaussian + ... + new_supported_pulse_name = commands.YourPulseCommandClass +""" +from abc import abstractmethod +from typing import Any, Callable, Dict, Optional +import math +import numpy as np + +from . import continuous +from .discrete import gaussian, gaussian_square, drag, constant +from .pulse import Pulse +from .sample_pulse import SamplePulse +from ..exceptions import PulseError + + +class ParametricPulse(Pulse): + """The abstract superclass for parametric pulses.""" + + @abstractmethod + def __init__(self, duration: int, name: Optional[str] = None): + """Create a parametric pulse and validate the input parameters. + + Args: + duration: Pulse length in terms of the the sampling period `dt`. + name: Display name for this pulse envelope. + """ + super().__init__(duration=duration, name=name) + self.validate_parameters() + + @abstractmethod + def get_sample_pulse(self) -> SamplePulse: + """Return a SamplePulse with samples filled according to the formula that the pulse + represents and the parameter values it contains. + """ + raise NotImplementedError + + @abstractmethod + def validate_parameters(self) -> None: + """ + Validate parameters. + + Raises: + PulseError: If the parameters passed are not valid. + """ + raise NotImplementedError + + @property + @abstractmethod + def parameters(self) -> Dict[str, Any]: + """Return a dictionary containing the pulse's parameters.""" + pass + + def draw(self, dt: float = 1, + style=None, + filename: Optional[str] = None, + interp_method: Optional[Callable] = None, + scale: float = 1, interactive: bool = False, + scaling: float = None): + """Plot the pulse. + + Args: + dt: Time interval of samples. + style (Optional[PulseStyle]): A style sheet to configure plot appearance + filename: Name required to save pulse image + interp_method: A function for interpolation + scale: Relative visual scaling of waveform amplitudes + interactive: When set true show the circuit in a new window + (this depends on the matplotlib backend being used supporting this) + scaling: Deprecated, see `scale` + + Returns: + matplotlib.figure: A matplotlib figure object of the pulse envelope + """ + return self.get_sample_pulse().draw(dt=dt, style=style, filename=filename, + interp_method=interp_method, scale=scale, + interactive=interactive) + + def __eq__(self, other: 'ParametricPulse') -> bool: + return super().__eq__(other) and self.parameters == other.parameters + + def __hash__(self) -> int: + return hash(self.parameters[k] for k in sorted(self.parameters)) + + +class Gaussian(ParametricPulse): + """A truncated pulse envelope shaped according to the Gaussian function whose mean is centered + at the center of the pulse (duration / 2): + + .. math:: + + f(x) = amp * exp( -(1/2) * (x - duration/2)^2 / sigma^2) ) , 0 <= x < duration + """ + + def __init__(self, + duration: int, + amp: complex, + sigma: float, + name: Optional[str] = None): + """Initialize the gaussian pulse. + + Args: + duration: Pulse length in terms of the the sampling period `dt`. + amp: The amplitude of the Gaussian envelope. + sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically + in the class docstring. + name: Display name for this pulse envelope. + """ + self._amp = complex(amp) + self._sigma = sigma + super().__init__(duration=duration, name=name) + + @property + def amp(self) -> complex: + """The Gaussian amplitude.""" + return self._amp + + @property + def sigma(self) -> float: + """The Gaussian standard deviation of the pulse width.""" + return self._sigma + + def get_sample_pulse(self) -> SamplePulse: + return gaussian(duration=self.duration, amp=self.amp, + sigma=self.sigma, zero_ends=False) + + def validate_parameters(self) -> None: + if abs(self.amp) > 1.: + raise PulseError("The amplitude norm must be <= 1, " + "found: {}".format(abs(self.amp))) + if self.sigma <= 0: + raise PulseError("Sigma must be greater than 0.") + + @property + def parameters(self) -> Dict[str, Any]: + return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma} + + def __repr__(self) -> str: + return '{}(duration={}, amp={}, sigma={})' \ + ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma) + + +class GaussianSquare(ParametricPulse): + """A square pulse with a Gaussian shaped risefall on either side: + + .. math:: + + risefall = (duration - width) / 2 + + 0 <= x < risefall + + f(x) = amp * exp( -(1/2) * (x - risefall/2)^2 / sigma^2) ) + + risefall <= x < risefall + width + + f(x) = amp + + risefall + width <= x < duration + + f(x) = amp * exp( -(1/2) * (x - (risefall + width)/2)^2 / sigma^2) ) + """ + + def __init__(self, + duration: int, + amp: complex, + sigma: float, + width: float, + name: Optional[str] = None): + """Initialize the gaussian square pulse. + + Args: + duration: Pulse length in terms of the the sampling period `dt`. + amp: The amplitude of the Gaussian and of the square pulse. + sigma: A measure of how wide or narrow the Gaussian risefall is; see the class + docstring for more details. + width: The duration of the embedded square pulse. + name: Display name for this pulse envelope. + """ + self._amp = complex(amp) + self._sigma = sigma + self._width = width + super().__init__(duration=duration, name=name) + + @property + def amp(self) -> complex: + """The Gaussian amplitude.""" + return self._amp + + @property + def sigma(self) -> float: + """The Gaussian standard deviation of the pulse width.""" + return self._sigma + + @property + def width(self) -> float: + """The width of the square portion of the pulse.""" + return self._width + + def get_sample_pulse(self) -> SamplePulse: + return gaussian_square(duration=self.duration, amp=self.amp, + width=self.width, sigma=self.sigma, + zero_ends=False) + + def validate_parameters(self) -> None: + if abs(self.amp) > 1.: + raise PulseError("The amplitude norm must be <= 1, " + "found: {}".format(abs(self.amp))) + if self.sigma <= 0: + raise PulseError("Sigma must be greater than 0.") + if self.width < 0 or self.width >= self.duration: + raise PulseError("The pulse width must be at least 0 and less than its duration.") + + @property + def parameters(self) -> Dict[str, Any]: + return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, + "width": self.width} + + def __repr__(self) -> str: + return '{}(duration={}, amp={}, sigma={}, width={})' \ + ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.width) + + +class Drag(ParametricPulse): + r"""The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse + with an additional Gaussian derivative component. It is designed to reduce the frequency + spectrum of a normal gaussian pulse near the :math:`|1\rangle` - :math:`|2\rangle` transition, + reducing the chance of leakage to the :math:`|2\rangle` state. + + .. math:: + + f(x) = Gaussian + 1j * beta * d/dx [Gaussian] + = Gaussian + 1j * beta * (-(x - duration/2) / sigma^2) [Gaussian] + + where 'Gaussian' is: + + .. math:: + + Gaussian(x, amp, sigma) = amp * exp( -(1/2) * (x - duration/2)^2 / sigma^2) ) + + References: + 1. |citation1|_ + + .. _citation1: https://link.aps.org/doi/10.1103/PhysRevA.83.012308 + + .. |citation1| replace:: *Gambetta, J. M., Motzoi, F., Merkel, S. T. & Wilhelm, F. K. + Analytic control methods for high-fidelity unitary operations + in a weakly nonlinear oscillator. Phys. Rev. A 83, 012308 (2011).* + + 2. |citation2|_ + + .. _citation2: https://link.aps.org/doi/10.1103/PhysRevLett.103.110501 + + .. |citation2| replace:: *F. Motzoi, J. M. Gambetta, P. Rebentrost, and F. K. Wilhelm + Phys. Rev. Lett. 103, 110501 – Published 8 September 2009.* + """ + + def __init__(self, + duration: int, + amp: complex, + sigma: float, + beta: float, + name: Optional[str] = None): + """Initialize the drag pulse. + + Args: + duration: Pulse length in terms of the the sampling period `dt`. + amp: The amplitude of the Drag envelope. + sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically + in the class docstring. + beta: The correction amplitude. + name: Display name for this pulse envelope. + """ + self._amp = complex(amp) + self._sigma = sigma + self._beta = beta + super().__init__(duration=duration, name=name) + + @property + def amp(self) -> complex: + """The Gaussian amplitude.""" + return self._amp + + @property + def sigma(self) -> float: + """The Gaussian standard deviation of the pulse width.""" + return self._sigma + + @property + def beta(self) -> float: + """The weighing factor for the Gaussian derivative component of the waveform.""" + return self._beta + + def get_sample_pulse(self) -> SamplePulse: + return drag(duration=self.duration, amp=self.amp, sigma=self.sigma, + beta=self.beta, zero_ends=False) + + def validate_parameters(self) -> None: + if abs(self.amp) > 1.: + raise PulseError("The amplitude norm must be <= 1, " + "found: {}".format(abs(self.amp))) + if self.sigma <= 0: + raise PulseError("Sigma must be greater than 0.") + if isinstance(self.beta, complex): + raise PulseError("Beta must be real.") + # Check if beta is too large: the amplitude norm must be <=1 for all points + if self.beta > self.sigma: + # If beta <= sigma, then the maximum amplitude is at duration / 2, which is + # already constrainted by self.amp <= 1 + + # 1. Find the first maxima associated with the beta * d/dx gaussian term + # This eq is derived from solving for the roots of the norm of the drag function. + # There is a second maxima mirrored around the center of the pulse with the same + # norm as the first, so checking the value at the first x maxima is sufficient. + argmax_x = (self.duration / 2 + - (self.sigma / self.beta) * math.sqrt(self.beta ** 2 - self.sigma ** 2)) + if argmax_x < 0: + # If the max point is out of range, either end of the pulse will do + argmax_x = 0 + + # 2. Find the value at that maximum + max_val = continuous.drag(np.array(argmax_x), sigma=self.sigma, + beta=self.beta, amp=self.amp, center=self.duration / 2) + if abs(max_val) > 1.: + raise PulseError("Beta is too large; pulse amplitude norm exceeds 1.") + + @property + def parameters(self) -> Dict[str, Any]: + return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, + "beta": self.beta} + + def __repr__(self) -> str: + return '{}(duration={}, amp={}, sigma={}, beta={})' \ + ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.beta) + + +class ConstantPulse(ParametricPulse): + """ + A simple constant pulse, with an amplitude value and a duration: + + .. math:: + + f(x) = amp , 0 <= x < duration + f(x) = 0 , elsewhere + """ + + def __init__(self, + duration: int, + amp: complex, + name: Optional[str] = None): + """ + Initialize the constant-valued pulse. + + Args: + duration: Pulse length in terms of the the sampling period `dt`. + amp: The amplitude of the constant square pulse. + name: Display name for this pulse envelope. + """ + self._amp = complex(amp) + super().__init__(duration=duration, name=name) + + @property + def amp(self) -> complex: + """The constant value amplitude.""" + return self._amp + + def get_sample_pulse(self) -> SamplePulse: + return constant(duration=self.duration, amp=self.amp) + + def validate_parameters(self) -> None: + if abs(self.amp) > 1.: + raise PulseError("The amplitude norm must be <= 1, " + "found: {}".format(abs(self.amp))) + + @property + def parameters(self) -> Dict[str, Any]: + return {"duration": self.duration, "amp": self.amp} + + def __repr__(self) -> str: + return '{}(duration={}, amp={})'.format(self.__class__.__name__, self.duration, self.amp) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py new file mode 100644 index 000000000000..36948f4374d0 --- /dev/null +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Pulses are descriptions of waveform envelopes. They can be transmitted by control electronics +to the device. +""" +import warnings +from typing import Callable, Optional +from abc import ABC, abstractmethod + +import numpy as np + +from ..channels import PulseChannel +from ..exceptions import PulseError +from ..instructions.play import Play + + +class Pulse(ABC): + """The abstract superclass for pulses. Pulses are complex-valued waveform envelopes. The + modulation phase and frequency are specified separately from ``Pulse``s. + """ + + @abstractmethod + def __init__(self, duration: int, name: Optional[str] = None): + if not isinstance(duration, (int, np.integer)): + raise PulseError('Pulse duration should be integer.') + self.duration = int(duration) + self.name = (name if name is not None + else '{}{}'.format(str(self.__class__.__name__).lower(), + self.__hash__())) + + def __call__(self, channel: PulseChannel) -> Play: + """Return new ``Play`` instruction that is fully instantiated with both ``pulse`` and a + ``channel``. + + Args: + channel: The channel that will have the pulse. + + Return: + Complete and ready to schedule ``Play``. + """ + warnings.warn("Calling a ``Pulse`` with a channel is deprecated. Instantiate ``Play`` " + "directly with a pulse and a channel.", DeprecationWarning) + return Play(self, channel) + + @abstractmethod + def draw(self, dt: float = 1, + style=None, + filename: Optional[str] = None, + interp_method: Optional[Callable] = None, + scale: float = 1, interactive: bool = False, + scaling: float = None): + """Plot the interpolated envelope of pulse. + + Args: + dt: Time interval of samples. + style (Optional[PulseStyle]): A style sheet to configure plot appearance + filename: Name required to save pulse image + interp_method: A function for interpolation + scale: Relative visual scaling of waveform amplitudes + interactive: When set true show the circuit in a new window + (this depends on the matplotlib backend being used supporting this) + scaling: Deprecated, see `scale` + + Returns: + matplotlib.figure: A matplotlib figure object of the pulse envelope + """ + raise NotImplementedError + + @abstractmethod + def __eq__(self, other: 'Pulse') -> bool: + return isinstance(other, type(self)) + + @abstractmethod + def __hash__(self) -> int: + raise NotImplementedError diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py new file mode 100644 index 000000000000..83ff1d2ddcda --- /dev/null +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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 pulse that is described by complex-valued sample points.""" +import warnings +from typing import Callable, Union, List, Optional + +import numpy as np + +from ..exceptions import PulseError +from .pulse import Pulse + + +class SamplePulse(Pulse): + """A pulse specified completely by complex-valued samples; each sample is played for the + duration of the backend cycle-time, dt. + """ + + def __init__(self, samples: Union[np.ndarray, List[complex]], + name: Optional[str] = None, + epsilon: float = 1e-7): + """Create new sample pulse command. + + Args: + samples: Complex array of the samples in the pulse envelope. + name: Unique name to identify the pulse. + epsilon: Pulse sample norm tolerance for clipping. + If any sample's norm exceeds unity by less than or equal to epsilon + it will be clipped to unit norm. If the sample + norm is greater than 1+epsilon an error will be raised. + """ + samples = np.asarray(samples, dtype=np.complex_) + self._samples = self._clip(samples, epsilon=epsilon) + super().__init__(duration=len(samples), name=name) + + @property + def samples(self) -> np.ndarray: + """Return sample values.""" + return self._samples + + def _clip(self, samples: np.ndarray, epsilon: float = 1e-7) -> np.ndarray: + """If samples are within epsilon of unit norm, clip sample by reducing norm by (1-epsilon). + + If difference is greater than epsilon error is raised. + + Args: + samples: Complex array of the samples in the pulse envelope. + epsilon: Pulse sample norm tolerance for clipping. + If any sample's norm exceeds unity by less than or equal to epsilon + it will be clipped to unit norm. If the sample + norm is greater than 1+epsilon an error will be raised. + + Returns: + Clipped pulse samples. + + Raises: + PulseError: If there exists a pulse sample with a norm greater than 1+epsilon. + """ + samples_norm = np.abs(samples) + to_clip = (samples_norm > 1.) & (samples_norm <= 1. + epsilon) + + if np.any(to_clip): + # first try normalizing by the abs value + clip_where = np.argwhere(to_clip) + clip_angle = np.angle(samples[clip_where]) + clipped_samples = np.exp(1j*clip_angle, dtype=np.complex_) + + # if norm still exceed one subtract epsilon + # required for some platforms + clipped_sample_norms = np.abs(clipped_samples) + to_clip_epsilon = clipped_sample_norms > 1. + if np.any(to_clip_epsilon): + clip_where_epsilon = np.argwhere(to_clip_epsilon) + clipped_samples_epsilon = np.exp( + (1-epsilon)*1j*clip_angle[clip_where_epsilon], dtype=np.complex_) + clipped_samples[clip_where_epsilon] = clipped_samples_epsilon + + # update samples with clipped values + samples[clip_where] = clipped_samples + samples_norm[clip_where] = np.abs(clipped_samples) + + if np.any(samples_norm > 1.): + raise PulseError('Pulse contains sample with norm greater than 1+epsilon.') + + return samples + + def draw(self, dt: float = 1, + style=None, + filename: Optional[str] = None, + interp_method: Optional[Callable] = None, + scale: float = 1, interactive: bool = False, + scaling: float = None): + """Plot the interpolated envelope of pulse. + + Args: + dt: Time interval of samples. + style (Optional[PulseStyle]): A style sheet to configure plot appearance. + filename: Name required to save pulse image. + interp_method: A function for interpolation. + scale: Relative visual scaling of waveform amplitudes. + interactive: When set true show the circuit in a new window. + (This depends on the matplotlib backend being used.) + scaling: Deprecated, see `scale`, + + Returns: + matplotlib.figure: A matplotlib figure object of the pulse envelope + """ + # pylint: disable=invalid-name, cyclic-import + if scaling is not None: + warnings.warn( + 'The parameter "scaling" is being replaced by "scale"', + DeprecationWarning, 3) + scale = scaling + + from qiskit import visualization + + return visualization.pulse_drawer(self, dt=dt, style=style, filename=filename, + interp_method=interp_method, scale=scale, + interactive=interactive) + + def __eq__(self, other: 'SamplePulse') -> bool: + """Two SamplePulses are the same if they are of the same type + and have the same name and samples. + + Args: + other: Object to compare to. + + Returns: + True iff self and other are equal. + """ + return super().__eq__(other) and (self.samples == other.samples).all() + + def __hash__(self): + return hash(self.samples.tostring()) + + def __repr__(self): + opt = np.get_printoptions() + np.set_printoptions(threshold=50) + np.set_printoptions(**opt) + return '{}({}, name="{}")'.format(self.__class__.__name__, repr(self.samples), self.name) diff --git a/qiskit/pulse/pulse_lib/samplers/__init__.py b/qiskit/pulse/pulse_lib/samplers/__init__.py index 86f9117c941d..ef0f88acee1d 100644 --- a/qiskit/pulse/pulse_lib/samplers/__init__.py +++ b/qiskit/pulse/pulse_lib/samplers/__init__.py @@ -14,4 +14,4 @@ """Module for Samplers.""" -from .decorators import * +from .decorators import left, right, midpoint diff --git a/qiskit/pulse/pulse_lib/samplers/decorators.py b/qiskit/pulse/pulse_lib/samplers/decorators.py index 1a6ad829b89c..2e6a815d2a1f 100644 --- a/qiskit/pulse/pulse_lib/samplers/decorators.py +++ b/qiskit/pulse/pulse_lib/samplers/decorators.py @@ -135,12 +135,32 @@ def linear(times: np.ndarray, m: float, b: float) -> np.ndarray: import numpy as np -from qiskit.pulse.commands.sample_pulse import SamplePulse -from qiskit.pulse.commands.pulse_decorators import functional_pulse - +from ...exceptions import PulseError +from ..sample_pulse import SamplePulse from . import strategies +def functional_pulse(func: Callable) -> Callable: + """A decorator for generating SamplePulse from python callable. + + Args: + func: A function describing pulse envelope. + + Raises: + PulseError: when invalid function is specified. + """ + @functools.wraps(func) + def to_pulse(duration, *args, name=None, **kwargs): + """Return SamplePulse.""" + if isinstance(duration, (int, np.integer)) and duration > 0: + samples = func(duration, *args, **kwargs) + samples = np.asarray(samples, dtype=np.complex128) + return SamplePulse(samples=samples, name=name) + raise PulseError('The first argument must be an integer value representing duration.') + + return to_pulse + + def _update_annotations(discretized_pulse: Callable) -> Callable: """Update annotations of discretized continuous pulse function with duration. diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index f2fe9a19d5b0..68ebe14ce9f8 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -19,7 +19,7 @@ from enum import Enum -from qiskit.pulse import commands, channels, instructions +from qiskit.pulse import commands, channels, instructions, pulse_lib from qiskit.pulse.exceptions import PulseError from qiskit.pulse.configuration import Kernel, Discriminator from qiskit.pulse.parser import parse_string_expr @@ -347,6 +347,33 @@ def convert_parametric(self, shift, instruction): } return self._qobj_model(**command_dict) + @bind_instruction(instructions.Play) + def convert_play(self, shift, instruction): + """Return the converted `Play`. + + Args: + shift (int): Offset time. + instruction (Play): An instance of Play. + Returns: + dict: Dictionary of required parameters. + """ + if isinstance(instruction.pulse, pulse_lib.ParametricPulse): + command_dict = { + 'name': 'parametric_pulse', + 'pulse_shape': ParametricPulseShapes(type(instruction.pulse)).name, + 't0': shift + instruction.start_time, + 'ch': instruction.channel.name, + 'parameters': instruction.pulse.parameters + } + else: + command_dict = { + 'name': instruction.name, + 't0': shift + instruction.start_time, + 'ch': instruction.channel.name + } + + return self._qobj_model(**command_dict) + @bind_instruction(instructions.Snapshot) def convert_snapshot(self, shift, instruction): """Return converted `Snapshot`. @@ -550,11 +577,11 @@ def bind_pulse(self, pulse): pulse (PulseLibraryItem): Pulse to bind """ # pylint: disable=unused-variable - pulse = commands.SamplePulse(pulse.samples, pulse.name) + pulse = pulse_lib.SamplePulse(pulse.samples, pulse.name) @self.bind_name(pulse.name) def convert_named_drive(self, instruction): - """Return converted `PulseInstruction`. + """Return converted `Play`. Args: instruction (PulseQobjInstruction): pulse qobj @@ -563,7 +590,7 @@ def convert_named_drive(self, instruction): """ t0 = instruction.t0 channel = self.get_channel(instruction.ch) - return pulse(channel) << t0 + return instructions.Play(pulse, channel) << t0 @bind_name('parametric_pulse') def convert_parametric(self, instruction): diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py index e7dd987c7855..a608780c8357 100644 --- a/qiskit/visualization/pulse/matplotlib.py +++ b/qiskit/visualization/pulse/matplotlib.py @@ -33,7 +33,7 @@ MeasureChannel, AcquireChannel, SnapshotChannel) from qiskit.pulse.commands import FrameChangeInstruction -from qiskit.pulse import (SamplePulse, FrameChange, PersistentValue, Snapshot, +from qiskit.pulse import (SamplePulse, FrameChange, PersistentValue, Snapshot, Play, Acquire, PulseError, ParametricPulse, SetFrequency, ShiftPhase) @@ -59,18 +59,23 @@ def __init__(self, t0, tf): self._labels = None self.enable = False - def add_instruction(self, start_time, pulse): + def add_instruction(self, start_time, instruction): """Add new pulse instruction to channel. Args: start_time (int): Starting time of instruction - pulse (Instruction): Instruction object to be added + instruction (Instruction): Instruction object to be added """ - + if instruction.command is not None: + pulse = instruction.command + elif isinstance(instruction, Play): + pulse = instruction.pulse + else: + pulse = instruction if start_time in self.pulses.keys(): - self.pulses[start_time].append(pulse.command) + self.pulses[start_time].append(pulse) else: - self.pulses[start_time] = [pulse.command] + self.pulses[start_time] = [pulse] @property def waveform(self): diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index b7407e5f564a..a369d56d93f6 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -11,12 +11,14 @@ features: sched += Delay(5)(DriveChannel(0)) sched += ShiftPhase(np.pi)(DriveChannel(0)) + sched += SamplePulse([1.0, ...])(DriveChannel(0)) sched += Acquire(100)(AcquireChannel(0), MemorySlot(0)) or, equivalently (though less used):: sched += DelayInstruction(Delay(5), DriveChannel(0)) sched += ShiftPhaseInstruction(ShiftPhase(np.pi), DriveChannel(0)) + sched += PulseInstruction(SamplePulse([1.0, ...]), DriveChannel(0)) sched += AcquireInstruction(Acquire(100), AcquireChannel(0), MemorySlot(0)) @@ -25,10 +27,26 @@ features: sched += Delay(5, DriveChannel(0)) sched += ShiftPhase(np.pi, DriveChannel(0)) - sched += Acquire(100, AcquireChannel(0), MemorySlot(0)) + sched += Play(SamplePulse([1.0, ...]), DriveChannel(0)) sched += SetFrequency(5.5, DriveChannel(0)) # New instruction! + sched += Acquire(100, AcquireChannel(0), MemorySlot(0)) + - | + There is now a :py:class:`~qiskit.pulse.instructions.Play` instruction + which takes a description of a pulse envelope and a channel. There is a + new :py:class:`~qiskit.pulse.pulse_lib.Pulse` class in the ``pulse_lib`` + from which the pulse envelope description should subclass. + + For example:: + Play(SamplePulse([0.1]*10), DriveChannel(0)) + Play(ConstantPulse(duration=10, amp=0.1), DriveChannel(0)) deprecations: + - | + py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and + py:class:`~qiskit.pulse.pulse_lib.ParametricPulse` s (e.g. ``Gaussian``) + now subclass from :py:class:`~qiskit.pulse.pulse_lib.Pulse` and have been + moved to the ``pulse_lib``. The previous path via ``pulse.commands`` is + deprecated. - | ``DelayInstruction`` has been deprecated and replaced by :py:class:`~qiskit.pulse.instruction.Delay`. This new instruction has been @@ -56,6 +74,12 @@ deprecations: calling a command on a channel will be supported:: ShiftPhase()() + - | + The ``call`` method of py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and + py:class:`~qiskit.pulse.pulse_lib.ParametricPulse` s have been deprecated. + The migration is as follows:: + + Pulse(<*args>)() -> Play(Pulse(*args), ) - | ``AcquireInstruction`` has been deprecated and replaced by :py:class:`~qiskit.pulse.instructions.Acquire`. The changes are:: diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index 880f445484a1..4b3b07f66978 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -572,9 +572,8 @@ def test_pulse_name_conflicts(self): **self.config) validate_qobj_against_schema(qobj) - self.assertNotEqual(qobj.config.pulse_library[1], 'pulse0') - self.assertEqual(qobj.experiments[0].instructions[0].name, 'pulse0') - self.assertNotEqual(qobj.experiments[0].instructions[1].name, 'pulse0') + self.assertNotEqual(qobj.config.pulse_library[0].name, + qobj.config.pulse_library[1].name) def test_pulse_name_conflicts_in_other_schedule(self): """Test two pulses with the same name in different schedule can be resolved.""" diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index a820deb4875f..ebffaacbb752 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -14,11 +14,12 @@ """Unit tests for pulse instructions.""" -from qiskit.pulse import Delay, DriveChannel, ShiftPhase, Snapshot +from qiskit.pulse import DriveChannel, pulse_lib +from qiskit.pulse import Delay, Play, ShiftPhase, Snapshot from qiskit.test import QiskitTestCase -class TestDelayCommand(QiskitTestCase): +class TestDelay(QiskitTestCase): """Delay tests.""" def test_delay(self): @@ -52,3 +53,15 @@ def test_default(self): self.assertEqual(snapshot.name, "test_name") self.assertEqual(snapshot.type, "state") self.assertEqual(snapshot.duration, 0) + + +class TestPlay(QiskitTestCase): + """Play tests.""" + + def test_play(self): + """Test basic play instruction.""" + duration = 64 + pulse = pulse_lib.SamplePulse([1.0] * duration, name='test') + play = Play(pulse, DriveChannel(1)) + self.assertEqual(play.name, pulse.name) + self.assertEqual(play.duration, duration) diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py index 99d62cb7709a..0fda128ffaad 100644 --- a/test/python/pulse/test_schedule.py +++ b/test/python/pulse/test_schedule.py @@ -24,7 +24,7 @@ functional_pulse, PulseInstruction, Gaussian, Drag, GaussianSquare, ConstantPulse) -from qiskit.pulse import pulse_lib, SamplePulse, ShiftPhase, Instruction, SetFrequency +from qiskit.pulse import pulse_lib, Play, SamplePulse, ShiftPhase, Instruction, SetFrequency from qiskit.pulse.timeslots import TimeslotCollection, Interval from qiskit.pulse.exceptions import PulseError from qiskit.pulse.schedule import Schedule, ParameterizedSchedule @@ -395,7 +395,7 @@ def test_parametric_commands_in_sched(self): sigma=8, width=140)(MeasureChannel(0)) << sched_duration sched += Acquire(duration=1500)(AcquireChannel(0), [MemorySlot(0)]) << sched_duration self.assertEqual(sched.duration, 1525) - self.assertTrue('sigma' in sched.instructions[0][1].command.parameters) + self.assertTrue('sigma' in sched.instructions[0][1].pulse.parameters) class TestDelay(BaseTestSchedule): @@ -415,7 +415,7 @@ def test_delay_drive_channel(self): self.assertIsInstance(sched, Schedule) pulse_instr = sched.instructions[-1] # assert last instruction is pulse - self.assertIsInstance(pulse_instr[1], PulseInstruction) + self.assertIsInstance(pulse_instr[1], Play) # assert pulse is scheduled at time 10 self.assertEqual(pulse_instr[0], 10) # should fail due to overlap @@ -526,12 +526,12 @@ def test_filter_inst_types(self): # test two instruction types only_pulse_and_fc, no_pulse_and_fc = \ - self._filter_and_test_consistency(sched, instruction_types=[PulseInstruction, + self._filter_and_test_consistency(sched, instruction_types=[Play, ShiftPhase]) for _, inst in only_pulse_and_fc.instructions: - self.assertIsInstance(inst, (PulseInstruction, ShiftPhase)) + self.assertIsInstance(inst, (Play, ShiftPhase)) for _, inst in no_pulse_and_fc.instructions: - self.assertFalse(isinstance(inst, (PulseInstruction, ShiftPhase))) + self.assertFalse(isinstance(inst, (Play, ShiftPhase))) self.assertEqual(len(only_pulse_and_fc.instructions), 4) self.assertEqual(len(no_pulse_and_fc.instructions), 3) @@ -578,7 +578,7 @@ def test_filter_intervals(self): self.assertIsInstance(filtered.instructions[0][1], Acquire) self.assertEqual(len(excluded.instructions), 4) self.assertEqual(excluded.instructions[3][0], 90) - self.assertIsInstance(excluded.instructions[3][1], PulseInstruction) + self.assertIsInstance(excluded.instructions[3][1], Play) # split instructions based on the interval # (none should be, though they have some overlap with some of the instructions) @@ -617,26 +617,26 @@ def test_filter_multiple(self): # occurring in the time interval (25, 100) filtered, excluded = self._filter_and_test_consistency(sched, channels={self.config.drive(0)}, - instruction_types=[PulseInstruction], + instruction_types=[Play], time_ranges=[(25, 100)]) for time, inst in filtered.instructions: - self.assertIsInstance(inst, PulseInstruction) + self.assertIsInstance(inst, Play) self.assertTrue(all([chan.index == 0 for chan in inst.channels])) self.assertTrue(25 <= time <= 100) self.assertEqual(len(excluded.instructions), 5) self.assertTrue(excluded.instructions[0][1].channels[0] == DriveChannel(0)) self.assertTrue(excluded.instructions[2][0] == 30) - # split based on PulseInstructions in the specified intervals + # split based on Plays in the specified intervals filtered, excluded = self._filter_and_test_consistency(sched, - instruction_types=[PulseInstruction], + instruction_types=[Play], time_ranges=[(25, 100), (0, 11)]) self.assertTrue(len(excluded.instructions), 3) for time, inst in filtered.instructions: - self.assertIsInstance(inst, (ShiftPhase, PulseInstruction)) + self.assertIsInstance(inst, (ShiftPhase, Play)) self.assertTrue(len(filtered.instructions), 4) # make sure the PulseInstruction not in the intervals is maintained - self.assertIsInstance(excluded.instructions[0][1], PulseInstruction) + self.assertIsInstance(excluded.instructions[0][1], Play) # split based on Acquire in the specified intervals filtered, excluded = self._filter_and_test_consistency(sched, diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index ebcf828d6882..0484c5cbdcbe 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -206,14 +206,12 @@ def setUp(self): def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" - cmd = self.linear - instruction = cmd(DriveChannel(0)) << 10 + instruction = self.linear(DriveChannel(0)) qobj = PulseQobjInstruction(name='linear', ch='d0', t0=10) converted_instruction = self.converter(qobj) - self.assertEqual(converted_instruction.timeslots, instruction.timeslots) - self.assertEqual(converted_instruction.instructions[0][-1].command, cmd) + self.assertEqual(converted_instruction.instructions[0][-1], instruction) def test_parametric_pulses(self): """Test converted qobj from ParametricInstruction.""" @@ -226,7 +224,7 @@ def test_parametric_pulses(self): parameters={'duration': 25, 'sigma': 15, 'amp': -0.5 + 0.2j}) converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.timeslots, instruction.timeslots) - self.assertEqual(converted_instruction.instructions[0][-1].command, instruction.command) + self.assertEqual(converted_instruction.instructions[0][-1], instruction) def test_frame_change(self): """Test converted qobj from FrameChangeInstruction.""" diff --git a/test/python/visualization/references/parametric_matplotlib_ref.png b/test/python/visualization/references/parametric_matplotlib_ref.png index b0c3793d2a6c..fd60b14eb005 100644 Binary files a/test/python/visualization/references/parametric_matplotlib_ref.png and b/test/python/visualization/references/parametric_matplotlib_ref.png differ diff --git a/test/python/visualization/test_pulse_visualization_output.py b/test/python/visualization/test_pulse_visualization_output.py index 90fbcc2dff33..a654b10f95e3 100644 --- a/test/python/visualization/test_pulse_visualization_output.py +++ b/test/python/visualization/test_pulse_visualization_output.py @@ -23,9 +23,8 @@ from qiskit.visualization import pulse_drawer from qiskit.pulse.channels import (DriveChannel, MeasureChannel, ControlChannel, AcquireChannel, MemorySlot, RegisterSlot) -from qiskit.pulse.commands import (FrameChange, Acquire, ConstantPulse, Snapshot, Delay, - Gaussian) -from qiskit.pulse.instructions import SetFrequency +from qiskit.pulse.commands import FrameChange +from qiskit.pulse.instructions import SetFrequency, Play, Acquire, Delay, Snapshot from qiskit.pulse.schedule import Schedule from qiskit.pulse import pulse_lib @@ -65,7 +64,8 @@ def sample_schedule(self): delay = Delay(100) sched = Schedule() sched = sched.append(gp0(DriveChannel(0))) - sched = sched.insert(0, ConstantPulse(duration=60, amp=0.2 + 0.4j)(ControlChannel(0))) + sched = sched.insert(0, pulse_lib.ConstantPulse(duration=60, amp=0.2 + 0.4j)( + ControlChannel(0))) sched = sched.insert(60, FrameChange(phase=-1.57)(DriveChannel(0))) sched = sched.insert(60, SetFrequency(8.0, DriveChannel(0))) sched = sched.insert(30, gp1(DriveChannel(1))) @@ -85,9 +85,22 @@ def sample_schedule(self): @unittest.skip('Useful for refactoring purposes, skipping by default.') def test_parametric_pulse_schedule(self): """Test that parametric instructions/schedules can be drawn.""" - filename = self._get_resource_path('current_schedule_matplotlib_ref.png') + filename = self._get_resource_path('current_parametric_matplotlib_ref.png') + schedule = Schedule(name='test_parametric') + schedule += pulse_lib.Gaussian(duration=25, sigma=4, amp=0.5j)(DriveChannel(0)) + pulse_drawer(schedule, filename=filename) + self.assertImagesAreEqual(filename, self.parametric_matplotlib_reference) + os.remove(filename) + + @unittest.skipIf(not HAS_MATPLOTLIB, 'matplotlib not available.') + @unittest.skip('Useful for refactoring purposes, skipping by default.') + def test_play(self): + """Test that Play instructions can be drawn. The output should be the same as the + parametric_pulse_schedule test. + """ + filename = self._get_resource_path('current_play_matplotlib_ref.png') schedule = Schedule(name='test_parametric') - schedule += Gaussian(duration=25, sigma=4, amp=0.5j)(DriveChannel(0)) + schedule += Play(pulse_lib.Gaussian(duration=25, sigma=4, amp=0.5j), DriveChannel(0)) pulse_drawer(schedule, filename=filename) self.assertImagesAreEqual(filename, self.parametric_matplotlib_reference) os.remove(filename)