Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializable parametric pulse #7821

Merged
Merged
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
afe4f96
Symbolic parametric pulse
nkanazawa1989 Feb 27, 2022
9a8a582
Improve performance of parametrized pulse evaluation
wshanks Mar 16, 2022
6f2496a
Implement Constant pulse using Piecewise
wshanks Mar 17, 2022
0424636
Fall back to sympy for parametric pulse evaluation
wshanks Mar 17, 2022
1bf11fa
Use sympy Symbol with sympy lambdify
wshanks Mar 17, 2022
2335f32
WIP: cache symbolic pulse lambda functions
wshanks Mar 18, 2022
2af235d
Merge pull request #48 from wshanks/upgrade/serializable-parametric-p…
nkanazawa1989 Mar 23, 2022
0cc7383
move lambdify to init subclass for called once
nkanazawa1989 Mar 23, 2022
a59af77
turn parameter properties into attribute and remove slots
nkanazawa1989 Mar 23, 2022
21ef762
remove attribute and add __getattr__ and readd slots for better perfo…
nkanazawa1989 Mar 23, 2022
509470a
move symbolic funcs directly to _define
nkanazawa1989 Mar 23, 2022
ed2f661
Convert numerical_func to staticmethod
nkanazawa1989 Mar 23, 2022
c4b41d5
remove symbolic add constraints, numerical func is renamed to definit…
nkanazawa1989 Mar 27, 2022
71ef841
revert changes to parametric pulse and create new symbolic pulse file
nkanazawa1989 Apr 12, 2022
cf7f302
add ITE-like program
nkanazawa1989 Apr 12, 2022
c2981d0
sympy runtime import
nkanazawa1989 Apr 12, 2022
cf17341
update test notebook
nkanazawa1989 Apr 12, 2022
ba685ef
Merge branch 'main' of github.com:Qiskit/qiskit-terra into upgrade/se…
nkanazawa1989 Apr 12, 2022
f2ef950
add attribute docs
nkanazawa1989 Apr 12, 2022
2ea6390
fix non-constraints
nkanazawa1989 Apr 12, 2022
f48796e
remove pulse type instance variable
nkanazawa1989 Apr 12, 2022
0738497
use descriptor
nkanazawa1989 Apr 12, 2022
92f42eb
remove redundant comment
nkanazawa1989 Apr 12, 2022
1440a23
Merge branch 'main' of github.com:Qiskit/qiskit-terra into upgrade/se…
nkanazawa1989 Apr 13, 2022
4345c3b
Update
nkanazawa1989 Apr 13, 2022
1c63ddb
keep raw data for QPY encoding
nkanazawa1989 Apr 13, 2022
d121b0d
update unittest
nkanazawa1989 Apr 13, 2022
48ff3cc
update helper function name
nkanazawa1989 Apr 13, 2022
b9cab67
add reno
nkanazawa1989 Apr 13, 2022
42b3dd9
remove notebook
nkanazawa1989 Apr 13, 2022
761235d
fix lint
nkanazawa1989 Apr 13, 2022
cc12bf8
fix unittest and logic
nkanazawa1989 Apr 13, 2022
6c21edd
add more docs
nkanazawa1989 Apr 14, 2022
43e0e0b
review comments
nkanazawa1989 Apr 15, 2022
6b956de
Merge branch 'main' of github.com:Qiskit/qiskit-terra into upgrade/se…
nkanazawa1989 Apr 18, 2022
eefa5e4
lint fix
nkanazawa1989 Apr 18, 2022
252f62b
fix documentation
nkanazawa1989 Apr 18, 2022
f24e648
minor drawer fix
nkanazawa1989 Apr 19, 2022
4d191be
documentation upgrade
nkanazawa1989 Apr 21, 2022
2c8e7f8
Merge branch 'main' of github.com:Qiskit/qiskit-terra into upgrade/se…
nkanazawa1989 Apr 21, 2022
3a0f506
review comment misc
nkanazawa1989 Apr 28, 2022
d5517d5
remove abstract class methods
nkanazawa1989 May 2, 2022
be2ed0c
add error handling for amplitude
nkanazawa1989 May 5, 2022
67ac61f
treat amp as a special parameter
nkanazawa1989 May 6, 2022
78288ac
Remove expressions for amplitude validation
nkanazawa1989 May 19, 2022
cbada64
Merge branch 'main' of github.com:Qiskit/qiskit-terra into upgrade/se…
nkanazawa1989 May 20, 2022
ecb4ea7
support symengine
nkanazawa1989 Jun 2, 2022
45ae755
use real=False option
nkanazawa1989 Jun 2, 2022
a4a56db
review comment
nkanazawa1989 Jun 6, 2022
d957458
Merge branch 'upgrade/serializable-parametric-pulse-symengine' into u…
nkanazawa1989 Jun 6, 2022
6776743
- fix attribute
nkanazawa1989 Jun 6, 2022
d4e8c43
undo change to requirements-dev
nkanazawa1989 Jun 6, 2022
41bf4f5
fix __getattr__ mechanism
nkanazawa1989 Jun 6, 2022
2aa4f5a
fix type hint reference
nkanazawa1989 Jun 6, 2022
2e521a7
simplification
nkanazawa1989 Jun 6, 2022
09855d7
move amp from constructor to `parameters` dict
nkanazawa1989 Jun 7, 2022
b99eb5c
review comment
nkanazawa1989 Jun 8, 2022
ac23d6f
fix bug
nkanazawa1989 Jun 8, 2022
401c1f4
fix typo
nkanazawa1989 Jun 8, 2022
686e77b
add eval_conditions to skip waveform generation
nkanazawa1989 Jun 8, 2022
84344c2
fall back to sympy lamdify when function is not supported
nkanazawa1989 Jun 9, 2022
914018e
documentation update
nkanazawa1989 Jun 12, 2022
045d67b
replace eval_conditions with valid_amp_conditions
nkanazawa1989 Jun 12, 2022
3b2c571
update hashing and equality, redefine expressions more immutably
nkanazawa1989 Jun 12, 2022
8edaba9
add error message for missing parameter
nkanazawa1989 Jun 12, 2022
9b0adc3
cleanup
nkanazawa1989 Jun 12, 2022
cda7336
check parameter before hashing
nkanazawa1989 Jun 13, 2022
ef35cd9
move amp check to constructor
nkanazawa1989 Jun 13, 2022
15e9f7c
add envelope to hash
nkanazawa1989 Jun 14, 2022
d844253
update docs
nkanazawa1989 Jun 15, 2022
526aaed
Update qiskit/pulse/library/symbolic_pulses.py
nkanazawa1989 Jun 15, 2022
87d3538
Merge branch 'main' into upgrade/serializable-parametric-pulse
nkanazawa1989 Jun 15, 2022
be2b4e0
lint
nkanazawa1989 Jun 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion qiskit/assembler/assemble_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ def _assemble_circuit(

# TODO: why do we need n_qubits and memory_slots in both the header and the config
config = QasmQobjExperimentConfig(n_qubits=num_qubits, memory_slots=memory_slots)

calibrations, pulse_library = _assemble_pulse_gates(circuit, run_config)
if calibrations:
config.calibrations = calibrations
Expand Down
39 changes: 22 additions & 17 deletions qiskit/assembler/assemble_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,25 +187,30 @@ def _assemble_instructions(
acquire_instruction_map = defaultdict(list)
for time, instruction in sched.instructions:

if isinstance(instruction, instructions.Play) and isinstance(
instruction.pulse, library.ParametricPulse
):
pulse_shape = ParametricPulseShapes(type(instruction.pulse)).name
if pulse_shape not in run_config.parametric_pulses:
if isinstance(instruction, instructions.Play):
if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)):
is_backend_supported = True
try:
pulse_shape = ParametricPulseShapes(type(instruction.pulse)).name
if pulse_shape not in run_config.parametric_pulses:
is_backend_supported = False
except ValueError:
# Custom pulse class, or bare SymbolicPulse object.
is_backend_supported = False

if not is_backend_supported:
instruction = instructions.Play(
instruction.pulse.get_waveform(), instruction.channel, name=instruction.name
)

if isinstance(instruction.pulse, library.Waveform):
name = hashlib.sha256(instruction.pulse.samples).hexdigest()
instruction = instructions.Play(
instruction.pulse.get_waveform(), instruction.channel, name=instruction.name
library.Waveform(name=name, samples=instruction.pulse.samples),
channel=instruction.channel,
name=name,
)

if isinstance(instruction, instructions.Play) and isinstance(
instruction.pulse, library.Waveform
):
name = hashlib.sha256(instruction.pulse.samples).hexdigest()
instruction = instructions.Play(
library.Waveform(name=name, samples=instruction.pulse.samples),
channel=instruction.channel,
name=name,
)
user_pulselib[name] = instruction.pulse.samples
user_pulselib[name] = instruction.pulse.samples

# ignore explicit delay instrs on acq channels as they are invalid on IBMQ backends;
# timing of other instrs will still be shifted appropriately
Expand Down
1 change: 1 addition & 0 deletions qiskit/pulse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
Gaussian,
GaussianSquare,
ParametricPulse,
SymbolicPulse,
Waveform,
)
from qiskit.pulse.library.samplers.decorators import functional_pulse
Expand Down
100 changes: 86 additions & 14 deletions qiskit/pulse/library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,107 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

r"""
=====================================================
Pulse Library (waveforms :mod:`qiskit.pulse.library`)
=====================================================
"""
===========================================
Pulse Library (:mod:`qiskit.pulse.library`)
===========================================

This library provides Pulse users with convenient methods to build Pulse waveforms.

Arbitrary waveforms can be described with :py:class:`~qiskit.pulse.library.Waveform`\ s.
A pulse programmer can choose from one of several :ref:`pulse_models` such as
:class:`~Waveform` and :class:`~SymbolicPulse` to create a pulse program.
The :class:`~Waveform` model directly stores the waveform data points in each class instance.
This model provides the most flexibility to express arbitrary waveforms and allows
a rapid prototyping of new control techniques. However, this model is typically memory
inefficient and might be hard to scale to large-size quantum processors.
Several waveform subclasses are defined by :ref:`waveforms`,
but a user can also directly instantiate the :class:`~Waveform` class with ``samples`` argument
which is usually a complex numpy array or any kind of array-like data.

In contrast, the :class:`~SymbolicPulse` model only stores the function and its parameters
that generate the waveform in a class instance.
It thus provides greater memory efficiency at the price of less flexibility in the waveform.
This model also defines a small set of pulse subclasses in :ref:`symbolic_pulses`
which are commonly used in superconducting quantum processors.
An instance of these subclasses can be serialized in the :ref:`qpy_format`
while keeping the memory-efficient parametric representation of waveforms.
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
Note that :class:`~Waveform` object can be generated from an instance of
a :class:`~SymbolicPulse` which will set values for the parameters and
sample the parametric expression to create the :class:`~Waveform`.

.. note::

QPY serialization support for :class:`.SymbolicPulse` is currently not available.
This feature will be implemented soon in Qiskit terra version 0.21.


The :py:mod:`~qiskit.pulse.library.discrete` module will generate
:py:class:`~qiskit.pulse.library.Waveform`\ s for common waveform envelopes.
.. _pulse_models:

The parametric pulses, :py:class:`~qiskit.pulse.library.Gaussian`,
:py:class:`~qiskit.pulse.library.GaussianSquare`, :py:class:`~qiskit.pulse.library.Drag` and
:py:class:`~qiskit.pulse.library.Constant` will generate parameterized descriptions of
those pulses, which can greatly reduce the size of the job sent to the backend.
Pulse Models
============

.. autosummary::
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
:toctree: ../stubs/

~qiskit.pulse.library.discrete
Waveform
SymbolicPulse
ParametricPulse


.. _waveforms:

Waveform Pulse Representation
=============================

.. autosummary::
:toctree: ../stubs/

constant
zero
square
sawtooth
triangle
cos
sin
gaussian
gaussian_deriv
sech
sech_deriv
gaussian_square
drag


.. _symbolic_pulses:

Parametric Pulse Representation
===============================

.. autosummary::
:toctree: ../stubs/

Constant
Drag
Gaussian
GaussianSquare

"""
from .discrete import *
from .parametric_pulses import ParametricPulse, Gaussian, GaussianSquare, Drag, Constant

from .discrete import (
constant,
zero,
square,
sawtooth,
triangle,
cos,
sin,
gaussian,
gaussian_deriv,
sech,
sech_deriv,
gaussian_square,
drag,
)
from .parametric_pulses import ParametricPulse
from .symbolic_pulses import SymbolicPulse, Gaussian, GaussianSquare, Drag, Constant
from .pulse import Pulse
from .waveform import Waveform
29 changes: 23 additions & 6 deletions qiskit/pulse/library/parametric_pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class ParametricPulseShapes(Enum):
...
new_supported_pulse_name = library.YourPulseWaveformClass
"""
import warnings
from abc import abstractmethod
from typing import Any, Dict, Optional, Union

Expand All @@ -51,7 +52,15 @@ class ParametricPulseShapes(Enum):


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

.. warning::

This class is superseded by :class:`.SymbolicPulse` and will be deprecated
and eventually removed in the future because of the poor flexibility
for defining a new waveform type and serializing it through the :mod:`qiskit.qpy` framework.

"""

@abstractmethod
def __init__(
Expand All @@ -70,6 +79,14 @@ def __init__(
amplitude is constrained to 1.
"""
super().__init__(duration=duration, name=name, limit_amplitude=limit_amplitude)

nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
warnings.warn(
"ParametricPulse and its subclass will be deprecated and will be replaced with "
"SymbolicPulse and its subclass because of QPY serialization support. "
"See qiskit.pulse.library.symbolic_pulses for details.",
PendingDeprecationWarning,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing ParametricPulse with SymbolicPulse brings several issues to mind for me. I haven't settled on what I think is best regarding them:

  • SymbolicPulse allows for a future where a symbolic pulse expression is sent to the backend and rendered there. However, at the moment, all that is supported are the same possibilities as ParametricPulse now -- either sending the name and parameter values or falling back to the a sample pulse.
  • I wonder if there is value in merging the new symbolic features into ParametricPulse and keeping the ParametricPulse class. That might be more trouble than it's worth, though it seems like ParametricPulse subclasses need to override all the important methods any way, so they would almost already work if they inherited from SymbolicPulse instead.
  • Another thing I wonder about is if we want all pulses to be expressible symbolically or if we should allow for a pulse to be named with parameters and rendered on the backend the way it works now without a symbolic expression. We could keep ParametricPulse for this case, though I was surprised to realize only specific classes in ParametricPulseShapes are allowed to be passed to the backend. I would have expected to be able to pass a pulse name and set of parameters and as long as the pulse name was in the backend's run config for it to be okay.

What I am a little concerned about is this disconnect between fully specifying a pulse with a symbolic expression and specifying a pulse as a name and set of parameters that is implemented by the backend. Is it best to try to blur the line between those two or keep them explicitly two separate things? Serialization is not in this PR, but another thing I wonder about is if Gaussian, for example, would be serialized as a symbolic expression and deserialized as a generic symbolic pulse or if it would be serialized as a "gaussian" pulse and then deserialized back to a Gaussian.

Copy link
Contributor Author

@nkanazawa1989 nkanazawa1989 Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data format in the communication layer is one of the important aspects of the symbolic pulse, however, for us, the most important feature is the serialization of calibration database, where users may actively introduce new pulse shapes with new calibration techniques.

I tend to disagree with supporting both ParametricPulse and SymbolicPulse from viewpoint of maintenance overhead. This means we need to keep double amount of unit tests. Since we already have Schedule and ScheduleBlock, this will make situation more complicated, i.e. there are 4 different combination of data models to create a pulse program.

I would have expected to be able to pass a pulse name and set of parameters and as long as the pulse name was in the backend's run config for it to be okay.

This is why we need symbolic pulse. Indeed how this code is used is unclear now, because job submission mechanism is totally offloaded to provider. Qiskit just passes python object to the provider. If one introduces new pulse shape in the backend config, he/she should update the provider code together with parametric pulse library in Qiskit. And user need to wait for next release to enjoy backend-supported parametric pulse. I think this is why DCX (target) pulse is still provided as waveform.

I agree keeping current mechanism of name + parameters for predefined pulses. This will help us to reduce data volume of QPY binary. Regarding the serialization, I plan to write symbolic equations in the program header, instead of duplicating it for every appearance of play instruction. For example, in the circuit we have
https://github.com/Qiskit/qiskit-terra/blob/04807a13a7ba53d97c37bb092324e419296e3fba/qiskit/qpy/binary_io/circuits.py#L591-L607

If we only save equations of custom pulses, we can compactly represent the program (i.e. DRAG is very often used but constraints is quite big object). Same mechanism exists in circuit IO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one introduces new pulse shape in the backend config, he/she should update the provider code together with parametric pulse library in Qiskit. And user need to wait for next release to enjoy backend-supported parametric pulse.

The backend configuration includes a parametric_pulses entry that lists the supported parametric pulses. assemble_schedules currently gets the type of the Pulse in each Play instruction and translates it to a string with the ParametricPulseShapes enum. Then it checks if this string is in the backend parametric_pulses.

Here is a suggestion for how things could work differently to decouple the provider from terra:

  • The symbolic pulses could use the snake_case name for the pulse as pulse_type instead of the class name, so gaussian_square instead of GaussianSquare, to match the format of parametric_pulses in the provider.
  • assemble_schedules could check if pulse.pulse_type is the backend's parametric_pulses directly without doing a translation through the ParametricPulseShapes enum.

Then a provider could add support for additional pulses and the user would just need to define SymbolicPulse instances with the right pulse_type but would not need to modify terra.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was thinking to use pulse_type for this purpose but this induces a breaking API change. Because this camel case name is used everywhere, e.g. printing schedule would give

Schedule((0, Play(GaussianSquare(duration=800, amp=(0.1+0j), sigma=40, width=544), DriveChannel(0))), name="sched17")

If the symbolic pulse needs to use conventional names in parametric_pulses, then this will become

Schedule((0, Play(gaussian_square(duration=800, amp=(0.1+0j), sigma=40, width=544), DriveChannel(0))), name="sched17")

which may confuse the users. This also changes labels in the pulse visualizer and repr of pulse instances. Also we still need this Enum to convert backend's CmdDef back to the schedules (i.e. construction of instmap) because we need to extract envelope expressions from somewhere in the pulse module unless the backend replaces CmdDef with QPY schedules. I agree this approach is still effective to simplify the logic of schedule -> Qobj.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so what is a path forward for not requiring pulses to be hardcoded in terra for them to be passed parametrically to the backend? Wait for a backend and provider that supports QPY jobs and arbitrary symbolic pulses?

For backwards compatibility we could store GaussianSquare and gaussian_square separately in SymbolicPulse and use one for display and one for checking for backend support.

I don't see a way to avoid keeping the enum defined for now for the instruction schedule map usage. Perhaps we could get backends to start publishing the CmdDef as SymbolicPulses in the future.

stacklevel=3,
)
self.validate_parameters()

@abstractmethod
Expand Down Expand Up @@ -155,7 +172,7 @@ def get_waveform(self) -> Waveform:
return gaussian(duration=self.duration, amp=self.amp, sigma=self.sigma, zero_ends=True)

def validate_parameters(self) -> None:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self.limit_amplitude:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self._limit_amplitude:
raise PulseError(
f"The amplitude norm must be <= 1, found: {abs(self.amp)}"
+ "This can be overruled by setting Pulse.limit_amplitude."
Expand Down Expand Up @@ -277,7 +294,7 @@ def get_waveform(self) -> Waveform:
)

def validate_parameters(self) -> None:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self.limit_amplitude:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self._limit_amplitude:
raise PulseError(
f"The amplitude norm must be <= 1, found: {abs(self.amp)}"
+ "This can be overruled by setting Pulse.limit_amplitude."
Expand Down Expand Up @@ -426,7 +443,7 @@ def get_waveform(self) -> Waveform:
)

def validate_parameters(self) -> None:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self.limit_amplitude:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self._limit_amplitude:
raise PulseError(
f"The amplitude norm must be <= 1, found: {abs(self.amp)}"
+ "This can be overruled by setting Pulse.limit_amplitude."
Expand All @@ -440,7 +457,7 @@ def validate_parameters(self) -> None:
not _is_parameterized(self.beta)
and not _is_parameterized(self.sigma)
and np.abs(self.beta) > self.sigma
and self.limit_amplitude
and self._limit_amplitude
):
# If beta <= sigma, then the maximum amplitude is at duration / 2, which is
# already constrained by self.amp <= 1
Expand Down Expand Up @@ -523,7 +540,7 @@ def get_waveform(self) -> Waveform:
return constant(duration=self.duration, amp=self.amp)

def validate_parameters(self) -> None:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self.limit_amplitude:
if not _is_parameterized(self.amp) and abs(self.amp) > 1.0 and self._limit_amplitude:
raise PulseError(
f"The amplitude norm must be <= 1, found: {abs(self.amp)}"
+ "This can be overruled by setting Pulse.limit_amplitude."
Expand Down
5 changes: 3 additions & 2 deletions qiskit/pulse/library/pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Pulse(ABC):
modulation phase and frequency are specified separately from ``Pulse``s.
"""

__slots__ = ("duration", "name", "_limit_amplitude")

limit_amplitude = True

@abstractmethod
Expand All @@ -46,8 +48,7 @@ def __init__(

self.duration = duration
self.name = name
if limit_amplitude is not None:
self.limit_amplitude = limit_amplitude
self._limit_amplitude = limit_amplitude or self.__class__.limit_amplitude

@property
def id(self) -> int: # pylint: disable=invalid-name
Expand Down
Loading