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 <nkanazawa1989@gmail.com>

* Update amp param to float

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>

* Update angle def param to float

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>

* Update amp constrain

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>

* 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 <wshaos@posteo.net>

* rename to gaussian_square_echo

* fix lint

* Update qiskit/qobj/converters/pulse_instruction.py

Co-authored-by: Will Shanks <wshaos@posteo.net>

* Update releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml

Co-authored-by: Will Shanks <wshaos@posteo.net>

---------

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>
Co-authored-by: Will Shanks <wshaos@posteo.net>
This commit is contained in:
Mirko Amico 2023-05-19 19:58:45 -06:00 committed by GitHub
parent c3b0afaab2
commit bedecbdce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 320 additions and 0 deletions

View File

@ -142,6 +142,7 @@ from qiskit.pulse.library import (
Gaussian,
GaussianSquare,
GaussianSquareDrag,
gaussian_square_echo,
Sin,
Cos,
Sawtooth,

View File

@ -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,

View File

@ -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.

View File

@ -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"

View File

@ -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.

View File

@ -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):