Source code for qiskit.pulse.pulse_lib.parametric_pulses

# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2020.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Parametric waveforms module. These are pulses which are described by a specified
parameterization.

If a backend supports parametric pulses, it will have the attribute
`backend.configuration().parametric_pulses`, which is a list of supported pulse shapes, such as
`['gaussian', 'gaussian_square', 'drag']`. A Pulse Schedule, using parametric pulses, which is
assembled for a backend which supports those pulses, will result in a Qobj which is dramatically
smaller than one which uses SamplePulses.

This module can easily be extended to describe more pulse shapes. The new class should:
  - have a descriptive name
  - be a well known and/or well described formula (include the formula in the class docstring)
  - take some parameters (at least `duration`) and validate them, if necessary
  - implement a `get_sample_pulse` method which returns a corresponding SamplePulse in the
    case that it is assembled for a backend which does not support it.

The new pulse must then be registered by the assembler in
`qiskit/qobj/converters/pulse_instruction.py:ParametricPulseShapes`
by following the existing pattern:
    class ParametricPulseShapes(Enum):
        gaussian = commands.Gaussian
        ...
        new_supported_pulse_name = commands.YourPulseCommandClass
"""
import warnings
from abc import abstractmethod
from typing import Any, Callable, Dict, Optional
import math
import numpy as np

from . import continuous
from .discrete import gaussian, gaussian_square, drag, constant
from .pulse import Pulse
from .sample_pulse import SamplePulse
from ..exceptions import PulseError


class ParametricPulse(Pulse):
    """The abstract superclass for parametric pulses."""

    @abstractmethod
    def __init__(self, duration: int, name: Optional[str] = None):
        """Create a parametric pulse and validate the input parameters.

        Args:
            duration: Pulse length in terms of the the sampling period `dt`.
            name: Display name for this pulse envelope.
        """
        super().__init__(duration=duration, name=name)
        self.validate_parameters()

    @abstractmethod
    def get_sample_pulse(self) -> SamplePulse:
        """Return a SamplePulse with samples filled according to the formula that the pulse
        represents and the parameter values it contains.
        """
        raise NotImplementedError

    @abstractmethod
    def validate_parameters(self) -> None:
        """
        Validate parameters.

        Raises:
            PulseError: If the parameters passed are not valid.
        """
        raise NotImplementedError

    @property
    @abstractmethod
    def parameters(self) -> Dict[str, Any]:
        """Return a dictionary containing the pulse's parameters."""
        pass

    def draw(self, dt: float = 1,
             style=None,
             filename: Optional[str] = None,
             interp_method: Optional[Callable] = None,
             scale: float = 1, interactive: bool = False,
             scaling: float = None):
        """Plot the pulse.

        Args:
            dt: Time interval of samples.
            style (Optional[PulseStyle]): A style sheet to configure plot appearance
            filename: Name required to save pulse image
            interp_method: A function for interpolation
            scale: Relative visual scaling of waveform amplitudes
            interactive: When set true show the circuit in a new window
                (this depends on the matplotlib backend being used supporting this)
            scaling: Deprecated, see `scale`

        Returns:
            matplotlib.figure: A matplotlib figure object of the pulse envelope
        """
        return self.get_sample_pulse().draw(dt=dt, style=style, filename=filename,
                                            interp_method=interp_method, scale=scale,
                                            interactive=interactive)

    def __eq__(self, other: Pulse) -> 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))


[docs]class Gaussian(ParametricPulse): """A truncated pulse envelope shaped according to the Gaussian function whose mean is centered at the center of the pulse (duration / 2): .. math:: f(x) = amp * exp( -(1/2) * (x - duration/2)^2 / sigma^2) ) , 0 <= x < duration """ def __init__(self, duration: int, amp: complex, sigma: float, name: Optional[str] = None): """Initialize the gaussian pulse. Args: duration: Pulse length in terms of the the sampling period `dt`. amp: The amplitude of the Gaussian envelope. sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically in the class docstring. name: Display name for this pulse envelope. """ self._amp = complex(amp) self._sigma = sigma super().__init__(duration=duration, name=name) @property def amp(self) -> complex: """The Gaussian amplitude.""" return self._amp @property def sigma(self) -> float: """The Gaussian standard deviation of the pulse width.""" return self._sigma
[docs] def get_sample_pulse(self) -> SamplePulse: return gaussian(duration=self.duration, amp=self.amp, sigma=self.sigma, zero_ends=False)
[docs] def validate_parameters(self) -> None: if abs(self.amp) > 1.: raise PulseError("The amplitude norm must be <= 1, " "found: {}".format(abs(self.amp))) if self.sigma <= 0: raise PulseError("Sigma must be greater than 0.")
@property def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma} def __repr__(self) -> str: return "{}(duration={}, amp={}, sigma={}{})" \ "".format(self.__class__.__name__, self.duration, self.amp, self.sigma, ", name='{}'".format(self.name) if self.name is not None else "")
[docs]class GaussianSquare(ParametricPulse): """A square pulse with a Gaussian shaped risefall on either side: .. math:: risefall = (duration - width) / 2 0 <= x < risefall f(x) = amp * exp( -(1/2) * (x - risefall/2)^2 / sigma^2) ) risefall <= x < risefall + width f(x) = amp risefall + width <= x < duration f(x) = amp * exp( -(1/2) * (x - (risefall + width)/2)^2 / sigma^2) ) """ def __init__(self, duration: int, amp: complex, sigma: float, width: float, name: Optional[str] = None): """Initialize the gaussian square pulse. Args: duration: Pulse length in terms of the the sampling period `dt`. amp: The amplitude of the Gaussian and of the square pulse. sigma: A measure of how wide or narrow the Gaussian risefall is; see the class docstring for more details. width: The duration of the embedded square pulse. name: Display name for this pulse envelope. """ self._amp = complex(amp) self._sigma = sigma self._width = width super().__init__(duration=duration, name=name) @property def amp(self) -> complex: """The Gaussian amplitude.""" return self._amp @property def sigma(self) -> float: """The Gaussian standard deviation of the pulse width.""" return self._sigma @property def width(self) -> float: """The width of the square portion of the pulse.""" return self._width
[docs] def get_sample_pulse(self) -> SamplePulse: return gaussian_square(duration=self.duration, amp=self.amp, width=self.width, sigma=self.sigma, zero_ends=False)
[docs] def validate_parameters(self) -> None: if abs(self.amp) > 1.: raise PulseError("The amplitude norm must be <= 1, " "found: {}".format(abs(self.amp))) if self.sigma <= 0: raise PulseError("Sigma must be greater than 0.") if self.width < 0 or self.width >= self.duration: raise PulseError("The pulse width must be at least 0 and less than its duration.")
@property def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, "width": self.width} def __repr__(self) -> str: return "{}(duration={}, amp={}, sigma={}, width={}{})" \ "".format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.width, ", name='{}'".format(self.name) if self.name is not None else "")
[docs]class Drag(ParametricPulse): r"""The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse with an additional Gaussian derivative component. It is designed to reduce the frequency spectrum of a normal gaussian pulse near the :math:`|1\rangle` - :math:`|2\rangle` transition, reducing the chance of leakage to the :math:`|2\rangle` state. .. math:: f(x) = Gaussian + 1j * beta * d/dx [Gaussian] = Gaussian + 1j * beta * (-(x - duration/2) / sigma^2) [Gaussian] where 'Gaussian' is: .. math:: Gaussian(x, amp, sigma) = amp * exp( -(1/2) * (x - duration/2)^2 / sigma^2) ) References: 1. |citation1|_ .. _citation1: https://link.aps.org/doi/10.1103/PhysRevA.83.012308 .. |citation1| replace:: *Gambetta, J. M., Motzoi, F., Merkel, S. T. & Wilhelm, F. K. Analytic control methods for high-fidelity unitary operations in a weakly nonlinear oscillator. Phys. Rev. A 83, 012308 (2011).* 2. |citation2|_ .. _citation2: https://link.aps.org/doi/10.1103/PhysRevLett.103.110501 .. |citation2| replace:: *F. Motzoi, J. M. Gambetta, P. Rebentrost, and F. K. Wilhelm Phys. Rev. Lett. 103, 110501 – Published 8 September 2009.* """ def __init__(self, duration: int, amp: complex, sigma: float, beta: float, name: Optional[str] = None): """Initialize the drag pulse. Args: duration: Pulse length in terms of the the sampling period `dt`. amp: The amplitude of the Drag envelope. sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically in the class docstring. beta: The correction amplitude. name: Display name for this pulse envelope. """ self._amp = complex(amp) self._sigma = sigma self._beta = beta super().__init__(duration=duration, name=name) @property def amp(self) -> complex: """The Gaussian amplitude.""" return self._amp @property def sigma(self) -> float: """The Gaussian standard deviation of the pulse width.""" return self._sigma @property def beta(self) -> float: """The weighing factor for the Gaussian derivative component of the waveform.""" return self._beta
[docs] def get_sample_pulse(self) -> SamplePulse: return drag(duration=self.duration, amp=self.amp, sigma=self.sigma, beta=self.beta, zero_ends=False)
[docs] def validate_parameters(self) -> None: if abs(self.amp) > 1.: raise PulseError("The amplitude norm must be <= 1, " "found: {}".format(abs(self.amp))) if self.sigma <= 0: raise PulseError("Sigma must be greater than 0.") if isinstance(self.beta, complex): raise PulseError("Beta must be real.") # Check if beta is too large: the amplitude norm must be <=1 for all points if self.beta > self.sigma: # If beta <= sigma, then the maximum amplitude is at duration / 2, which is # already constrainted by self.amp <= 1 # 1. Find the first maxima associated with the beta * d/dx gaussian term # This eq is derived from solving for the roots of the norm of the drag function. # There is a second maxima mirrored around the center of the pulse with the same # norm as the first, so checking the value at the first x maxima is sufficient. argmax_x = (self.duration / 2 - (self.sigma / self.beta) * math.sqrt(self.beta ** 2 - self.sigma ** 2)) if argmax_x < 0: # If the max point is out of range, either end of the pulse will do argmax_x = 0 # 2. Find the value at that maximum max_val = continuous.drag(np.array(argmax_x), sigma=self.sigma, beta=self.beta, amp=self.amp, center=self.duration / 2) if abs(max_val) > 1.: raise PulseError("Beta is too large; pulse amplitude norm exceeds 1.")
@property def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp, "sigma": self.sigma, "beta": self.beta} def __repr__(self) -> str: return "{}(duration={}, amp={}, sigma={}, beta={}{})" \ "".format(self.__class__.__name__, self.duration, self.amp, self.sigma, self.beta, ", name='{}'".format(self.name) if self.name is not None else "")
[docs]class Constant(ParametricPulse): """ A simple constant pulse, with an amplitude value and a duration: .. math:: f(x) = amp , 0 <= x < duration f(x) = 0 , elsewhere """ def __init__(self, duration: int, amp: complex, name: Optional[str] = None): """ Initialize the constant-valued pulse. Args: duration: Pulse length in terms of the the sampling period `dt`. amp: The amplitude of the constant square pulse. name: Display name for this pulse envelope. """ self._amp = complex(amp) super().__init__(duration=duration, name=name) @property def amp(self) -> complex: """The constant value amplitude.""" return self._amp
[docs] def get_sample_pulse(self) -> SamplePulse: return constant(duration=self.duration, amp=self.amp)
[docs] def validate_parameters(self) -> None: if abs(self.amp) > 1.: raise PulseError("The amplitude norm must be <= 1, " "found: {}".format(abs(self.amp)))
@property def parameters(self) -> Dict[str, Any]: return {"duration": self.duration, "amp": self.amp} def __repr__(self) -> str: return "{}(duration={}, amp={}{})" \ "".format(self.__class__.__name__, self.duration, self.amp, ", name='{}'".format(self.name) if self.name is not None else "")
class ConstantPulse(Constant): """ Deprecated. A simple constant pulse, with an amplitude value and a duration: .. math:: f(x) = amp , 0 <= x < duration f(x) = 0 , elsewhere """ def __init__(self, duration: int, amp: complex, name: Optional[str] = None): """ Initialize the constant-valued pulse. Args: duration: Pulse length in terms of the the sampling period `dt`. amp: The amplitude of the constant square pulse. name: Display name for this pulse envelope. """ super(ConstantPulse, self).__init__(duration, amp, name) warnings.warn("The ConstantPulse is deprecated. Use Constant instead", DeprecationWarning)