diff --git a/docs/conf.py b/docs/conf.py index b3ec73843a54..f481d65102b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,7 +68,7 @@ "rustworkx": ("https://qiskit.org/ecosystem/rustworkx/", None), "qiskit-ibm-runtime": ("https://qiskit.org/ecosystem/ibm-runtime/", None), "qiskit-aer": ("https://qiskit.org/ecosystem/aer/", None), - "numpy": ("https://numpy.org/doc/stable/", None) + "numpy": ("https://numpy.org/doc/stable/", None), } # -- Options for HTML output ------------------------------------------------- @@ -112,6 +112,8 @@ "qiskit.pulse.library.Sin": "qiskit.pulse.library.Sin_class.rst", "qiskit.pulse.library.Gaussian": "qiskit.pulse.library.Gaussian_class.rst", "qiskit.pulse.library.Drag": "qiskit.pulse.library.Drag_class.rst", + "qiskit.pulse.library.Square": "qiskit.pulse.library.Square_fun.rst", + "qiskit.pulse.library.Sech": "qiskit.pulse.library.Sech_fun.rst", } autoclass_content = "both" @@ -119,10 +121,15 @@ # -- Options for Doctest -------------------------------------------------------- -doctest_default_flags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.IGNORE_EXCEPTION_DETAIL | doctest.DONT_ACCEPT_TRUE_FOR_1 +doctest_default_flags = ( + doctest.ELLIPSIS + | doctest.NORMALIZE_WHITESPACE + | doctest.IGNORE_EXCEPTION_DETAIL + | doctest.DONT_ACCEPT_TRUE_FOR_1 +) # Leaving this string empty disables testing of doctest blocks from docstrings. # Doctest blocks are structures like this one: # >> code # output -doctest_test_doctest_blocks = "" \ No newline at end of file +doctest_test_doctest_blocks = "" diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 5fe159b947fb..07ebcba99b7b 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -147,6 +147,10 @@ Cos, Sawtooth, Triangle, + Square, + GaussianDeriv, + Sech, + SechDeriv, ParametricPulse, SymbolicPulse, ScalableSymbolicPulse, diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index c7103b7a0d0a..2562f96f24ef 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -89,10 +89,14 @@ GaussianSquare GaussianSquareDrag gaussian_square_echo + GaussianDeriv Sin Cos Sawtooth Triangle + Square + Sech + SechDeriv """ @@ -119,12 +123,16 @@ GaussianSquare, GaussianSquareDrag, gaussian_square_echo, + GaussianDeriv, Drag, Constant, Sin, Cos, Sawtooth, Triangle, + Square, + Sech, + SechDeriv, ) from .pulse import Pulse from .waveform import Waveform diff --git a/qiskit/pulse/library/continuous.py b/qiskit/pulse/library/continuous.py index a7098ef25850..cffe086798a5 100644 --- a/qiskit/pulse/library/continuous.py +++ b/qiskit/pulse/library/continuous.py @@ -222,7 +222,7 @@ def gaussian_deriv( rescale_amp=rescale_amp, ret_x=True, ) - gauss_deriv = -x / sigma * gauss + gauss_deriv = -x / sigma * gauss # Note that x is shifted and normalized by sigma if ret_gaussian: return gauss_deriv, gauss return gauss_deriv diff --git a/qiskit/pulse/library/discrete.py b/qiskit/pulse/library/discrete.py index d5f6932a0be2..046944471270 100644 --- a/qiskit/pulse/library/discrete.py +++ b/qiskit/pulse/library/discrete.py @@ -17,6 +17,7 @@ """ from typing import Optional +from qiskit.utils.deprecation import deprecate_func from ..exceptions import PulseError from .waveform import Waveform from . import continuous @@ -26,6 +27,16 @@ _sampled_constant_pulse = samplers.midpoint(continuous.constant) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including constant() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Constant(...).get_waveform(). " + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def constant(duration: int, amp: complex, name: Optional[str] = None) -> Waveform: r"""Generates constant-sampled :class:`~qiskit.pulse.library.Waveform`. @@ -46,6 +57,13 @@ def constant(duration: int, amp: complex, name: Optional[str] = None) -> Wavefor _sampled_zero_pulse = samplers.midpoint(continuous.zero) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including zero() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Constant(amp=0,...).get_waveform().", + pending=True, +) def zero(duration: int, name: Optional[str] = None) -> Waveform: """Generates zero-sampled :class:`~qiskit.pulse.library.Waveform`. @@ -65,6 +83,15 @@ def zero(duration: int, name: Optional[str] = None) -> Waveform: _sampled_square_pulse = samplers.midpoint(continuous.square) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including square() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Square(...).get_waveform()." + " Note that pulse.Square() does not support complex values for `amp`," + " and that the phase is defined differently. See documentation.", + pending=True, +) def square( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -97,6 +124,17 @@ def square( _sampled_sawtooth_pulse = samplers.midpoint(continuous.sawtooth) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sawtooth() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Sawtooth(...).get_waveform()." + " Note that pulse.Sawtooth() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`)." + " Also note that the phase is defined differently, such that 2*pi phase" + " shifts by a full cycle.", + pending=True, +) def sawtooth( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -143,6 +181,15 @@ def sawtooth( _sampled_triangle_pulse = samplers.midpoint(continuous.triangle) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including triangle() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Triangle(...).get_waveform()." + " Note that pulse.Triangle() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def triangle( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -189,6 +236,15 @@ def triangle( _sampled_cos_pulse = samplers.midpoint(continuous.cos) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including cos() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Cos(...).get_waveform()." + " Note that pulse.Cos() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def cos( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -218,6 +274,15 @@ def cos( _sampled_sin_pulse = samplers.midpoint(continuous.sin) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sin() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Sin(...).get_waveform()." + " Note that pulse.Sin() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def sin( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -247,6 +312,16 @@ def sin( _sampled_gaussian_pulse = samplers.midpoint(continuous.gaussian) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including gaussian() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Gaussian(...).get_waveform()." + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def gaussian( duration: int, amp: complex, sigma: float, name: Optional[str] = None, zero_ends: bool = True ) -> Waveform: @@ -291,6 +366,15 @@ def gaussian( _sampled_gaussian_deriv_pulse = samplers.midpoint(continuous.gaussian_deriv) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including gaussian_deriv() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.GaussianDeriv(...).get_waveform()." + " Note that pulse.GaussianDeriv() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def gaussian_deriv( duration: int, amp: complex, sigma: float, name: Optional[str] = None ) -> Waveform: @@ -301,7 +385,8 @@ def gaussian_deriv( .. math:: - f(x) = A\frac{(x - \mu)}{\sigma^2}\exp\left(\left(\frac{x - \mu}{2\sigma}\right)^2 \right) + f(x) = -A\frac{(x - \mu)}{\sigma^2}\exp + \left(-\frac{1}{2}\left(\frac{x - \mu}{\sigma}\right)^2 \right) i.e. the derivative of the Gaussian function, with center :math:`\mu=` ``duration/2``. @@ -318,6 +403,15 @@ def gaussian_deriv( _sampled_sech_pulse = samplers.midpoint(continuous.sech) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sech() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Sech(...).get_waveform()." + " Note that pulse.Sech() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def sech( duration: int, amp: complex, sigma: float, name: str = None, zero_ends: bool = True ) -> Waveform: @@ -360,6 +454,15 @@ def sech( _sampled_sech_deriv_pulse = samplers.midpoint(continuous.sech_deriv) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sech_deriv() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.SechDeriv(...).get_waveform()." + " Note that pulse.SechDeriv() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def sech_deriv(duration: int, amp: complex, sigma: float, name: str = None) -> Waveform: r"""Generates unnormalized sech derivative :class:`~qiskit.pulse.library.Waveform`. @@ -385,6 +488,16 @@ def sech_deriv(duration: int, amp: complex, sigma: float, name: str = None) -> W _sampled_gaussian_square_pulse = samplers.midpoint(continuous.gaussian_square) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including gaussian_square() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.GaussianSquare(...).get_waveform()." + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def gaussian_square( duration: int, amp: complex, @@ -451,6 +564,16 @@ def gaussian_square( _sampled_drag_pulse = samplers.midpoint(continuous.drag) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including drag() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Drag(...).get_waveform()." + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def drag( duration: int, amp: complex, diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 878f6a4741f5..b7cb04df9503 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -1085,9 +1085,9 @@ def gaussian_square_echo( The Gaussian Square Echo pulse is composed of three pulses. First, a Gaussian Square pulse :math:`f_{echo}(x)` with amplitude ``amp`` and phase ``angle`` playing for half duration, followed by a second Gaussian Square pulse :math:`-f_{echo}(x)` with opposite amplitude - and same phase playing for the rest of the duration. Third a Gaussian Square pulse + and same phase playing for the rest of the duration. Third a Gaussian Square pulse :math:`f_{active}(x)` with amplitude ``active_amp`` and phase ``active_angle`` - playing for the entire duration. The Gaussian Square Echo pulse :math:`g_e()` + playing for the entire duration. The Gaussian Square Echo pulse :math:`g_e()` can be written as: .. math:: @@ -1099,11 +1099,11 @@ def gaussian_square_echo( & \\frac{\\text{duration}}{2} < x\ \\end{cases}\\\\ - One case where this pulse can be used is when implementing a direct CNOT gate with - a cross-resonance superconducting qubit architecture. When applying this pulse to - the target qubit, the active portion can be used to cancel IX terms from the + One case where this pulse can be used is when implementing a direct CNOT gate with + a cross-resonance superconducting qubit architecture. When applying this pulse to + the target qubit, the active portion can be used to cancel IX terms from the cross-resonance drive while the echo portion can reduce the impact of a static ZZ coupling. - + Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified. If ``risefall_sigma_ratio`` is not ``None`` and ``width`` is ``None``: @@ -1122,10 +1122,10 @@ def gaussian_square_echo( .. _citation1: https://iopscience.iop.org/article/10.1088/2058-9565/abe519 - .. |citation1| replace:: *Jurcevic, P., Javadi-Abhari, A., Bishop, L. S., - Lauer, I., Bogorin, D. F., Brink, M., Capelluto, L., G{\"u}nl{\"u}k, O., + .. |citation1| replace:: *Jurcevic, P., Javadi-Abhari, A., Bishop, L. S., + Lauer, I., Bogorin, D. F., Brink, M., Capelluto, L., G{\"u}nl{\"u}k, O., Itoko, T., Kanazawa, N. & others - Demonstration of quantum volume 64 on a superconducting quantum + Demonstration of quantum volume 64 on a superconducting quantum computing system. (Section V)* Args: duration: Pulse length in terms of the sampling period `dt`. @@ -1269,6 +1269,72 @@ def gaussian_square_echo( return instance +def GaussianDeriv( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """An unnormalized Gaussian derivative pulse. + + The Gaussian function is centered around the halfway point of the pulse, + and the envelope of the pulse is given by: + + .. math:: + + f(x) = -\\text{A}\\frac{x-\\mu}{\\text{sigma}^{2}}\\exp + \\left[-\\left(\\frac{x-\\mu}{2\\text{sigma}}\\right)^{2}\\right] , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + and :math:`\\mu=\\text{duration}/2`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the pulse + (the value of the corresponding Gaussian at the midpoint `duration`/2). + sigma: A measure of how wide or narrow the corresponding Gaussian peak is in terms of `dt`; + described mathematically in the class docstring. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + parameters = {"sigma": sigma} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") + envelope_expr = ( + -_amp + * sym.exp(sym.I * _angle) + * ((_t - (_duration / 2)) / _sigma**2) + * sym.exp(-(1 / 2) * ((_t - (_duration / 2)) / _sigma) ** 2) + ) + consts_expr = _sigma > 0 + valid_amp_conditions_expr = sym.Abs(_amp / _sigma) <= sym.exp(1 / 2) + + instance = ScalableSymbolicPulse( + pulse_type="GaussianDeriv", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + class Drag(metaclass=_PulseType): """The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse with an additional Gaussian derivative component and lifting applied. @@ -1649,7 +1715,7 @@ def Triangle( name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> ScalableSymbolicPulse: - """A triangle pulse. + """A triangle wave pulse. The envelope of the pulse is given by: @@ -1709,3 +1775,221 @@ def Triangle( instance.validate_parameters() return instance + + +def Square( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + phase: Union[float, ParameterExpression], + freq: Optional[Union[float, ParameterExpression]] = None, + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """A square wave pulse. + + The envelope of the pulse is given by: + + .. math:: + + f(x) = \\text{A}\\text{sign}\\left[\\sin + \\left(2\\pi x\\times\\text{freq}+\\text{phase}\\right)\\right] , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + and :math:`\\text{sign}` + is the sign function with the convention :math:`\\text{sign}\\left(0\\right)=1`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the square wave. Wave range is [-`amp`,`amp`]. + phase: The phase of the square wave (note that this is not equivalent to the angle of + the complex amplitude). + freq: The frequency of the square wave, in terms of 1 over sampling period. + If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). + The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + if freq is None: + freq = 1 / duration + parameters = {"freq": freq, "phase": phase} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") + _x = _freq * _t + _phase / (2 * sym.pi) + + envelope_expr = ( + _amp * sym.exp(sym.I * _angle) * (2 * (2 * sym.floor(_x) - sym.floor(2 * _x)) + 1) + ) + + consts_expr = sym.And(_freq > 0, _freq < 0.5) + + # This might fail for waves shorter than a single cycle + valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 + + instance = ScalableSymbolicPulse( + pulse_type="Square", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + +def Sech( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + zero_ends: Optional[bool] = True, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """An unnormalized sech pulse. + + The sech function is centered around the halfway point of the pulse, + and the envelope of the pulse is given by: + + .. math:: + + f(x) = \\text{A}\\text{sech}\\left( + \\frac{x-\\mu}{\\text{sigma}}\\right) , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + and :math:`\\mu=\\text{duration}/2`. + + If `zero_ends` is set to `True`, the output `y` is modified: + .. math:: + + y\\left(x\\right) \\mapsto \\text{A}\\frac{y-y^{*}}{\\text{A}-y^{*}}, + + where :math:`y^{*}` is the value of :math:`y` at the endpoints (at :math:`x=-1 + and :math:`x=\\text{duration}+1`). This shifts the endpoints value to zero, while also + rescaling to preserve the amplitude at `:math:`\\text{duration}/2``. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the pulse (the value at the midpoint `duration`/2). + sigma: A measure of how wide or narrow the sech peak is in terms of `dt`; + described mathematically in the class docstring. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + zero_ends: If True, zeros the ends at x = -1, x = `duration` + 1, + but rescales to preserve `amp`. Default value True. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + parameters = {"sigma": sigma} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") + complex_amp = _amp * sym.exp(sym.I * _angle) + envelope_expr = complex_amp * sym.sech((_t - (_duration / 2)) / _sigma) + + if zero_ends: + shift_val = complex_amp * sym.sech((-1 - (_duration / 2)) / _sigma) + envelope_expr = complex_amp * (envelope_expr - shift_val) / (complex_amp - shift_val) + + consts_expr = _sigma > 0 + + valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 + + instance = ScalableSymbolicPulse( + pulse_type="Sech", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + +def SechDeriv( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """An unnormalized sech derivative pulse. + + The sech function is centered around the halfway point of the pulse, and the envelope of the + pulse is given by: + + .. math:: + + f(x) = \\text{A}\\frac{d}{dx}\\left[\\text{sech} + \\left(\\frac{x-\\mu}{\\text{sigma}}\\right)\\right] , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + :math:`\\mu=\\text{duration}/2`, and :math:`d/dx` is a derivative with respect to `x`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the pulse (the value of the corresponding sech + function at the midpoint `duration`/2). + sigma: A measure of how wide or narrow the corresponding sech peak is, in terms of `dt`; + described mathematically in the class docstring. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + parameters = {"sigma": sigma} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") + time_argument = (_t - (_duration / 2)) / _sigma + sech_deriv = -sym.tanh(time_argument) * sym.sech(time_argument) / _sigma + + envelope_expr = _amp * sym.exp(sym.I * _angle) * sech_deriv + + consts_expr = _sigma > 0 + + valid_amp_conditions_expr = sym.Abs(_amp) / _sigma <= 2.0 + + instance = ScalableSymbolicPulse( + pulse_type="SechDeriv", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance diff --git a/releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml b/releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml new file mode 100644 index 000000000000..37c776060d89 --- /dev/null +++ b/releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + The :class:`~qiskit.pulse.SymbolicPulse` library was extended. The new pulses in the library are: + + * :func:`~qiskit.pulse.library.GaussianDeriv` + * :func:`~qiskit.pulse.library.Sech` + * :func:`~qiskit.pulse.library.SechDeriv` + * :func:`~qiskit.pulse.library.Square` + + The new functions return a :class:`ScalableSymbolicPulse`, and match the functionality + of the corresponding functions in the discrete pulse library, with the exception of + `Square()` for which a phase of :math:`2\\pi` shifts by a full cycle (contrary to the + discrete `square()` where such a shift was induced by a :math:`\\pi` phase). diff --git a/test/python/pulse/test_discrete_pulses.py b/test/python/pulse/test_discrete_pulses.py index 02b87bdfc364..53157d54c5f8 100644 --- a/test/python/pulse/test_discrete_pulses.py +++ b/test/python/pulse/test_discrete_pulses.py @@ -224,3 +224,32 @@ def test_drag(self): drag_pulse = library.drag(duration, amp, sigma, beta=beta) self.assertIsInstance(drag_pulse, Waveform) np.testing.assert_array_almost_equal(drag_pulse.samples, drag_ref) + + def test_pending_deprecation_warnings(self): + """Test that pending deprecation warnings are raised when the discrete library is used.""" + with self.assertWarns(PendingDeprecationWarning): + library.drag(duration=10, amp=0.5, sigma=0.1, beta=0.1) + with self.assertWarns(PendingDeprecationWarning): + library.gaussian_square(duration=10, amp=0.5, sigma=0.1, risefall=2, width=6) + with self.assertWarns(PendingDeprecationWarning): + library.gaussian(duration=10, amp=0.5, sigma=0.1) + with self.assertWarns(PendingDeprecationWarning): + library.sin(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.cos(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.sawtooth(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.zero(duration=10) + with self.assertWarns(PendingDeprecationWarning): + library.constant(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.triangle(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.gaussian_deriv(duration=10, amp=0.5, sigma=3) + with self.assertWarns(PendingDeprecationWarning): + library.sech_deriv(duration=10, amp=0.5, sigma=3) + with self.assertWarns(PendingDeprecationWarning): + library.sech(duration=10, amp=0.5, sigma=3) + with self.assertWarns(PendingDeprecationWarning): + library.square(duration=10, amp=0.5) diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index a289ad018955..675864c323af 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -25,18 +25,26 @@ GaussianSquare, GaussianSquareDrag, gaussian_square_echo, + GaussianDeriv, Drag, Sin, Cos, Sawtooth, Triangle, + Square, + Sech, + SechDeriv, gaussian, gaussian_square, + gaussian_deriv, drag as pl_drag, sin, cos, triangle, sawtooth, + square, + sech, + sech_deriv, ) from qiskit.pulse import functional_pulse, PulseError @@ -138,12 +146,17 @@ def test_construction(self): Gaussian(duration=25, sigma=4, amp=0.5j) GaussianSquare(duration=150, amp=0.2, sigma=8, width=140) GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=2.5) + GaussianDeriv(duration=150, amp=0.2, sigma=8) Constant(duration=150, amp=0.1 + 0.4j) Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4) Sin(duration=25, amp=0.5, freq=0.1, phase=0.5, angle=0.5) Cos(duration=30, amp=0.5, freq=0.1, phase=-0.5) Sawtooth(duration=40, amp=0.5, freq=0.2, phase=3.14) Triangle(duration=50, amp=0.5, freq=0.01, phase=0.5) + Square(duration=50, amp=0.5, freq=0.01, phase=0.5) + Sech(duration=50, amp=0.5, sigma=10) + Sech(duration=50, amp=0.5, sigma=10, zero_ends=False) + SechDeriv(duration=50, amp=0.5, sigma=10) # This test should be removed once deprecation of complex amp is completed. def test_complex_amp_deprecation(self): @@ -412,7 +425,7 @@ def test_sin_pulse(self): Sin(duration=duration, amp=amp, freq=5, phase=phase) def test_cos_pulse(self): - """Test that Cin sample pulse matches expectations, and parameter validation""" + """Test that Cos sample pulse matches expectations, and parameter validation""" duration = 100 amp = 0.5 freq = 0.1 @@ -428,6 +441,20 @@ def test_cos_pulse(self): with self.assertRaises(PulseError): Cos(duration=duration, amp=amp, freq=5, phase=phase) + def test_square_pulse(self): + """Test that Square sample pulse matches expectations, and parameter validation""" + duration = 100 + amp = 0.5 + freq = 0.1 + phase = 0.3 + square_pulse = Square(duration=duration, amp=amp, freq=freq, phase=phase) + square_waveform = square(duration=duration, amp=amp, freq=freq, phase=phase / 2) + + np.testing.assert_almost_equal(square_pulse.get_waveform().samples, square_waveform.samples) + + with self.assertRaises(PulseError): + Square(duration=duration, amp=amp, freq=5, phase=phase) + def test_sawtooth_pulse(self): """Test that Sawtooth sample pulse matches expectations, and parameter validation""" duration = 100 @@ -449,7 +476,7 @@ def test_sawtooth_pulse(self): Sawtooth(duration=duration, amp=amp, freq=5, phase=phase) def test_triangle_pulse(self): - """Test that Sawtooth sample pulse matches expectations, and parameter validation""" + """Test that Triangle sample pulse matches expectations, and parameter validation""" duration = 100 amp = 0.5 freq = 0.1 @@ -467,6 +494,51 @@ def test_triangle_pulse(self): with self.assertRaises(PulseError): Triangle(duration=duration, amp=amp, freq=5, phase=phase) + def test_gaussian_deriv_pulse(self): + """Test that GaussianDeriv sample pulse matches expectations""" + duration = 300 + amp = 0.5 + sigma = 100 + gaussian_deriv_pulse = GaussianDeriv(duration=duration, amp=amp, sigma=sigma) + gaussian_deriv_waveform = gaussian_deriv(duration=duration, amp=amp, sigma=sigma) + np.testing.assert_almost_equal( + gaussian_deriv_pulse.get_waveform().samples, gaussian_deriv_waveform.samples + ) + with self.assertRaises(PulseError): + Sech(duration=duration, amp=amp, sigma=0) + + def test_sech_pulse(self): + """Test that Sech sample pulse matches expectations, and parameter validation""" + duration = 100 + amp = 0.5 + sigma = 10 + # Zero ends = True + sech_pulse = Sech(duration=duration, amp=amp, sigma=sigma) + sech_waveform = sech(duration=duration, amp=amp, sigma=sigma) + np.testing.assert_almost_equal(sech_pulse.get_waveform().samples, sech_waveform.samples) + + # Zero ends = False + sech_pulse = Sech(duration=duration, amp=amp, sigma=sigma, zero_ends=False) + sech_waveform = sech(duration=duration, amp=amp, sigma=sigma, zero_ends=False) + np.testing.assert_almost_equal(sech_pulse.get_waveform().samples, sech_waveform.samples) + + with self.assertRaises(PulseError): + Sech(duration=duration, amp=amp, sigma=-5) + + def test_sech_deriv_pulse(self): + """Test that SechDeriv sample pulse matches expectations, and parameter validation""" + duration = 100 + amp = 0.5 + sigma = 10 + sech_deriv_pulse = SechDeriv(duration=duration, amp=amp, sigma=sigma) + sech_deriv_waveform = sech_deriv(duration=duration, amp=amp, sigma=sigma) + np.testing.assert_almost_equal( + sech_deriv_pulse.get_waveform().samples, sech_deriv_waveform.samples + ) + + with self.assertRaises(PulseError): + SechDeriv(duration=duration, amp=amp, sigma=-5) + def test_constant_samples(self): """Test the constant pulse and its sampled construction.""" const = Constant(duration=150, amp=0.1 + 0.4j) @@ -531,6 +603,32 @@ def test_repr(self): self.assertEqual(repr(drag), "Drag(duration=5, sigma=7, beta=1, amp=0.5, angle=0)") const = Constant(duration=150, amp=0.1, angle=0.3) self.assertEqual(repr(const), "Constant(duration=150, amp=0.1, angle=0.3)") + sin_pulse = Sin(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(sin_pulse), "Sin(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + cos_pulse = Cos(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(cos_pulse), "Cos(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + triangle_pulse = Triangle(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(triangle_pulse), "Triangle(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + sawtooth_pulse = Sawtooth(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(sawtooth_pulse), "Sawtooth(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + sech_pulse = Sech(duration=150, amp=0.1, angle=0.3, sigma=10) + self.assertEqual(repr(sech_pulse), "Sech(duration=150, sigma=10, amp=0.1, angle=0.3)") + sech_deriv_pulse = SechDeriv(duration=150, amp=0.1, angle=0.3, sigma=10) + self.assertEqual( + repr(sech_deriv_pulse), "SechDeriv(duration=150, sigma=10, amp=0.1, angle=0.3)" + ) + gaussian_deriv_pulse = GaussianDeriv(duration=150, amp=0.1, angle=0.3, sigma=10) + self.assertEqual( + repr(gaussian_deriv_pulse), "GaussianDeriv(duration=150, sigma=10, amp=0.1, angle=0.3)" + ) def test_param_validation(self): """Test that parametric pulse parameters are validated when initialized.""" @@ -727,6 +825,74 @@ def test_triangle_limit_amplitude_per_instance(self): waveform = Triangle(duration=100, amp=1.1, phase=0, limit_amplitude=False) self.assertGreater(np.abs(waveform.amp), 1.0) + def test_square_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + Square(duration=100, amp=1.1, phase=0) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = Square(duration=100, amp=1.1, phase=0) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_square_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + Square(duration=100, amp=1.1, phase=0) + + waveform = Square(duration=100, amp=1.1, phase=0, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_gaussian_deriv_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + GaussianDeriv(duration=100, amp=5, sigma=1) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = GaussianDeriv(duration=100, amp=5, sigma=1) + self.assertGreater(np.abs(waveform.amp / waveform.sigma), np.exp(0.5)) + + def test_gaussian_deriv_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + GaussianDeriv(duration=100, amp=5, sigma=1) + + waveform = GaussianDeriv(duration=100, amp=5, sigma=1, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp / waveform.sigma), np.exp(0.5)) + + def test_sech_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + Sech(duration=100, amp=5, sigma=1) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = Sech(duration=100, amp=5, sigma=1) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_sech_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + Sech(duration=100, amp=5, sigma=1) + + waveform = Sech(duration=100, amp=5, sigma=1, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_sech_deriv_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + SechDeriv(duration=100, amp=5, sigma=1) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = SechDeriv(duration=100, amp=5, sigma=1) + self.assertGreater(np.abs(waveform.amp) / waveform.sigma, 2.0) + + def test_sech_deriv_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + SechDeriv(duration=100, amp=5, sigma=1) + + waveform = SechDeriv(duration=100, amp=5, sigma=1, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp) / waveform.sigma, 2.0) + def test_get_parameters(self): """Test getting pulse parameters as attribute.""" drag_pulse = Drag(duration=100, amp=0.1, sigma=40, beta=3)