Skip to content

Commit

Permalink
Serializable parametric pulse (#7821)
Browse files Browse the repository at this point in the history
* Symbolic parametric pulse

This PR makes following changes.

1. add `_define` method to ParametricPulse.

This method is expected to be implemented by each ParametricPulse subclass. Subclass must return Sympy or symengine symbolic equation that can be serialized. This method is called once in the constructor to fill `.definition` attribute of the instance. This definition is used for QPY serialization.

2. change behavior of `get_waveform`

This method is originally implemented as abstractmethod that calls another callback that generates numpy array of samples (thus not serializable). Now this is a baseclass method that generates waveform array from the symbolic equation.

3. minor updates

Now pulse parameters are not direct class attribute. Parameter names are defined as class attribute `PARAMS_DEF` and values are stored as instance variable `param_values`. This is good for writing serializer since it doesn't need to know attribute of each subclass, but just can call name def and values to get data to serialize.

* Improve performance of parametrized pulse evaluation

* Implement Constant pulse using Piecewise

This works around an issue with sympy where its lambda returns a scalar
instead of an array when the expression evaluates to a scalar:

sympy/sympy#5642

* Fall back to sympy for parametric pulse evaluation

* Use sympy Symbol with sympy lambdify

* WIP: cache symbolic pulse lambda functions

* move lambdify to init subclass for called once

* turn parameter properties into attribute and remove slots

* remove attribute and add __getattr__ and readd slots for better performance.

* move symbolic funcs directly to _define

* Convert numerical_func to staticmethod

Co-authored-by: Will Shanks <[email protected]>

* remove symbolic add constraints, numerical func is renamed to definition for better integration with circuit instruction

* revert changes to parametric pulse and create new symbolic pulse file

* add ITE-like program

* sympy runtime import

* update test notebook

* add attribute docs

* fix non-constraints

* remove pulse type instance variable

* use descriptor

* remove redundant comment

* Update
- Add symbolic pulse to assemble
- Add symbplic pulse to parameter manager
- Remove risefall ratio from GaussianSquare paramters

* keep raw data for QPY encoding

* update unittest

* update helper function name

* add reno

* remove notebook

* fix lint

* fix unittest and logic

* add more docs

* review comments

* lint fix

* fix documentation

* minor drawer fix

* documentation upgrade

Co-authored-by: Daniel J. Egger <[email protected]>

* review comment misc

Co-authored-by: Will Shanks <[email protected]>

* remove abstract class methods

This commit removes class methods for symbolic expression so that SymbolicPulse can be instantiated by itself. And descriptors only saves Dict[Expr, Callable], i.e. lambda cache, rather than enforcing name-based mapping.

* add error handling for amplitude

* treat amp as a special parameter

* Remove expressions for amplitude validation

* support symengine
- symengine doesn't support lamdify of complex expression, e.g. DRAG pulse
- symengine doesn't support Less expression with complex value substitution
- symengine Lambda function doesn't support args in mixture of complex and float object

To overcome these, envelope expressions are separately stored for I, Q channel, and evaluation of 'amp' is excluded from symbolic operation. Thus in this implementation symbols are all real numbers.

* use real=False option

* review comment

Co-authored-by: Will Shanks <[email protected]>

* - fix attribute
- duration and amp are required
- instantiate subclass with expression
- remove __dict__ usage in __getattr__ and readd __slots__
- fix documentation
- update private attirbute names

* undo change to requirements-dev

* fix __getattr__ mechanism

Directly accessing to the instance variable crashes copy and deepcopy because when the object is copied the __init__ is not called before the first getattr is called. Then copied instance tries to find missing (not initialized) attribute in recursive way. use of __getattribute__ fixes this problem.

* fix type hint reference

* simplification

* move amp from constructor to `parameters` dict

* review comment

Co-authored-by: Will Shanks <[email protected]>

* fix bug

- use getattr in __eq__; e.g. Waveform doesn't have envelope
- fix parameter maanger

in addition, this replaces _param_vals and _param_names tuple with _params dictionary because overhead of generating a dict is significant.

* fix typo

* add eval_conditions to skip waveform generation

* fall back to sympy lamdify when function is not supported

* documentation update

Co-authored-by: Will Shanks <[email protected]>
Co-authored-by: Daniel Egger <[email protected]>

* replace eval_conditions with valid_amp_conditions

Co-authored-by: Will Shanks <[email protected]>

* update hashing and equality, redefine expressions more immutably

* add error message for missing parameter

* cleanup

* check parameter before hashing

* move amp check to constructor

* add envelope to hash

* update docs

* Update qiskit/pulse/library/symbolic_pulses.py

Co-authored-by: Matthew Treinish <[email protected]>

* lint

Co-authored-by: Will Shanks <[email protected]>
Co-authored-by: Daniel J. Egger <[email protected]>
Co-authored-by: Matthew Treinish <[email protected]>
  • Loading branch information
4 people authored Jun 15, 2022
1 parent a0b9a84 commit 1e872b7
Show file tree
Hide file tree
Showing 17 changed files with 1,218 additions and 92 deletions.
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.
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::
: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)

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,
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 @@ -287,7 +304,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 @@ -431,7 +448,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 @@ -445,7 +462,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 @@ -528,7 +545,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

0 comments on commit 1e872b7

Please sign in to comment.