From 5ae572aee63e86f083de5a75ef7ba9e3d65bcf6a Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 20 Feb 2020 14:30:32 -0500 Subject: [PATCH 01/38] Deprecate PulseCommands, create new Play Instruction-implementation. The Play instruction takes a Pulse and a PulseChannel. Pulse is a new class within the pulse library that is very close to the deprecated PulseCommand abstract class. The PulseCommand implementations (SamplePulse and the ParametricPulse subclasses) have been moved to the pulse library as well, subclassing from Pulse, and all valid arguments to Play. --- qiskit/pulse/commands/parametric_pulses.py | 369 +----------------- qiskit/pulse/commands/sample_pulse.py | 153 +------- qiskit/pulse/instructions/__init__.py | 1 + qiskit/pulse/instructions/play.py | 53 +++ qiskit/pulse/pulse_lib/__init__.py | 4 + qiskit/pulse/pulse_lib/parametric_pulses.py | 395 ++++++++++++++++++++ qiskit/pulse/pulse_lib/pulse.py | 61 +++ qiskit/pulse/pulse_lib/sample_pulse.py | 158 ++++++++ 8 files changed, 683 insertions(+), 511 deletions(-) create mode 100644 qiskit/pulse/instructions/play.py create mode 100644 qiskit/pulse/pulse_lib/parametric_pulses.py create mode 100644 qiskit/pulse/pulse_lib/pulse.py create mode 100644 qiskit/pulse/pulse_lib/sample_pulse.py diff --git a/qiskit/pulse/commands/parametric_pulses.py b/qiskit/pulse/commands/parametric_pulses.py index 767964797da3..0bfe0a9009c6 100644 --- a/qiskit/pulse/commands/parametric_pulses.py +++ b/qiskit/pulse/commands/parametric_pulses.py @@ -37,370 +37,13 @@ class ParametricPulseShapes(Enum): ... new_supported_pulse_name = commands.YourPulseCommandClass """ -from abc import abstractmethod -from typing import Any, Callable, Dict, Optional -import math +import warnings -import numpy as np +from qiskit.pulse.pulse_lib import ParametricPulse, Gaussian, GaussianSquare, Drag, ConstantPulse -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): + warnings.warn("TODO", DeprecationWarning) diff --git a/qiskit/pulse/commands/sample_pulse.py b/qiskit/pulse/commands/sample_pulse.py index 91a23b254f60..f1fb6bde0d63 100644 --- a/qiskit/pulse/commands/sample_pulse.py +++ b/qiskit/pulse/commands/sample_pulse.py @@ -12,160 +12,17 @@ # 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 ..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 +from typing import Optional - # 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 +from ..channels import PulseChannel +from ..pulse_lib import SamplePulse class PulseInstruction(Instruction): """Instruction to drive a pulse to an `PulseChannel`.""" def __init__(self, command: SamplePulse, channel: PulseChannel, name: Optional[str] = None): - super().__init__(command, channel, name=name) + warnings.warn("TODO", DeprecationWarning) diff --git a/qiskit/pulse/instructions/__init__.py b/qiskit/pulse/instructions/__init__.py index 820c670c856c..d4d9ea4ffd09 100644 --- a/qiskit/pulse/instructions/__init__.py +++ b/qiskit/pulse/instructions/__init__.py @@ -22,3 +22,4 @@ sequence of scheduled Pulse ``Instruction`` s over many channels. """ from .instruction import Instruction +from .play import Play diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py new file mode 100644 index 000000000000..95841ee0b049 --- /dev/null +++ b/qiskit/pulse/instructions/play.py @@ -0,0 +1,53 @@ +# -*- 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. + +"""TODO""" +from typing import Optional + +from qiskit.pulse.channels import PulseChannel + +from .instruction import Instruction + + +class Play(Instruction): + """TODO""" + + def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = None): + """TODO""" + self._pulse + self._channel = channel + super().__init__(pulse.duration, channel) + + @property + def channel: + """TODO""" + return channel + + @property + def pulse(self) -> Pulse: + """TODO""" + return pulse + + @property + def pulse_name(self) -> str: + return pulse.name + + @property + def operands(self): + """TODO""" + return # TODO + + def __repr__(self): + """TODO""" + pass diff --git a/qiskit/pulse/pulse_lib/__init__.py b/qiskit/pulse/pulse_lib/__init__.py index dd0e35a95fa8..82cc0835d9e8 100644 --- a/qiskit/pulse/pulse_lib/__init__.py +++ b/qiskit/pulse/pulse_lib/__init__.py @@ -15,3 +15,7 @@ """Module for builtin ``pulse_lib``.""" from .discrete import * +from .parametric_pulses import (ParametricPulse, ParametricInstruction, Gaussian, GaussianSquare, + Drag, ConstantPulse) +from .pulse import Pulse +from .sample_pulse import SamplePulse \ No newline at end of file diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py new file mode 100644 index 000000000000..391877742031 --- /dev/null +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -0,0 +1,395 @@ +# -*- 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. + + +# TODO: Update the below +"""Parametric pulse commands module. These are pulse commands 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 qiskit.pulse.channels import PulseChannel +from qiskit.pulse.exceptions import PulseError + +from . import continuous +from .discrete import gaussian, gaussian_square, drag, constant +from .pulse import Pulse +from .sample_pulse import SamplePulse + + +class ParametricPulse(Pulse): + """The abstract superclass for parametric pulses.""" + + @abstractmethod + def __init__(self, duration: int, channel: Optional[PulseChannel] = None): + """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() + + def __call__(self): + """TODO, deprecated""" + pass + + @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: Optional['PulseStyle'] = 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: 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, + channel: Optional[PulseChannel] = 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. + """ + 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, + channel: Optional[PulseChannel] = 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. + """ + 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): + """ + 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 |1>-|2> transition, reducing the chance of + leakage to the |2> 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) ) + + Ref: + [1] 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] 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, + channel: Optional[PulseChannel] = 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. + """ + 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, + channel: Optional[PulseChannel] = 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. + """ + 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) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py new file mode 100644 index 000000000000..a1e5996e83d1 --- /dev/null +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -0,0 +1,61 @@ +# -*- 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. + +"""Any command which implements a transmit signal on a channel.""" +from typing import Callable, List, Optional + +from abc import ABC, abstractmethod + +from qiskit.pulse.channels import PulseChannel +from .instruction import Instruction +from .command import Command + + +class Pulse(ABC): + """The abstract superclass for pulses.""" + + @abstractmethod #??? + def __init__(self, duration: int = None): + if isinstance(duration, (int, np.integer)): + self._duration = int(duration) + else: + raise PulseError('Pulse duration should be integer.') + + def __call__(self): + """TODO, deprecated""" + pass + + @abstractmethod + def draw(self, dt: float = 1, + style: Optional['PulseStyle'] = 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: 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 + """ + pass diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py new file mode 100644 index 000000000000..53c2915e4c8f --- /dev/null +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -0,0 +1,158 @@ +# -*- 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. + +"""TODO""" +import warnings + +from typing import Callable, Union, List, Optional + +from qiskit.pulse.channels import PulseChannel +from .pulse import Pulse + + +class SamplePulse(Pulse): + """TODO""" + + def __init__(self, samples: Union[np.ndarray, List[complex]], + channel: Optional[PulseChannel] = None, + 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: Optional['PulseStyle'] = 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: 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 + + def __call__(self): + """TODO, deprecate""" + pass From 3433240add76e9aa4289a97d0fc85418ef6021fd Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 9 Mar 2020 16:40:02 -0400 Subject: [PATCH 02/38] Update import statements in Pulse --- qiskit/pulse/pulse_lib/pulse.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index a1e5996e83d1..5b1fa8dd5a39 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -17,9 +17,7 @@ from abc import ABC, abstractmethod -from qiskit.pulse.channels import PulseChannel -from .instruction import Instruction -from .command import Command +from ..channels import PulseChannel class Pulse(ABC): From 2a71ffa127fe91c7a9f1a48a75cc778d029c23a4 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Wed, 11 Mar 2020 16:54:14 -0400 Subject: [PATCH 03/38] Fill in the implementation of some IP work like __call__ --- qiskit/pulse/instructions/play.py | 25 +++++++++++---------- qiskit/pulse/pulse_lib/parametric_pulses.py | 4 ---- qiskit/pulse/pulse_lib/pulse.py | 18 +++++++++++---- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 95841ee0b049..f47e925b5fc2 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -25,29 +25,30 @@ class Play(Instruction): def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = None): """TODO""" - self._pulse + self._pulse = pulse self._channel = channel - super().__init__(pulse.duration, channel) + super().__init__(pulse.duration, channel, name=name) @property - def channel: + def operands(self) -> List: """TODO""" - return channel + return [self.pulse, self.channel] @property - def pulse(self) -> Pulse: + def channel(self) -> PulseChannel: """TODO""" - return pulse + return self._channel @property - def pulse_name(self) -> str: - return pulse.name + def pulse(self) -> Pulse: + """TODO""" + return self._pulse @property - def operands(self): - """TODO""" - return # TODO + def pulse_name(self) -> str: + return self.pulse.name def __repr__(self): """TODO""" - pass + return "{}({}, {}, name={})".format(self.__class__.__name__, self.pulse, self.channel, + self.name) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index 391877742031..8254933b9614 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -65,10 +65,6 @@ def __init__(self, duration: int, channel: Optional[PulseChannel] = None): super().__init__(duration=duration) self.validate_parameters() - def __call__(self): - """TODO, deprecated""" - pass - @abstractmethod def get_sample_pulse(self) -> SamplePulse: """Return a SamplePulse with samples filled according to the formula that the pulse diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 5b1fa8dd5a39..6b3d7ca19e0c 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -30,9 +30,19 @@ def __init__(self, duration: int = None): else: raise PulseError('Pulse duration should be integer.') - def __call__(self): - """TODO, deprecated""" - pass + def __call__(self, channel: PulseChannel) -> 'Instruction': + """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, @@ -56,4 +66,4 @@ def draw(self, dt: float = 1, Returns: matplotlib.figure: A matplotlib figure object of the pulse envelope """ - pass + raise NotImplementedError From 9bbc01feb3fd451359b1bd5fa65b309e85cb05d4 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Fri, 13 Mar 2020 15:01:39 -0400 Subject: [PATCH 04/38] Fill in and update documentation, complete import paths --- qiskit/pulse/__init__.py | 10 ++-- qiskit/pulse/commands/delay.py | 3 +- qiskit/pulse/commands/parametric_pulses.py | 38 ++++---------- qiskit/pulse/commands/sample_pulse.py | 7 ++- qiskit/pulse/instructions/play.py | 48 +++++++++++------ qiskit/pulse/pulse_lib/__init__.py | 5 +- qiskit/pulse/pulse_lib/parametric_pulses.py | 57 ++++++++++----------- qiskit/pulse/pulse_lib/pulse.py | 19 ++++--- qiskit/pulse/pulse_lib/sample_pulse.py | 36 ++++++------- 9 files changed, 112 insertions(+), 111 deletions(-) diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 1d6a551eac5e..5954a46e4aeb 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -133,14 +133,12 @@ from .channels import (DriveChannel, MeasureChannel, AcquireChannel, ControlChannel, RegisterSlot, MemorySlot) from .cmd_def import CmdDef -from .commands import (Acquire, AcquireInstruction, FrameChange, - PersistentValue, SamplePulse, Kernel, - Discriminator, ParametricPulse, - ParametricInstruction, Gaussian, - GaussianSquare, Drag, ConstantPulse, functional_pulse) +from .commands import (Acquire, AcquireInstruction, FrameChange, PersistentValue, Kernel, + Discriminator, functional_pulse) from .configuration import LoConfig, LoRange from .exceptions import PulseError from .instruction_schedule_map import InstructionScheduleMap -from .instructions import Instruction, Delay, Snapshot +from .instructions import Instruction, Delay, Play, Snapshot from .interfaces import ScheduleComponent +from .pulse_lib import SamplePulse, Gaussian, GaussianSquare, Drag, ConstantPulse, ParametricPulse 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 0bfe0a9009c6..b28add4d9835 100644 --- a/qiskit/pulse/commands/parametric_pulses.py +++ b/qiskit/pulse/commands/parametric_pulses.py @@ -12,38 +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. - -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 -""" +"""Deprecated path to parametric pulses.""" import warnings +# pylint: disable=unused-import + from qiskit.pulse.pulse_lib import ParametricPulse, Gaussian, GaussianSquare, Drag, ConstantPulse +from qiskit.pulse.channels import Channel class ParametricInstruction: """Instruction to drive a parametric pulse to an `PulseChannel`.""" - def __init__(self): - warnings.warn("TODO", DeprecationWarning) + 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/sample_pulse.py b/qiskit/pulse/commands/sample_pulse.py index f1fb6bde0d63..1392a2c9400b 100644 --- a/qiskit/pulse/commands/sample_pulse.py +++ b/qiskit/pulse/commands/sample_pulse.py @@ -19,10 +19,15 @@ from ..channels import PulseChannel from ..pulse_lib import SamplePulse +from ..instructions import Instruction class PulseInstruction(Instruction): """Instruction to drive a pulse to an `PulseChannel`.""" def __init__(self, command: SamplePulse, channel: PulseChannel, name: Optional[str] = None): - warnings.warn("TODO", DeprecationWarning) + 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/play.py b/qiskit/pulse/instructions/play.py index f47e925b5fc2..50c6137e165e 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -12,43 +12,57 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""TODO""" -from typing import Optional - -from qiskit.pulse.channels import PulseChannel +"""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 ..pulse_lib import Pulse from .instruction import Instruction class Play(Instruction): - """TODO""" + """An instruction specifying the exact time dynamics of the output signal envelope for a + limited time on the channel that the instruction is operating on. + + This signal 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): - """TODO""" + """Create a new pulse instruction. + + Args: + pulse: A pulse waveform description, such as + :py:class:`~qiskit.pulse.pulse_lib.SamplePulse`. + channel: The channel that will have the pulse. + name: Name of the delay for display purposes. + """ self._pulse = pulse self._channel = channel super().__init__(pulse.duration, channel, name=name) @property - def operands(self) -> List: - """TODO""" + def operands(self) -> List[Union[Pulse, PulseChannel]]: + """Return a list of instruction operands.""" return [self.pulse, self.channel] - @property - def channel(self) -> PulseChannel: - """TODO""" - return self._channel - @property def pulse(self) -> Pulse: - """TODO""" + """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 pulse_name(self) -> str: - return self.pulse.name + def channel(self) -> PulseChannel: + """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is + scheduled on. + """ + return self._channel def __repr__(self): - """TODO""" 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 82cc0835d9e8..e14ed399f7e4 100644 --- a/qiskit/pulse/pulse_lib/__init__.py +++ b/qiskit/pulse/pulse_lib/__init__.py @@ -15,7 +15,6 @@ """Module for builtin ``pulse_lib``.""" from .discrete import * -from .parametric_pulses import (ParametricPulse, ParametricInstruction, Gaussian, GaussianSquare, - Drag, ConstantPulse) +from .parametric_pulses import ParametricPulse, Gaussian, GaussianSquare, Drag, ConstantPulse from .pulse import Pulse -from .sample_pulse import SamplePulse \ No newline at end of file +from .sample_pulse import SamplePulse diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index 8254933b9614..da11af986fe5 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -12,9 +12,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. - -# TODO: Update the below -"""Parametric pulse commands module. These are pulse commands which are described by a specified +"""Parametric waveforms module. These are pulses which are described by a specified parameterization. If a backend supports parametric pulses, it will have the attribute @@ -43,9 +41,7 @@ class ParametricPulseShapes(Enum): import math import numpy as np -from qiskit.pulse.channels import PulseChannel -from qiskit.pulse.exceptions import PulseError - +from ..exceptions import PulseError from . import continuous from .discrete import gaussian, gaussian_square, drag, constant from .pulse import Pulse @@ -56,7 +52,7 @@ class ParametricPulse(Pulse): """The abstract superclass for parametric pulses.""" @abstractmethod - def __init__(self, duration: int, channel: Optional[PulseChannel] = None): + def __init__(self, duration: int): """Create a parametric pulse and validate the input parameters. Args: @@ -115,9 +111,8 @@ def draw(self, dt: float = 1, 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): + """A truncated pulse envelope shaped according to the Gaussian function whose mean is centered + at the center of the pulse (duration / 2): .. math:: @@ -127,8 +122,7 @@ class Gaussian(ParametricPulse): def __init__(self, duration: int, amp: complex, - sigma: float, - channel: Optional[PulseChannel] = None): + sigma: float): """Initialize the gaussian pulse. Args: @@ -142,11 +136,13 @@ def __init__(self, super().__init__(duration=duration) @property - def amp(self): + def amp(self) -> complex: + """The Gaussian amplitude.""" return self._amp @property - def sigma(self): + def sigma(self) -> float: + """The Gaussian standard deviation of the pulse width.""" return self._sigma def get_sample_pulse(self) -> SamplePulse: @@ -170,8 +166,7 @@ def __repr__(self): class GaussianSquare(ParametricPulse): - """ - A square pulse with a Gaussian shaped risefall on either side: + """A square pulse with a Gaussian shaped risefall on either side: .. math:: @@ -194,8 +189,7 @@ def __init__(self, duration: int, amp: complex, sigma: float, - width: float, - channel: Optional[PulseChannel] = None): + width: float): """Initialize the gaussian square pulse. Args: @@ -211,15 +205,18 @@ def __init__(self, super().__init__(duration=duration) @property - def amp(self): + def amp(self) -> complex: + """The Gaussian amplitude.""" return self._amp @property - def sigma(self): + def sigma(self) -> float: + """The Gaussian standard deviation of the pulse width.""" return self._sigma @property - def width(self): + def width(self) -> float: + """The width of the square portion of the pulse.""" return self._width def get_sample_pulse(self) -> SamplePulse: @@ -276,8 +273,7 @@ def __init__(self, duration: int, amp: complex, sigma: float, - beta: float, - channel: Optional[PulseChannel] = None): + beta: float): """Initialize the drag pulse. Args: @@ -293,15 +289,18 @@ def __init__(self, super().__init__(duration=duration) @property - def amp(self): + def amp(self) -> complex: + """The Gaussian amplitude.""" return self._amp @property - def sigma(self): + def sigma(self) -> float: + """The Gaussian standard deviation of the pulse width.""" return self._sigma @property - def beta(self): + def beta(self) -> float: + """The weighing factor for the Gaussian derivative component of the waveform.""" return self._beta def get_sample_pulse(self) -> SamplePulse: @@ -359,8 +358,7 @@ class ConstantPulse(ParametricPulse): def __init__(self, duration: int, - amp: complex, - channel: Optional[PulseChannel] = None): + amp: complex): """ Initialize the constant-valued pulse. @@ -372,7 +370,8 @@ def __init__(self, super().__init__(duration=duration) @property - def amp(self): + def amp(self) -> complex: + """The constant value amplitude.""" return self._amp def get_sample_pulse(self) -> SamplePulse: diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 6b3d7ca19e0c..6de80a2fbe2e 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -13,22 +13,27 @@ # that they have been altered from the originals. """Any command which implements a transmit signal on a channel.""" -from typing import Callable, List, Optional - +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.""" - @abstractmethod #??? - def __init__(self, duration: int = None): - if isinstance(duration, (int, np.integer)): - self._duration = int(duration) - else: + @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(self.__class__.__name__, + self.__hash__()) def __call__(self, channel: PulseChannel) -> 'Instruction': """Return new ``Play`` instruction that is fully instantiated with both ``pulse`` and a diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index 53c2915e4c8f..ae3beed71e3c 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -12,21 +12,23 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""TODO""" +"""A pulse that is described by complex-valued sample points.""" import warnings - from typing import Callable, Union, List, Optional -from qiskit.pulse.channels import PulseChannel +import numpy as np + +from ..exceptions import PulseError from .pulse import Pulse class SamplePulse(Pulse): - """TODO""" + """A pulse specified completely by complex-valued amplitudes; implicitly time separated by the + backend cycle-time, dt. + """ def __init__(self, samples: Union[np.ndarray, List[complex]], - channel: Optional[PulseChannel] = None, - name: Optional[str] = None, + name: Optional[str] = None, epsilon: float = 1e-7): """Create new sample pulse command. @@ -38,34 +40,32 @@ def __init__(self, samples: Union[np.ndarray, List[complex]], 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)) - + super().__init__(duration=len(samples), name=name) samples = np.asarray(samples, dtype=np.complex_) - self._samples = self._clip(samples, epsilon=epsilon) - self._name = SamplePulse.create_name(name) @property - def samples(self): + def samples(self) -> np.ndarray: """Return sample values.""" return self._samples - def _clip(self, samples: np.ndarray, epsilon: float = 1e-7): + 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 pulse envelope + 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 + norm is greater than 1+epsilon an error will be raised. Returns: - np.ndarray: Clipped pulse samples + Clipped pulse samples. + Raises: - PulseError: If there exists a pulse sample with a norm greater than 1+epsilon + 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) @@ -152,7 +152,3 @@ def __repr__(self): self.name) np.set_printoptions(**opt) return repr_str - - def __call__(self): - """TODO, deprecate""" - pass From c5b0741c933f55e52291e6092b9f6e29860f6eb9 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Fri, 13 Mar 2020 16:31:15 -0400 Subject: [PATCH 05/38] fixup cyclic imports --- qiskit/pulse/commands/pulse_decorators.py | 29 +------------------ qiskit/pulse/pulse_lib/discrete.py | 5 ++-- qiskit/pulse/pulse_lib/samplers/__init__.py | 2 +- qiskit/pulse/pulse_lib/samplers/decorators.py | 22 +++++++++++++- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/qiskit/pulse/commands/pulse_decorators.py b/qiskit/pulse/commands/pulse_decorators.py index 4de0f5f163f5..65145a84f9e4 100644 --- a/qiskit/pulse/commands/pulse_decorators.py +++ b/qiskit/pulse/commands/pulse_decorators.py @@ -18,31 +18,4 @@ 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/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/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..63cc591aae59 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 ..sample_pulse import SamplePulse from . import strategies +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 + + def _update_annotations(discretized_pulse: Callable) -> Callable: """Update annotations of discretized continuous pulse function with duration. From 780e5f8863362f9932f8b2e4294cb159f8f8ae58 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 16 Mar 2020 11:07:41 -0400 Subject: [PATCH 06/38] IP updates to assemble_schedules for supporting Play instruction --- qiskit/qobj/converters/pulse_instruction.py | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index bd65f303b8f5..735bf8ed5340 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 +from qiskit.pulse import commands, channels, instructions, pulse_lib from qiskit.pulse.exceptions import PulseError from qiskit.pulse.parser import parse_string_expr from qiskit.pulse.schedule import ParameterizedSchedule, Schedule @@ -263,6 +263,29 @@ 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: + # TODO + + return self._qobj_model(**command_dict) + @bind_instruction(commands.Snapshot) def convert_snapshot(self, shift, instruction): """Return converted `Snapshot`. @@ -447,7 +470,7 @@ 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): From d4418e335b5779f777b6fe64c47c721b23b863eb Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 16 Mar 2020 18:27:53 -0400 Subject: [PATCH 07/38] Give all pulses a name arg and update reno note --- qiskit/pulse/instructions/play.py | 2 +- qiskit/pulse/pulse_lib/parametric_pulses.py | 18 +++++++++++---- qiskit/pulse/pulse_lib/pulse.py | 5 ++-- qiskit/pulse/pulse_lib/sample_pulse.py | 6 ++--- ...uctions-and-commands-aaa6d8724b8a29d3.yaml | 23 +++++++++++++++++++ 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 50c6137e165e..b32f8a666877 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -42,7 +42,7 @@ def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = No """ self._pulse = pulse self._channel = channel - super().__init__(pulse.duration, channel, name=name) + super().__init__(pulse.duration, channel, name=name if name is not None else pulse.name) @property def operands(self) -> List[Union[Pulse, PulseChannel]]: diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index da11af986fe5..ff1ea9c7ab54 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -52,13 +52,14 @@ class ParametricPulse(Pulse): """The abstract superclass for parametric pulses.""" @abstractmethod - def __init__(self, duration: int): + 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) + super().__init__(duration=duration, name=name) self.validate_parameters() @abstractmethod @@ -122,7 +123,8 @@ class Gaussian(ParametricPulse): def __init__(self, duration: int, amp: complex, - sigma: float): + sigma: float, + name: Optional[str] = None): """Initialize the gaussian pulse. Args: @@ -130,6 +132,7 @@ def __init__(self, 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 @@ -189,7 +192,8 @@ def __init__(self, duration: int, amp: complex, sigma: float, - width: float): + width: float, + name: Optional[str] = None): """Initialize the gaussian square pulse. Args: @@ -198,6 +202,7 @@ def __init__(self, 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 @@ -273,7 +278,8 @@ def __init__(self, duration: int, amp: complex, sigma: float, - beta: float): + beta: float, + name: Optional[str] = None): """Initialize the drag pulse. Args: @@ -282,6 +288,7 @@ def __init__(self, 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 @@ -365,6 +372,7 @@ def __init__(self, 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) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 6de80a2fbe2e..057f064ffc1a 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -32,8 +32,9 @@ 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(self.__class__.__name__, - self.__hash__()) + self.name = (name if name is not None + else '{}{}'.format(str(self.__class__.__name__).lower(), + self.__hash__())) def __call__(self, channel: PulseChannel) -> 'Instruction': """Return new ``Play`` instruction that is fully instantiated with both ``pulse`` and a diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index ae3beed71e3c..9c2f0cd1b348 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -28,17 +28,17 @@ class SamplePulse(Pulse): """ def __init__(self, samples: Union[np.ndarray, List[complex]], - name: Optional[str] = None, - epsilon: float = 1e-7): + epsilon: float = 1e-7, + name: Optional[str] = None): """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 + name: Unique name to identify the pulse """ super().__init__(duration=len(samples), name=name) samples = np.asarray(samples, dtype=np.complex_) diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index f1eeb5958123..0d78ca180258 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -11,17 +11,34 @@ features: sched += Delay(5)(DriveChannel(0)) sched += ShiftPhase(np.pi)(DriveChannel(0)) + sched += SamplePulse([1.0, ...])(DriveChannel(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)) Now, rather than build a command *and* an instruction, each command has been migrated into an instruction:: sched += Delay(5, DriveChannel(0)) sched += ShiftPhase(np.pi, DriveChannel(0)) + sched += Play(SamplePulse([1.0, ...], DriveChannel(0)) + + - | + There is now a :py:class:`~qiskit.pulse.instructions.Play` instruction + which takes a description of a pulse envelope and a channel. The pulse + description must subclass from :py:class:`~qiskit.pulse.pulse_lib.Pulse`. + py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and + py:class:`~qiskit.pulse.pulse_lib.ParametricPulse` s (e.g. ``Gaussian``) + now subclass from ``Pulse`` and have been moved to the `pulse_lib`. + + For example:: + + Play(SamplePulse[0.1]*10, DriveChannel(0)) + Play(ConstantPulse(duration=10, amp=0.1), DriveChannel(0)) + deprecations: - | ``DelayInstruction`` has been deprecated and replaced by @@ -38,3 +55,9 @@ deprecations: Delay()() The new ``Delay`` instruction does not support a ``command`` attribute. + - | + The call method of py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and + py:class:`~qiskit.pulse.pulse_lib.ParametricPulse`s has been deprecated. + The migration is as follows:: + + Pulse(<*args>)() -> Play(Pulse(*args), ) From 66d2a25f81e8e225efab3482fec5ef815495419f Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 16 Mar 2020 18:34:06 -0400 Subject: [PATCH 08/38] Continue pulse_instruction conversion support for Play --- qiskit/pulse/pulse_lib/pulse.py | 4 ++-- qiskit/pulse/pulse_lib/samplers/decorators.py | 2 -- qiskit/qobj/converters/pulse_instruction.py | 6 +++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 057f064ffc1a..cfddfd4ff89c 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -33,8 +33,8 @@ def __init__(self, duration: int, name: Optional[str] = None): 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__())) + else '{}{}'.format(str(self.__class__.__name__).lower(), + self.__hash__())) def __call__(self, channel: PulseChannel) -> 'Instruction': """Return new ``Play`` instruction that is fully instantiated with both ``pulse`` and a diff --git a/qiskit/pulse/pulse_lib/samplers/decorators.py b/qiskit/pulse/pulse_lib/samplers/decorators.py index 63cc591aae59..88c7a27518c9 100644 --- a/qiskit/pulse/pulse_lib/samplers/decorators.py +++ b/qiskit/pulse/pulse_lib/samplers/decorators.py @@ -135,8 +135,6 @@ def linear(times: np.ndarray, m: float, b: float) -> np.ndarray: import numpy as np -from qiskit.pulse.commands.pulse_decorators import functional_pulse - from ..sample_pulse import SamplePulse from . import strategies diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 735bf8ed5340..b036ba29dc01 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -282,7 +282,11 @@ def convert_play(self, shift, instruction): 'parameters': instruction.pulse.parameters } else: - # TODO + command_dict = { + 'name': instruction.name, + 't0': shift + instruction.start_time, + 'ch': instruction.channel.name + } return self._qobj_model(**command_dict) From 646529c3df0c7d1c2e80b3df33efed76d69afcdf Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 16 Mar 2020 18:50:56 -0400 Subject: [PATCH 09/38] Add support for Play in assemble schedules, fix style --- qiskit/assembler/assemble_schedules.py | 23 ++++++++++++++++--- qiskit/pulse/commands/pulse_decorators.py | 2 +- qiskit/pulse/pulse_lib/samplers/decorators.py | 4 +++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index 0ea5c4b739bd..bb7432e00deb 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -17,7 +17,8 @@ from typing import Any, Dict, List, Tuple from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule, Delay +from qiskit.pulse import Schedule, Delay, Play +from qiskit.pulse.pulse_lib import ParametricPulse from qiskit.pulse.commands import (Command, PulseInstruction, Acquire, AcquireInstruction, DelayInstruction, SamplePulse, ParametricInstruction) from qiskit.qobj import (PulseQobj, QobjHeader, QobjExperimentHeader, @@ -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/commands/pulse_decorators.py b/qiskit/pulse/commands/pulse_decorators.py index 65145a84f9e4..af8e237bcf71 100644 --- a/qiskit/pulse/commands/pulse_decorators.py +++ b/qiskit/pulse/commands/pulse_decorators.py @@ -12,7 +12,7 @@ # 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. diff --git a/qiskit/pulse/pulse_lib/samplers/decorators.py b/qiskit/pulse/pulse_lib/samplers/decorators.py index 88c7a27518c9..2e6a815d2a1f 100644 --- a/qiskit/pulse/pulse_lib/samplers/decorators.py +++ b/qiskit/pulse/pulse_lib/samplers/decorators.py @@ -135,15 +135,17 @@ def linear(times: np.ndarray, m: float, b: float) -> np.ndarray: import numpy as np +from ...exceptions import PulseError from ..sample_pulse import SamplePulse from . import strategies -def functional_pulse(func: Callable): +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. """ From 6104e884ea4f3cdff3aad98c291b8c408a3667b1 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 17 Mar 2020 00:49:41 -0400 Subject: [PATCH 10/38] Attempt to fix build error --- qiskit/pulse/instructions/play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index b32f8a666877..8bd1e9552c15 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -18,7 +18,7 @@ from typing import List, Optional, Union from ..channels import PulseChannel -from ..pulse_lib import Pulse +from ..pulse_lib.pulse import Pulse from .instruction import Instruction From 67ab1b427986eb1a41dcfb6c27b95e466fdf468d Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 17 Mar 2020 10:56:48 -0400 Subject: [PATCH 11/38] Add missing name field to Constant pulse --- qiskit/pulse/pulse_lib/parametric_pulses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index ff1ea9c7ab54..1c796901a995 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -365,7 +365,8 @@ class ConstantPulse(ParametricPulse): def __init__(self, duration: int, - amp: complex): + amp: complex, + name: Optional[str] = None): """ Initialize the constant-valued pulse. From 36134c26e0cf42645ee318b744def01a8dc3fb89 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 17 Mar 2020 10:58:21 -0400 Subject: [PATCH 12/38] To support __call__ from Pulse, I need to import Play, which means I can't import Pulse from Play (for typehints) for the timebeing --- qiskit/pulse/instructions/play.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 8bd1e9552c15..9419e05e1c15 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -18,7 +18,6 @@ from typing import List, Optional, Union from ..channels import PulseChannel -from ..pulse_lib.pulse import Pulse from .instruction import Instruction @@ -31,7 +30,7 @@ class Play(Instruction): cycle time, dt, of the backend. """ - def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = None): + def __init__(self, pulse: 'Pulse', channel: PulseChannel, name: Optional[str] = None): """Create a new pulse instruction. Args: @@ -45,12 +44,12 @@ def __init__(self, pulse: Pulse, channel: PulseChannel, name: Optional[str] = No super().__init__(pulse.duration, channel, name=name if name is not None else pulse.name) @property - def operands(self) -> List[Union[Pulse, PulseChannel]]: + def operands(self) -> List[Union['Pulse', PulseChannel]]: """Return a list of instruction operands.""" return [self.pulse, self.channel] @property - def pulse(self) -> Pulse: + 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. """ From efbbd1746953bbb5220cd3331351b384058c62e6 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 17 Mar 2020 21:23:16 -0400 Subject: [PATCH 13/38] Fixup bugs introduced in SamplePulse --- qiskit/pulse/pulse_lib/sample_pulse.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index 9c2f0cd1b348..14132d7a1750 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -28,21 +28,21 @@ class SamplePulse(Pulse): """ def __init__(self, samples: Union[np.ndarray, List[complex]], - epsilon: float = 1e-7, - name: Optional[str] = None): + 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 - name: Unique name to identify the pulse """ - super().__init__(duration=len(samples), name=name) 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: From 33c8da47c239b6bdb40042617fd6c4640252d02c Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 17 Mar 2020 21:33:15 -0400 Subject: [PATCH 14/38] Update tests with new API --- test/python/pulse/test_schedule.py | 26 ++++++++++++------------ test/python/qobj/test_pulse_converter.py | 7 +++---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py index 35a6595ee589..d0ef6d99f490 100644 --- a/test/python/pulse/test_schedule.py +++ b/test/python/pulse/test_schedule.py @@ -25,7 +25,7 @@ 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 @@ -396,7 +396,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): @@ -416,7 +416,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 @@ -527,12 +527,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) @@ -579,7 +579,7 @@ def test_filter_intervals(self): self.assertIsInstance(filtered.instructions[0][1], AcquireInstruction) 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) @@ -618,26 +618,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 AcquireInstruction 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 224bd0ba171f..b16ac8de345b 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -209,13 +209,12 @@ def setUp(self): def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" cmd = self.linear - instruction = cmd(DriveChannel(0)) << 10 + instruction = cmd(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.""" @@ -228,7 +227,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.""" From 87399979a9868335c784abae7fbe5f809765b88d Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 10:13:21 -0400 Subject: [PATCH 15/38] Add tests and fixup missing name passing --- qiskit/pulse/pulse_lib/parametric_pulses.py | 8 ++++---- test/python/pulse/test_instructions.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index 1c796901a995..eeb024a0bc8d 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -136,7 +136,7 @@ def __init__(self, """ self._amp = complex(amp) self._sigma = sigma - super().__init__(duration=duration) + super().__init__(duration=duration, name=name) @property def amp(self) -> complex: @@ -207,7 +207,7 @@ def __init__(self, self._amp = complex(amp) self._sigma = sigma self._width = width - super().__init__(duration=duration) + super().__init__(duration=duration, name=name) @property def amp(self) -> complex: @@ -293,7 +293,7 @@ def __init__(self, self._amp = complex(amp) self._sigma = sigma self._beta = beta - super().__init__(duration=duration) + super().__init__(duration=duration, name=name) @property def amp(self) -> complex: @@ -376,7 +376,7 @@ def __init__(self, name: Display name for this pulse envelope. """ self._amp = complex(amp) - super().__init__(duration=duration) + super().__init__(duration=duration, name=name) @property def amp(self) -> complex: 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) From 0bed2c2a6bdf5cebb1dd65fae6750b25189e33b9 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 13:35:05 -0400 Subject: [PATCH 16/38] Fixup tests, remove deprecation warning from assemble execution, add hash and eq methods to pulses --- qiskit/pulse/pulse_lib/parametric_pulses.py | 14 ++++++++++---- qiskit/pulse/pulse_lib/pulse.py | 8 ++++++++ qiskit/pulse/pulse_lib/sample_pulse.py | 4 ++-- qiskit/qobj/converters/pulse_instruction.py | 4 ++-- test/python/compiler/test_assembler.py | 5 ++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index eeb024a0bc8d..f3dde1b7a13f 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -110,6 +110,12 @@ def draw(self, dt: float = 1, 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 @@ -163,7 +169,7 @@ def validate_parameters(self) -> None: def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma} - def __repr__(self): + def __repr__(self) -> str: return '{}(duration={}, amp={}, sigma={})' \ ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma) @@ -243,7 +249,7 @@ def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, "width": self.width} - def __repr__(self): + def __repr__(self) -> str: return '{}(duration={}, amp={}, sigma={}, width={})' \ ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.width) @@ -348,7 +354,7 @@ def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, "beta": self.beta} - def __repr__(self): + def __repr__(self) -> str: return '{}(duration={}, amp={}, sigma={}, beta={})' \ ''.format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.beta) @@ -395,5 +401,5 @@ def validate_parameters(self) -> None: def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp} - def __repr__(self): + 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 index cfddfd4ff89c..5df4c79589ab 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -73,3 +73,11 @@ def draw(self, dt: float = 1, 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 index 14132d7a1750..d6b385fca131 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -129,7 +129,7 @@ def draw(self, dt: float = 1, interp_method=interp_method, scale=scale, interactive=interactive) - def __eq__(self, other: 'SamplePulse'): + 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. @@ -142,7 +142,7 @@ def __eq__(self, other: 'SamplePulse'): return super().__eq__(other) and (self.samples == other.samples).all() def __hash__(self): - return hash((super().__hash__(), self.samples.tostring())) + return hash(self.samples.tostring()) def __repr__(self): opt = np.get_printoptions() diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 060e711e6c62..0f58f41c70bc 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -586,7 +586,7 @@ def bind_pulse(self, pulse): @self.bind_name(pulse.name) def convert_named_drive(self, instruction): - """Return converted `PulseInstruction`. + """Return converted `Play`. Args: instruction (PulseQobjInstruction): pulse qobj @@ -595,7 +595,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/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index 880f445484a1..90b167591eb9 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], + qobj.config.pulse_library[1]) def test_pulse_name_conflicts_in_other_schedule(self): """Test two pulses with the same name in different schedule can be resolved.""" From dc1d42d40cca8fdf2b4473c28ec5773ba546c075 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 13:46:20 -0400 Subject: [PATCH 17/38] Fixup docs --- .../unify-instructions-and-commands-aaa6d8724b8a29d3.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index b4712108d8c2..3ec825aff144 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -36,7 +36,7 @@ features: description must subclass from :py:class:`~qiskit.pulse.pulse_lib.Pulse`. py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and py:class:`~qiskit.pulse.pulse_lib.ParametricPulse` s (e.g. ``Gaussian``) - now subclass from ``Pulse`` and have been moved to the `pulse_lib`. + now subclass from ``Pulse`` and have been moved to the ``pulse_lib``. For example:: @@ -72,7 +72,7 @@ deprecations: ShiftPhase()() - | The call method of py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and - py:class:`~qiskit.pulse.pulse_lib.ParametricPulse`s has been deprecated. + py:class:`~qiskit.pulse.pulse_lib.ParametricPulse` s have been deprecated. The migration is as follows:: Pulse(<*args>)() -> Play(Pulse(*args), ) From e7facb34760ab45cffc3baf1d9f200c6afcc2ced Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 14:07:30 -0400 Subject: [PATCH 18/38] Update functional_pulse import path and Instruction type ref --- qiskit/pulse/__init__.py | 4 ++-- qiskit/pulse/pulse_lib/pulse.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 1c62ffb47220..bf85861207cf 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -133,12 +133,12 @@ from .channels import (DriveChannel, MeasureChannel, AcquireChannel, ControlChannel, RegisterSlot, MemorySlot) from .cmd_def import CmdDef -from .commands import (AcquireInstruction, FrameChange, PersistentValue, - 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, 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/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 5df4c79589ab..8299b4130f98 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -36,7 +36,7 @@ def __init__(self, duration: int, name: Optional[str] = None): else '{}{}'.format(str(self.__class__.__name__).lower(), self.__hash__())) - def __call__(self, channel: PulseChannel) -> 'Instruction': + def __call__(self, channel: PulseChannel) -> 'qiskit.pulse.instruction.Instruction': """Return new ``Play`` instruction that is fully instantiated with both ``pulse`` and a ``channel``. From cbefe8a9b4127930055faccdf543f61cf0dab4c9 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 14:28:57 -0400 Subject: [PATCH 19/38] try again with the type hints --- qiskit/pulse/pulse_lib/pulse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 8299b4130f98..7c5bb6c40110 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -36,7 +36,7 @@ def __init__(self, duration: int, name: Optional[str] = None): else '{}{}'.format(str(self.__class__.__name__).lower(), self.__hash__())) - def __call__(self, channel: PulseChannel) -> 'qiskit.pulse.instruction.Instruction': + def __call__(self, channel: PulseChannel) -> Play: """Return new ``Play`` instruction that is fully instantiated with both ``pulse`` and a ``channel``. From 98169f3736816f3a75f6df5735a783d871843534 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 15:25:12 -0400 Subject: [PATCH 20/38] Was missing updates to PulseCommand that needed to be migrated with the new Pulse class --- qiskit/pulse/pulse_lib/pulse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 7c5bb6c40110..ff9f7cbc8f79 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -52,7 +52,7 @@ def __call__(self, channel: PulseChannel) -> Play: @abstractmethod def draw(self, dt: float = 1, - style: Optional['PulseStyle'] = None, + style=None, filename: Optional[str] = None, interp_method: Optional[Callable] = None, scale: float = 1, interactive: bool = False, From 74ada9b07ba9921c8b22791d9e34049c47bc8959 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 15:28:36 -0400 Subject: [PATCH 21/38] Was missing docstring type documentation for unresolvable type --- qiskit/pulse/pulse_lib/pulse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index ff9f7cbc8f79..6a4a3ec7a69d 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -61,7 +61,7 @@ def draw(self, dt: float = 1, Args: dt: Time interval of samples. - style: A style sheet to configure plot appearance + 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 From a889e478bddc73634d27aa0c8d4180915fe1ffe5 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 15:47:07 -0400 Subject: [PATCH 22/38] Was missing removal of PulseStyle in parametric pulses --- qiskit/pulse/pulse_lib/parametric_pulses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index f3dde1b7a13f..cf75262eeadc 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -86,7 +86,7 @@ def parameters(self) -> Dict[str, Any]: pass def draw(self, dt: float = 1, - style: Optional['PulseStyle'] = None, + style=None, filename: Optional[str] = None, interp_method: Optional[Callable] = None, scale: float = 1, interactive: bool = False, @@ -95,7 +95,7 @@ def draw(self, dt: float = 1, Args: dt: Time interval of samples. - style: A style sheet to configure plot appearance + 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 From 16278c062060dd9d9e71f0994835d14becfd24c2 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 16:09:28 -0400 Subject: [PATCH 23/38] The changes from the sphinx warrnings pass hadn't been moved with the migrated ParametricPulses module. Moved the drag pulse docstring over finally --- qiskit/pulse/pulse_lib/parametric_pulses.py | 31 ++++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index cf75262eeadc..34c5958e43b6 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -41,11 +41,11 @@ class ParametricPulseShapes(Enum): import math import numpy as np -from ..exceptions import PulseError 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): @@ -255,29 +255,26 @@ def __repr__(self) -> str: class Drag(ParametricPulse): - """ - The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse + 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 |1>-|2> transition, reducing the chance of - leakage to the |2> state. - + 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) ) - - Ref: - [1] 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] F. Motzoi, J. M. Gambetta, P. Rebentrost, and F. K. Wilhelm - Phys. Rev. Lett. 103, 110501 – Published 8 September 2009 + 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, From 14fc97352fe5624e383cde6664a7d0cc8b813925 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Thu, 19 Mar 2020 16:38:12 -0400 Subject: [PATCH 24/38] Fix spacing in drag pulse docstring --- qiskit/pulse/pulse_lib/parametric_pulses.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/qiskit/pulse/pulse_lib/parametric_pulses.py b/qiskit/pulse/pulse_lib/parametric_pulses.py index 34c5958e43b6..2de2c9c9a2b7 100644 --- a/qiskit/pulse/pulse_lib/parametric_pulses.py +++ b/qiskit/pulse/pulse_lib/parametric_pulses.py @@ -259,20 +259,31 @@ class Drag(ParametricPulse): 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.* """ From 3d9efd8d68a10f98ac178bf587693e66ee473e6a Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Fri, 20 Mar 2020 09:57:40 -0400 Subject: [PATCH 25/38] The docs should finally build now --- qiskit/pulse/pulse_lib/sample_pulse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index d6b385fca131..763d9b9cc39d 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -96,7 +96,7 @@ def _clip(self, samples: np.ndarray, epsilon: float = 1e-7) -> np.ndarray: return samples def draw(self, dt: float = 1, - style: Optional['PulseStyle'] = None, + style=None, filename: Optional[str] = None, interp_method: Optional[Callable] = None, scale: float = 1, interactive: bool = False, @@ -105,7 +105,7 @@ def draw(self, dt: float = 1, Args: dt: Time interval of samples. - style: A style sheet to configure plot appearance + 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 From d753a45fa38509d24ff4200803e0f504ec86cbbb Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 23 Mar 2020 11:38:25 -0400 Subject: [PATCH 26/38] Update qiskit/pulse/instructions/play.py Co-Authored-By: eggerdj <38065505+eggerdj@users.noreply.github.com> --- qiskit/pulse/instructions/play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 9419e05e1c15..6d6a2f9235fd 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -36,7 +36,7 @@ def __init__(self, pulse: 'Pulse', channel: PulseChannel, name: Optional[str] = Args: pulse: A pulse waveform description, such as :py:class:`~qiskit.pulse.pulse_lib.SamplePulse`. - channel: The channel that will have the pulse. + channel: The channel to which the pulse is applied. name: Name of the delay for display purposes. """ self._pulse = pulse From e05c9374d78bc659d04d4d1d8183b9e9627b8f33 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 23 Mar 2020 11:38:46 -0400 Subject: [PATCH 27/38] Update qiskit/pulse/pulse_lib/sample_pulse.py Co-Authored-By: eggerdj <38065505+eggerdj@users.noreply.github.com> --- qiskit/pulse/pulse_lib/sample_pulse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index 763d9b9cc39d..50a6e9877fd7 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -34,11 +34,11 @@ def __init__(self, samples: Union[np.ndarray, List[complex]], Args: samples: Complex array of pulse envelope - name: Unique name to identify the pulse + 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 + 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) From 30a517d0bb68e2a9cc0b72188fe7627e25208981 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 23 Mar 2020 11:40:44 -0400 Subject: [PATCH 28/38] Apply suggestions from code review Co-Authored-By: eggerdj <38065505+eggerdj@users.noreply.github.com> --- qiskit/pulse/pulse_lib/sample_pulse.py | 4 ++-- .../unify-instructions-and-commands-aaa6d8724b8a29d3.yaml | 4 ++-- test/python/qobj/test_pulse_converter.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index 50a6e9877fd7..0c636d0e83d9 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -33,7 +33,7 @@ def __init__(self, samples: Union[np.ndarray, List[complex]], """Create new sample pulse command. Args: - samples: Complex array of pulse envelope + 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 @@ -55,7 +55,7 @@ def _clip(self, samples: np.ndarray, epsilon: float = 1e-7) -> np.ndarray: If difference is greater than epsilon error is raised. Args: - samples: Complex array of pulse envelope. + 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 diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index 3ec825aff144..2a8c5ba01ae5 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -27,7 +27,7 @@ features: sched += Delay(5, DriveChannel(0)) sched += ShiftPhase(np.pi, DriveChannel(0)) - sched += Play(SamplePulse([1.0, ...], DriveChannel(0)) + sched += Play(SamplePulse([1.0, ...]), DriveChannel(0)) sched += SetFrequency(5.5, DriveChannel(0)) # New instruction! sched += Acquire(100, AcquireChannel(0), MemorySlot(0)) - | @@ -40,7 +40,7 @@ features: For example:: - Play(SamplePulse[0.1]*10, DriveChannel(0)) + Play(SamplePulse([0.1]*10), DriveChannel(0)) Play(ConstantPulse(duration=10, amp=0.1), DriveChannel(0)) deprecations: - | diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index e0a3d669d30b..0484c5cbdcbe 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -206,8 +206,7 @@ def setUp(self): def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" - cmd = self.linear - instruction = cmd(DriveChannel(0)) + instruction = self.linear(DriveChannel(0)) qobj = PulseQobjInstruction(name='linear', ch='d0', t0=10) converted_instruction = self.converter(qobj) From c2ecd3d542ba18b180d54ac3b10341dbcac44b7f Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 23 Mar 2020 13:31:34 -0400 Subject: [PATCH 29/38] Documentation improvements --- qiskit/pulse/instructions/play.py | 8 +++---- qiskit/pulse/pulse_lib/pulse.py | 8 +++++-- qiskit/pulse/pulse_lib/sample_pulse.py | 27 +++++++++++------------- test/python/qobj/test_pulse_converter.py | 6 +++--- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 6d6a2f9235fd..74a54f85b788 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -22,10 +22,10 @@ class Play(Instruction): - """An instruction specifying the exact time dynamics of the output signal envelope for a - limited time on the channel that the instruction is operating on. + """This instruction is responsible for applying a pulse on a channel. - This signal is modulated by a phase and frequency which are controlled by separate + 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. """ @@ -37,7 +37,7 @@ def __init__(self, pulse: 'Pulse', channel: PulseChannel, name: Optional[str] = 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 delay for display purposes. + name: Name of the instruction for display purposes. """ self._pulse = pulse self._channel = channel diff --git a/qiskit/pulse/pulse_lib/pulse.py b/qiskit/pulse/pulse_lib/pulse.py index 6a4a3ec7a69d..36948f4374d0 100644 --- a/qiskit/pulse/pulse_lib/pulse.py +++ b/qiskit/pulse/pulse_lib/pulse.py @@ -12,7 +12,9 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Any command which implements a transmit signal on a channel.""" +"""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 @@ -25,7 +27,9 @@ class Pulse(ABC): - """The abstract superclass for pulses.""" + """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): diff --git a/qiskit/pulse/pulse_lib/sample_pulse.py b/qiskit/pulse/pulse_lib/sample_pulse.py index 0c636d0e83d9..83ff1d2ddcda 100644 --- a/qiskit/pulse/pulse_lib/sample_pulse.py +++ b/qiskit/pulse/pulse_lib/sample_pulse.py @@ -23,8 +23,8 @@ class SamplePulse(Pulse): - """A pulse specified completely by complex-valued amplitudes; implicitly time separated by the - backend cycle-time, dt. + """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]], @@ -105,13 +105,13 @@ def draw(self, dt: float = 1, 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` + 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 @@ -134,10 +134,10 @@ def __eq__(self, other: 'SamplePulse') -> bool: and have the same name and samples. Args: - other: other SamplePulse + other: Object to compare to. Returns: - bool: are self and other equal + True iff self and other are equal. """ return super().__eq__(other) and (self.samples == other.samples).all() @@ -147,8 +147,5 @@ def __hash__(self): 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 + return '{}({}, name="{}")'.format(self.__class__.__name__, repr(self.samples), self.name) diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index 0484c5cbdcbe..d86cb85d311a 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -23,7 +23,7 @@ LoConfigConverter) from qiskit.pulse.commands import (SamplePulse, FrameChange, PersistentValue, Snapshot, Acquire, Gaussian, GaussianSquare, ConstantPulse, Drag) -from qiskit.pulse.instructions import ShiftPhase, SetFrequency +from qiskit.pulse.instructions import ShiftPhase, SetFrequency, Play from qiskit.pulse.channels import (DriveChannel, ControlChannel, MeasureChannel, AcquireChannel, MemorySlot, RegisterSlot) from qiskit.pulse.schedule import ParameterizedSchedule, Schedule @@ -36,8 +36,8 @@ class TestInstructionToQobjConverter(QiskitTestCase): def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - command = SamplePulse(np.arange(0, 0.01), name='linear') - instruction = command(DriveChannel(0)) + pulse = SamplePulse(np.arange(0, 0.01), name='linear') + instruction = Play(pulse, DriveChannel(0)) valid_qobj = PulseQobjInstruction( name='linear', From 8430aecfdcf61428cf48b7a4bf614c97e26b71ac Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Mon, 23 Mar 2020 18:26:38 -0400 Subject: [PATCH 30/38] Test change because linux python35 is failing --- test/python/qobj/test_pulse_converter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index d86cb85d311a..0484c5cbdcbe 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -23,7 +23,7 @@ LoConfigConverter) from qiskit.pulse.commands import (SamplePulse, FrameChange, PersistentValue, Snapshot, Acquire, Gaussian, GaussianSquare, ConstantPulse, Drag) -from qiskit.pulse.instructions import ShiftPhase, SetFrequency, Play +from qiskit.pulse.instructions import ShiftPhase, SetFrequency from qiskit.pulse.channels import (DriveChannel, ControlChannel, MeasureChannel, AcquireChannel, MemorySlot, RegisterSlot) from qiskit.pulse.schedule import ParameterizedSchedule, Schedule @@ -36,8 +36,8 @@ class TestInstructionToQobjConverter(QiskitTestCase): def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - pulse = SamplePulse(np.arange(0, 0.01), name='linear') - instruction = Play(pulse, DriveChannel(0)) + command = SamplePulse(np.arange(0, 0.01), name='linear') + instruction = command(DriveChannel(0)) valid_qobj = PulseQobjInstruction( name='linear', From c1c64a3ef65a023dd8a6fdaa3a5e001f1cb15925 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 24 Mar 2020 12:03:42 -0400 Subject: [PATCH 31/38] Update releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml Co-Authored-By: eggerdj <38065505+eggerdj@users.noreply.github.com> --- .../notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index 2a8c5ba01ae5..55aa7bd3c9fe 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -18,7 +18,7 @@ features: sched += DelayInstruction(Delay(5), DriveChannel(0)) sched += ShiftPhaseInstruction(ShiftPhase(np.pi), DriveChannel(0)) - sched += PulseInstruction(SamplePulse([1.0, ...], DriveChannel(0)) + sched += PulseInstruction(SamplePulse([1.0, ...]), DriveChannel(0)) sched += AcquireInstruction(Acquire(100), AcquireChannel(0), MemorySlot(0)) From 85988f24746c55dcdec15cd275eb94b78361c5cd Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 24 Mar 2020 14:02:14 -0400 Subject: [PATCH 32/38] Fix plotting Play instruction --- qiskit/visualization/pulse/matplotlib.py | 14 +++++----- .../references/parametric_matplotlib_ref.png | Bin 36107 -> 36086 bytes .../test_pulse_visualization_output.py | 24 +++++++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py index e7dd987c7855..a42d3afed8b8 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, Delay, Acquire, PulseError, ParametricPulse, SetFrequency, ShiftPhase) @@ -59,18 +59,20 @@ 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 isinstance(instruction, Delay): + return + pulse = getattr(instruction, 'pulse', instruction.command) 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/test/python/visualization/references/parametric_matplotlib_ref.png b/test/python/visualization/references/parametric_matplotlib_ref.png index b0c3793d2a6cbe90d7b4599000d1d69da7ed69f1..fd60b14eb005ac941ca36931d0549e0a6d8d7da5 100644 GIT binary patch literal 36086 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%0DP}Fkr$h_TyYy(Ne8b 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 Date: Tue, 24 Mar 2020 14:19:55 -0400 Subject: [PATCH 33/38] style, line length --- test/python/visualization/test_pulse_visualization_output.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/python/visualization/test_pulse_visualization_output.py b/test/python/visualization/test_pulse_visualization_output.py index d726bb1b30c9..a654b10f95e3 100644 --- a/test/python/visualization/test_pulse_visualization_output.py +++ b/test/python/visualization/test_pulse_visualization_output.py @@ -64,7 +64,8 @@ def sample_schedule(self): delay = Delay(100) sched = Schedule() sched = sched.append(gp0(DriveChannel(0))) - sched = sched.insert(0, pulse_lib.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))) From 5d1fb84750fb4579147974c186f919205228ca91 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Tue, 24 Mar 2020 15:25:45 -0400 Subject: [PATCH 34/38] Attempt to fix failing test by comparing names directly --- test/python/compiler/test_assembler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index 90b167591eb9..4b3b07f66978 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -572,8 +572,8 @@ def test_pulse_name_conflicts(self): **self.config) validate_qobj_against_schema(qobj) - self.assertNotEqual(qobj.config.pulse_library[0], - qobj.config.pulse_library[1]) + 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.""" From 873e5a179c9094c4a7cc4cb1c1f013c6b5500654 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Wed, 25 Mar 2020 10:37:09 -0400 Subject: [PATCH 35/38] Handle all instruction types in add_instruction --- qiskit/visualization/pulse/matplotlib.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py index a42d3afed8b8..d193ee8ef427 100644 --- a/qiskit/visualization/pulse/matplotlib.py +++ b/qiskit/visualization/pulse/matplotlib.py @@ -32,8 +32,8 @@ from qiskit.pulse.channels import (DriveChannel, ControlChannel, MeasureChannel, AcquireChannel, SnapshotChannel) -from qiskit.pulse.commands import FrameChangeInstruction -from qiskit.pulse import (SamplePulse, FrameChange, PersistentValue, Snapshot, Delay, +from qiskit.pulse.commands import FrameChangeInstruction, PulseInstruction +from qiskit.pulse import (SamplePulse, FrameChange, PersistentValue, Snapshot, Play, Acquire, PulseError, ParametricPulse, SetFrequency, ShiftPhase) @@ -66,9 +66,12 @@ def add_instruction(self, start_time, instruction): start_time (int): Starting time of instruction instruction (Instruction): Instruction object to be added """ - if isinstance(instruction, Delay): - return - pulse = getattr(instruction, 'pulse', instruction.command) + 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) else: From 2c736135764bf0743203c10f9e2e04dd6eb62c86 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Wed, 25 Mar 2020 10:47:33 -0400 Subject: [PATCH 36/38] Remove unused import --- qiskit/visualization/pulse/matplotlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py index d193ee8ef427..a608780c8357 100644 --- a/qiskit/visualization/pulse/matplotlib.py +++ b/qiskit/visualization/pulse/matplotlib.py @@ -32,7 +32,7 @@ from qiskit.pulse.channels import (DriveChannel, ControlChannel, MeasureChannel, AcquireChannel, SnapshotChannel) -from qiskit.pulse.commands import FrameChangeInstruction, PulseInstruction +from qiskit.pulse.commands import FrameChangeInstruction from qiskit.pulse import (SamplePulse, FrameChange, PersistentValue, Snapshot, Play, Acquire, PulseError, ParametricPulse, SetFrequency, ShiftPhase) From e41082aaae6c0f5dbcb0c76c3d952881f7b807bc Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Wed, 25 Mar 2020 16:06:38 -0400 Subject: [PATCH 37/38] Update releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml Co-Authored-By: Matthew Treinish --- .../notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index 55aa7bd3c9fe..57484d805c89 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -71,7 +71,7 @@ deprecations: ShiftPhase()() - | - The call method of py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and + 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:: From 0e0c142f7639d5213317b6e6e3e8f077a48f8990 Mon Sep 17 00:00:00 2001 From: Lauren Capelluto Date: Wed, 25 Mar 2020 16:33:59 -0400 Subject: [PATCH 38/38] Separate note about Play feature into two notes: Play feature and Pulse movement --- ...instructions-and-commands-aaa6d8724b8a29d3.yaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml index 55aa7bd3c9fe..65f77a36094c 100644 --- a/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml +++ b/releasenotes/notes/unify-instructions-and-commands-aaa6d8724b8a29d3.yaml @@ -32,17 +32,21 @@ features: 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. The pulse - description must subclass from :py:class:`~qiskit.pulse.pulse_lib.Pulse`. - py:class:`~qiskit.pulse.pulse_lib.SamplePulse` and - py:class:`~qiskit.pulse.pulse_lib.ParametricPulse` s (e.g. ``Gaussian``) - now subclass from ``Pulse`` and have been moved to the ``pulse_lib``. + 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