From bedecbdce563f4e83acc11c7ec5ca6878a36a4b7 Mon Sep 17 00:00:00 2001 From: Mirko Amico <31739486+miamico@users.noreply.github.com> Date: Fri, 19 May 2023 19:58:45 -0600 Subject: [PATCH] add gaussian square echo pulse (#9370) * add gaussian square echo pulse * address some PR comments * fixed parameters of symbolicpulse * adding math description and reference * change variable names * fix lint * remove width_echo parameter * fix lint * add release notes * fix conflicts * address some PR comments * fixed parameters of symbolicpulse * adding math description and reference * change variable names * fix lint * remove width_echo parameter * fix lint * add release notes * Update first paragraph Co-authored-by: Naoki Kanazawa * Update amp param to float Co-authored-by: Naoki Kanazawa * Update angle def param to float Co-authored-by: Naoki Kanazawa * Update amp constrain Co-authored-by: Naoki Kanazawa * drafting tests * update docstring * finish up test * fix missing pulses from init * addding Cos * missing sin * fixed tests * black formatting * break string * fixing strings * reformatting * fix lint * fixing last things * fix tests * fix black * fix another test * fix amp validation * fixing docstring * Update qiskit/pulse/library/symbolic_pulses.py Co-authored-by: Will Shanks * rename to gaussian_square_echo * fix lint * Update qiskit/qobj/converters/pulse_instruction.py Co-authored-by: Will Shanks * Update releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml Co-authored-by: Will Shanks --------- Co-authored-by: Naoki Kanazawa Co-authored-by: Will Shanks --- qiskit/pulse/__init__.py | 1 + qiskit/pulse/library/__init__.py | 2 + qiskit/pulse/library/symbolic_pulses.py | 201 ++++++++++++++++++ qiskit/qobj/converters/pulse_instruction.py | 1 + ...an-square-echo-pulse-84306f1a02e2bb28.yaml | 9 + test/python/pulse/test_pulse_lib.py | 106 +++++++++ 6 files changed, 320 insertions(+) create mode 100644 releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index d5432eaa7b..5fe159b947 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -142,6 +142,7 @@ from qiskit.pulse.library import ( Gaussian, GaussianSquare, GaussianSquareDrag, + gaussian_square_echo, Sin, Cos, Sawtooth, diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 8cd80a3b32..c7103b7a0d 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -88,6 +88,7 @@ Parametric Pulse Representation Gaussian GaussianSquare GaussianSquareDrag + gaussian_square_echo Sin Cos Sawtooth @@ -117,6 +118,7 @@ from .symbolic_pulses import ( Gaussian, GaussianSquare, GaussianSquareDrag, + gaussian_square_echo, Drag, Constant, Sin, diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 9102df2355..2718792ad1 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -1068,6 +1068,207 @@ def GaussianSquareDrag( return instance +def gaussian_square_echo( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + width: Optional[Union[float, ParameterExpression]] = None, + angle: Optional[Union[float, ParameterExpression]] = 0.0, + active_amp: Optional[Union[float, ParameterExpression]] = 0.0, + active_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: + """An echoed Gaussian square pulse with an active tone overlaid on it. + + 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 + :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()` + can be written as: + + .. math:: + + g_e(x) &= \\begin{cases}\ + f_{\\text{active}} + f_{\\text{echo}}(x)\ + & x < \\frac{\\text{duration}}{2}\\\\ + f_{\\text{active}} - f_{\\text{echo}}(x)\ + & \\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 + 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``: + + .. 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} + + References: + 1. |citation1|_ + + .. _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., + Itoko, T., Kanazawa, N. & others + Demonstration of quantum volume 64 on a superconducting quantum + computing system. (Section V)* + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The amplitude of the rise and fall and of the echoed pulse. + sigma: A measure of how wide or narrow the risefall is; see the class + docstring for more details. + width: The duration of the embedded square pulse. + angle: The angle in radians of the complex phase factor uniformly + scaling the echoed pulse. Default value 0. + active_amp: The amplitude of the active pulse. + active_angle: The angle in radian of the complex phase factor uniformly + scaling the active 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: + ScalableSymbolicPulse 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, + "angle": angle, + "sigma": sigma, + "width": width, + "active_amp": active_amp, + "active_angle": active_angle, + } + + # Prepare symbolic expressions + ( + _t, + _duration, + _amp, + _sigma, + _active_amp, + _width, + _angle, + _active_angle, + ) = sym.symbols("t, duration, amp, sigma, active_amp, width, angle, active_angle") + + # gaussian square echo for rotary tone + _center = _duration / 4 + + _width_echo = (_duration - 2 * (_duration - _width)) / 2 + + _sq_t0 = _center - _width_echo / 2 + _sq_t1 = _center + _width_echo / 2 + + _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) + _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration / 2 + 1, _sigma) + + envelope_expr_p = ( + _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise( + (_gaussian_ledge, _t <= _sq_t0), + (_gaussian_redge, _t >= _sq_t1), + (1, True), + ) + ) + + _center_echo = _duration / 2 + _duration / 4 + + _sq_t0_echo = _center_echo - _width_echo / 2 + _sq_t1_echo = _center_echo + _width_echo / 2 + + _gaussian_ledge_echo = _lifted_gaussian(_t, _sq_t0_echo, _duration / 2 - 1, _sigma) + _gaussian_redge_echo = _lifted_gaussian(_t, _sq_t1_echo, _duration + 1, _sigma) + + envelope_expr_echo = ( + -1 + * _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise( + (_gaussian_ledge_echo, _t <= _sq_t0_echo), + (_gaussian_redge_echo, _t >= _sq_t1_echo), + (1, True), + ) + ) + + envelope_expr = sym.Piecewise( + (envelope_expr_p, _t <= _duration / 2), (envelope_expr_echo, _t >= _duration / 2), (0, True) + ) + + # gaussian square for active cancellation tone + _center_active = _duration / 2 + + _sq_t0_active = _center_active - _width / 2 + _sq_t1_active = _center_active + _width / 2 + + _gaussian_ledge_active = _lifted_gaussian(_t, _sq_t0_active, -1, _sigma) + _gaussian_redge_active = _lifted_gaussian(_t, _sq_t1_active, _duration + 1, _sigma) + + envelope_expr_active = ( + _active_amp + * sym.exp(sym.I * _active_angle) + * sym.Piecewise( + (_gaussian_ledge_active, _t <= _sq_t0_active), + (_gaussian_redge_active, _t >= _sq_t1_active), + (1, True), + ) + ) + + envelop_expr_total = envelope_expr + envelope_expr_active + + consts_expr = sym.And( + _sigma > 0, _width >= 0, _duration >= _width, _duration / 2 >= _width_echo + ) + + # Check validity of amplitudes + valid_amp_conditions_expr = sym.And(sym.Abs(_amp) + sym.Abs(_active_amp) <= 1.0) + + instance = SymbolicPulse( + pulse_type="gaussian_square_echo", + duration=duration, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelop_expr_total, + 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 9d4635ad7e..a278d7cca3 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" + gaussian_square_echo = "gaussian_square_echo" drag = "Drag" constant = "Constant" diff --git a/releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml b/releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml new file mode 100644 index 0000000000..b733f6ba62 --- /dev/null +++ b/releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add new :meth:`~qiskit.pulse.gaussian_square_echo` pulse shape. This pulse + is composed by three :class:`~qiskit.pulse.GaussianSquare` pulses. The + first two are echo pulses with duration half of the total duration and + implement rotary tones. The third pulse is a cancellation tone that lasts + the full duration of the pulse and implements correcting single qubit + rotations. \ No newline at end of file diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index f5485762dd..a289ad0189 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -24,6 +24,7 @@ from qiskit.pulse.library import ( Gaussian, GaussianSquare, GaussianSquareDrag, + gaussian_square_echo, Drag, Sin, Cos, @@ -288,6 +289,65 @@ class TestParametricPulses(QiskitTestCase): with self.assertRaises(PulseError): GaussianSquareDrag(duration=50, width=0, sigma=4, amp=0.8, beta=-20) + def test_gaussian_square_echo_pulse(self): + """Test that gaussian_square_echo sample pulse matches expectations. + + Test that the real part of the envelop matches GaussianSquare with + given amplitude and phase active for half duration with another + GaussianSquare active for the other half duration with opposite + amplitude and a GaussianSquare active on the entire duration with + its own amplitude and phase + """ + risefall = 32 + sigma = 4 + amp = 0.5 + width = 100 + duration = width + 2 * risefall + active_amp = 0.1 + width_echo = (duration - 2 * (duration - width)) / 2 + + gse = gaussian_square_echo( + duration=duration, sigma=sigma, amp=amp, width=width, active_amp=active_amp + ) + gse_samples = gse.get_waveform().samples + + gs_echo_pulse_pos = GaussianSquare( + duration=duration / 2, sigma=sigma, amp=amp, width=width_echo + ) + gs_echo_pulse_neg = GaussianSquare( + duration=duration / 2, sigma=sigma, amp=-amp, width=width_echo + ) + gs_active_pulse = GaussianSquare( + duration=duration, sigma=sigma, amp=active_amp, width=width + ) + gs_echo_pulse_pos_samples = np.array( + gs_echo_pulse_pos.get_waveform().samples.tolist() + [0] * int(duration / 2) + ) + gs_echo_pulse_neg_samples = np.array( + [0] * int(duration / 2) + gs_echo_pulse_neg.get_waveform().samples.tolist() + ) + gs_active_pulse_samples = gs_active_pulse.get_waveform().samples + + np.testing.assert_almost_equal( + gse_samples, + gs_echo_pulse_pos_samples + gs_echo_pulse_neg_samples + gs_active_pulse_samples, + ) + + def test_gaussian_square_echo_active_amp_validation(self): + """Test gaussian square echo active amp parameter validation.""" + + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.2) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.4) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.5, active_amp=0.3) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=-0.1, active_amp=0.2) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=-0.2) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.6) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=-0.5, angle=1.5, active_amp=0.25) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=1.1) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=50, width=0, sigma=4, amp=-0.8, active_amp=-0.3) + 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) @@ -451,6 +511,22 @@ class TestParametricPulses(QiskitTestCase): repr(gsd), "GaussianSquareDrag(duration=20, sigma=30, width=14.0, beta=1, amp=1.0, angle=0.0)", ) + gse = gaussian_square_echo(duration=20, sigma=30, amp=1.0, width=3) + self.assertEqual( + repr(gse), + ( + "gaussian_square_echo(duration=20, amp=1.0, angle=0.0, sigma=30, width=3," + " active_amp=0.0, active_angle=0.0)" + ), + ) + gse = gaussian_square_echo(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1) + self.assertEqual( + repr(gse), + ( + "gaussian_square_echo(duration=20, amp=1.0, angle=0.0, sigma=30, width=14.0," + " active_amp=0.0, active_angle=0.0)" + ), + ) drag = Drag(duration=5, amp=0.5, sigma=7, beta=1) 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) @@ -476,6 +552,17 @@ class TestParametricPulses(QiskitTestCase): with self.assertRaises(PulseError): GaussianSquareDrag(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10, beta=1) + with self.assertRaises(PulseError): + gaussian_square_echo( + duration=150, + amp=0.2, + sigma=8, + ) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=150, amp=0.2, sigma=8, width=160) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10) + with self.assertRaises(PulseError): Constant(duration=150, amp=0.9 + 0.8j) with self.assertRaises(PulseError): @@ -536,6 +623,25 @@ class TestParametricPulses(QiskitTestCase): ) self.assertGreater(np.abs(waveform.amp), 1.0) + def test_gaussian_square_echo_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + gaussian_square_echo(duration=1000, sigma=4.0, amp=1.01, width=100) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = gaussian_square_echo(duration=100, sigma=1.0, amp=1.1, width=10) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_gaussian_square_echo_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + gaussian_square_echo(duration=1000, sigma=4.0, amp=1.01, width=100) + + waveform = gaussian_square_echo( + duration=1000, sigma=4.0, amp=1.01, width=100, 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):