From e76a0a5b59de977ed3415775b743b8e3e063a58f Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Tue, 6 Dec 2022 18:30:59 -0500 Subject: [PATCH 1/2] Add GaussianSquareDrag symbolic pulse shape This pulse shape is similar to the GaussianSquare pulse with Drag components in the rise and fall of the pulse. --- qiskit/pulse/__init__.py | 1 + qiskit/pulse/library/__init__.py | 10 +- qiskit/pulse/library/symbolic_pulses.py | 145 ++++++++++++++++++ qiskit/qobj/converters/pulse_instruction.py | 1 + ...an-square-drag-pulse-1e54fe77e59d5247.yaml | 10 ++ test/python/pulse/test_pulse_lib.py | 111 ++++++++++++++ 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/gaussian-square-drag-pulse-1e54fe77e59d5247.yaml diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index e6dab41cfb29..f8c88f6308a2 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -141,6 +141,7 @@ Drag, Gaussian, GaussianSquare, + GaussianSquareDrag, ParametricPulse, SymbolicPulse, Waveform, diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index e7050dd1949b..5a5f42fd505f 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -92,6 +92,7 @@ Drag Gaussian GaussianSquare + GaussianSquareDrag """ @@ -111,6 +112,13 @@ drag, ) from .parametric_pulses import ParametricPulse -from .symbolic_pulses import SymbolicPulse, Gaussian, GaussianSquare, Drag, Constant +from .symbolic_pulses import ( + SymbolicPulse, + Gaussian, + GaussianSquare, + GaussianSquareDrag, + Drag, + Constant, +) from .pulse import Pulse from .waveform import Waveform diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 713265c47a46..5dc5493cc2ec 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -826,6 +826,151 @@ def __new__( return instance +def GaussianSquareDrag( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + beta: Union[float, ParameterExpression], + width: Optional[Union[float, ParameterExpression]] = None, + angle: Optional[Union[float, ParameterExpression]] = 0.0, + risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> SymbolicPulse: + """A square pulse with a Drag shaped rise and fall + + This pulse shape is similar to :class:`~.GaussianSquare` but uses + :class:`~.Drag` for its rise and fall instead of :class:`~.Gaussian`. The + addition of the DRAG component of the rise and fall is sometimes helpful in + suppressing the spectral content of the pulse at frequencies near the + fundamental frequency of the drive which can cause unwanted excitation of + spectators close in frequency to that fundamental frequency. + + 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``: + + .. math:: + + \\text{risefall} &= \\text{risefall_sigma_ratio} \\times \\text{sigma}\\\\ + \\text{width} &= \\text{duration} - 2 \\times \\text{risefall} + + If ``width`` is not None and ``risefall_sigma_ratio`` is None: + + .. math:: \\text{risefall} = \\frac{\\text{duration} - \\text{width}}{2} + + Gaussian :math:`g(x, c, σ)` and lifted gaussian :math:`g'(x, c, σ)` curves + can be written as: + + .. math:: + + g(x, c, σ) &= \\exp\\Bigl(-\\frac12 \\frac{(x - c)^2}{σ^2}\\Bigr)\\\\ + g'(x, c, σ) &= \\frac{g(x, c, σ)-g(-1, c, σ)}{1-g(-1, c, σ)} + + From these, the lifted DRAG curve :math:`d'(x, c, σ, β)` can be written as + + .. math:: + + d'(x, c, σ, β) = g'(x, c, σ) \\times \\Bigl(1 + 1j \\times β \\times\ + \\Bigl(-\\frac{x - c}{σ^2}\\Bigr)\\Bigr) + + The lifted gaussian square drag pulse :math:`f'(x)` is defined as: + + .. math:: + + f'(x) &= \\begin{cases}\ + \\text{A} \\times d'(x, \\text{risefall}, \\text{sigma}, \\text{beta})\ + & x < \\text{risefall}\\\\ + \\text{A}\ + & \\text{risefall} \\le x < \\text{risefall} + \\text{width}\\\\ + \\text{A} \\times \\times d'(\ + x - (\\text{risefall} + \\text{width}),\ + \\text{risefall},\ + \\text{sigma},\ + \\text{beta}\ + )\ + & \\text{risefall} + \\text{width} \\le x\ + \\end{cases}\\\\ + + where :math:`\\text{A} = \\text{amp} \\times + \\exp\\left(i\\times\\text{angle}\\right)`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The amplitude of the DRAG rise and fall and of the square pulse. + sigma: A measure of how wide or narrow the DRAG risefall is; see the class + docstring for more details. + beta: The DRAG correction amplitude. + width: The duration of the embedded square pulse. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + risefall_sigma_ratio: The ratio of each risefall duration to sigma. + 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: + SymbolicPulse instance. + + Raises: + PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. + """ + # Convert risefall_sigma_ratio into width which is defined in OpenPulse spec + if width is None and risefall_sigma_ratio is None: + raise PulseError( + "Either the pulse width or the risefall_sigma_ratio parameter must be specified." + ) + if width is not None and risefall_sigma_ratio is not None: + raise PulseError( + "Either the pulse width or the risefall_sigma_ratio parameter can be specified" + " but not both." + ) + if width is None and risefall_sigma_ratio is not None: + width = duration - 2.0 * risefall_sigma_ratio * sigma + + parameters = {"amp": amp, "sigma": sigma, "width": width, "beta": beta, "angle": angle} + + # Prepare symbolic expressions + _t, _duration, _amp, _sigma, _beta, _width, _angle = sym.symbols( + "t, duration, amp, sigma, beta, width, angle" + ) + _center = _duration / 2 + + _sq_t0 = _center - _width / 2 + _sq_t1 = _center + _width / 2 + + _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) + _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration + 1, _sigma) + _deriv_ledge = -(_t - _sq_t0) / (_sigma**2) * _gaussian_ledge + _deriv_redge = -(_t - _sq_t1) / (_sigma**2) * _gaussian_redge + + envelope_expr = ( + _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise( + (_gaussian_ledge + sym.I * _beta * _deriv_ledge, _t <= _sq_t0), + (_gaussian_redge + sym.I * _beta * _deriv_redge, _t >= _sq_t1), + (1, True), + ) + ) + consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) + valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) + + instance = SymbolicPulse( + pulse_type="GaussianSquareDrag", + duration=duration, + 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. diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 55480367dcd4..a3e855dd0675 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -43,6 +43,7 @@ class ParametricPulseShapes(Enum): gaussian = "Gaussian" gaussian_square = "GaussianSquare" + gaussian_square_drag = "GaussianSquareDrag" drag = "Drag" constant = "Constant" diff --git a/releasenotes/notes/gaussian-square-drag-pulse-1e54fe77e59d5247.yaml b/releasenotes/notes/gaussian-square-drag-pulse-1e54fe77e59d5247.yaml new file mode 100644 index 000000000000..b66821d3a68e --- /dev/null +++ b/releasenotes/notes/gaussian-square-drag-pulse-1e54fe77e59d5247.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add new :class:`~qiskit.pulse.GaussianSquareDrag` pulse shape. This pulse + shape is similar to :class:`~qiskit.pulse.GaussianSquare` but uses the + :class:`~qiskit.pulse.Drag` shape during its rise and fall. The correction + from the DRAG pulse shape can suppress part of the frequency spectrum of + the rise and fall of the pulse which can help avoid exciting spectator + qubits when they are close in frequency to the drive frequency of the + pulse. diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 428d8094a2f7..74fad36a5368 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -23,6 +23,7 @@ Constant, Gaussian, GaussianSquare, + GaussianSquareDrag, Drag, gaussian, gaussian_square, @@ -192,6 +193,79 @@ def test_gauss_square_passes_validation_after_construction(self): pulse = GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100) pulse.validate_parameters() + def test_gaussian_square_drag_pulse(self): + """Test that GaussianSquareDrag sample pulse matches expectations. + + Test that the real part of the envelop matches GaussianSquare and that + the rise and fall match Drag. + """ + risefall = 32 + sigma = 4 + amp = 0.5 + width = 100 + beta = 1 + duration = width + 2 * risefall + + gsd = GaussianSquareDrag(duration=duration, sigma=sigma, amp=amp, width=width, beta=beta) + gsd_samples = gsd.get_waveform().samples + + gs_pulse = GaussianSquare(duration=duration, sigma=sigma, amp=amp, width=width) + np.testing.assert_almost_equal( + np.real(gsd_samples), + np.real(gs_pulse.get_waveform().samples), + ) + gsd2 = GaussianSquareDrag( + duration=duration, + sigma=sigma, + amp=amp, + beta=beta, + risefall_sigma_ratio=risefall / sigma, + ) + np.testing.assert_almost_equal( + gsd_samples, + gsd2.get_waveform().samples, + ) + + drag_pulse = Drag(duration=2 * risefall, amp=amp, sigma=sigma, beta=beta) + np.testing.assert_almost_equal( + gsd_samples[:risefall], + drag_pulse.get_waveform().samples[:risefall], + ) + np.testing.assert_almost_equal( + gsd_samples[-risefall:], + drag_pulse.get_waveform().samples[-risefall:], + ) + + def test_gauss_square_drag_extreme(self): + """Test that the gaussian square drag pulse can build a drag pulse.""" + duration = 125 + sigma = 4 + amp = 0.5 + angle = 1.5 + beta = 1 + gsd = GaussianSquareDrag( + duration=duration, sigma=sigma, amp=amp, width=0, beta=beta, angle=angle + ) + drag = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) + np.testing.assert_almost_equal(gsd.get_waveform().samples, drag.get_waveform().samples) + + def test_gaussian_square_drag_validation(self): + """Test drag beta parameter validation.""" + + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=2) + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=4) + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=0.5, beta=20) + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=-1, beta=2) + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=-2) + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=6) + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=-0.5, beta=25, angle=1.5) + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=20) + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=50, width=0, sigma=4, amp=0.8, beta=20) + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=50, width=0, sigma=4, amp=0.8, beta=-20) + def test_drag_pulse(self): """Test that the Drag sample pulse matches the pulse library.""" drag = Drag(duration=25, sigma=4, amp=0.5j, beta=1) @@ -274,6 +348,16 @@ def test_repr(self): repr(gaus_square), "GaussianSquare(duration=20, amp=1.0, sigma=30, width=14.0, angle=0.2)", ) + gsd = GaussianSquareDrag(duration=20, sigma=30, amp=1.0, width=3, beta=1) + self.assertEqual( + repr(gsd), + "GaussianSquareDrag(duration=20, amp=1.0, sigma=30, width=3, beta=1, angle=0.0)", + ) + gsd = GaussianSquareDrag(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1, beta=1) + self.assertEqual( + repr(gsd), + "GaussianSquareDrag(duration=20, amp=1.0, sigma=30, width=14.0, beta=1, angle=0.0)", + ) drag = Drag(duration=5, amp=0.5, sigma=7, beta=1) self.assertEqual(repr(drag), "Drag(duration=5, amp=0.5, sigma=7, beta=1, angle=0)") const = Constant(duration=150, amp=0.1, angle=0.3) @@ -291,6 +375,14 @@ def test_param_validation(self): GaussianSquare(duration=150, amp=0.2, sigma=8, width=160) with self.assertRaises(PulseError): GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10) + + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=150, amp=0.2, sigma=8, beta=1) + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=150, amp=0.2, sigma=8, width=160, beta=1) + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10, beta=1) + with self.assertRaises(PulseError): Constant(duration=150, amp=0.9 + 0.8j) with self.assertRaises(PulseError): @@ -343,6 +435,25 @@ def test_gaussian_square_limit_amplitude_per_instance(self): ) self.assertGreater(np.abs(waveform.amp), 1.0) + def test_gaussian_square_drag_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = GaussianSquareDrag(duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_gaussian_square_drag_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + GaussianSquareDrag(duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10) + + waveform = GaussianSquareDrag( + duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10, limit_amplitude=False + ) + self.assertGreater(np.abs(waveform.amp), 1.0) + def test_drag_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): From 39dd8547916c512d4ba61820bea3c36a11c941c5 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 5 Jan 2023 17:13:46 -0500 Subject: [PATCH 2/2] Clarify description of pulse --- qiskit/pulse/library/symbolic_pulses.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 5dc5493cc2ec..678a51722fd3 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -842,9 +842,11 @@ def GaussianSquareDrag( This pulse shape is similar to :class:`~.GaussianSquare` but uses :class:`~.Drag` for its rise and fall instead of :class:`~.Gaussian`. The addition of the DRAG component of the rise and fall is sometimes helpful in - suppressing the spectral content of the pulse at frequencies near the - fundamental frequency of the drive which can cause unwanted excitation of - spectators close in frequency to that fundamental frequency. + suppressing the spectral content of the pulse at frequencies near to, but + slightly offset from, the fundamental frequency of the drive. When there is + a spectator qubit close in frequency to the fundamental frequency, + suppressing the drive at the spectator's frequency can help avoid unwanted + excitation of the spectator. Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified.