-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Remove symbolic pulse subclass implementation #8278
Changes from 1 commit
d6f0a81
a506e19
795fd79
662f3a9
e1794f2
8d43a38
3edfd02
8104e25
50469e6
4485702
ffa9666
382957d
051553e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
""" | ||
|
||
import functools | ||
import warnings | ||
from typing import Any, Dict, List, Optional, Union, Callable | ||
|
||
import numpy as np | ||
|
@@ -579,7 +580,42 @@ def __repr__(self) -> str: | |
) | ||
|
||
|
||
class Gaussian(SymbolicPulse): | ||
class _PulseType(type): | ||
"""Metaclass to warn at isinstance check.""" | ||
|
||
def __instancecheck__(cls, instance): | ||
try: | ||
cls_alias = getattr(cls, "alias") | ||
except AttributeError: | ||
return NotImplemented | ||
|
||
# TODO promote this to Deprecation warning in future. | ||
# Once type information usage is removed from user code, | ||
# we will convert pulse classes into functions. | ||
warnings.warn( | ||
"Typechecking with the symbolic pulse subclass will be deprecated. " | ||
f"'{cls_alias}' subclass instance is turned into SymbolicPulse instance. " | ||
f"Use self.pulse_type == '{cls_alias}' instead.", | ||
UserWarning, | ||
nkanazawa1989 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
if not isinstance(instance, SymbolicPulse): | ||
return False | ||
return instance.pulse_type == cls_alias | ||
|
||
def __getattr__(cls, item): | ||
# For pylint. A SymbolicPulse subclass must implement several methods | ||
# such as .get_waveform and .validate_parameters. | ||
# In addition, they conventionally offer attribute-like access to the pulse parameters, | ||
# for example, instance.amp returns instance._params["amp"]. | ||
# If pulse classes are directly instantiated, pylint yells no-member | ||
# since the pulse class itself implements nothing. These classes just | ||
# behave like a factory by internally instantiating the SymbolicPulse and return it. | ||
# It is not realistic to write disable=no-member across qiskit packages. | ||
return NotImplemented | ||
Comment on lines
+598
to
+607
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Super hacky, but it sounds like the right call if you need |
||
|
||
|
||
class Gaussian(metaclass=_PulseType): | ||
r"""A lifted and truncated pulse envelope shaped according to the Gaussian function whose | ||
mean is centered at the center of the pulse (duration / 2): | ||
|
||
|
@@ -591,14 +627,16 @@ class Gaussian(SymbolicPulse): | |
where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
alias = "Gaussian" | ||
|
||
def __new__( | ||
cls, | ||
duration: Union[int, ParameterExpression], | ||
amp: Union[complex, ParameterExpression], | ||
sigma: Union[float, ParameterExpression], | ||
name: Optional[str] = None, | ||
limit_amplitude: Optional[bool] = None, | ||
): | ||
) -> SymbolicPulse: | ||
"""Create new pulse instance. | ||
|
||
Args: | ||
|
@@ -610,6 +648,8 @@ def __init__( | |
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. | ||
""" | ||
parameters = {"amp": amp, "sigma": sigma} | ||
|
||
|
@@ -621,8 +661,8 @@ def __init__( | |
consts_expr = _sigma > 0 | ||
valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 | ||
|
||
super().__init__( | ||
pulse_type=self.__class__.__name__, | ||
instance = SymbolicPulse( | ||
pulse_type=cls.alias, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just checking: does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. It could have mathematical meaning, because a symbolic pulse instance is tied to a particular symbolic expression, and we implicitly assumes unique mapping to The motivation of this PR is to address round-trip serialization issue. The important point here is all pulse classes are not necessary defined in Qiskit. For example, in research code, end-user can define new pulse shape in parametric form, and they may choose not to expose it to Qiskit before publication. On the other hand, they may want to share the pulse with colleagues. Perhaps you know better idea :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that an end-user is not necessary a single Qiskit user. For example, IBM backend may have several pulses, and experiment library should be able to manage these resources without modifying QPY loader for different libraries. Another option would be upgrade QPY to take Encoder/Decoder like JSON. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pulse names have a special meaning because the backend can support a set of pulses parametrically for which only the name and parameters need to be provided. For IBM backends, these names are snakecase versions of the old pulse classes (Gaussian <-> gaussian). Some of the awkwardness of Ideally, one day the backend can accept a symbolic expression directly in the pulse data instead of just the name of a supported pulse and then we could send that expression and the names would not matter. It would be nice if we could move the backend names into the SymbolicPulse data and drop the special-case ParametricPulseShapes translation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ugh, in current framework
I'm bit against to this direction because this ties a particular backend to user program. Probably we can introduce an internal pulse class so that we can set backend name reported by backends in the transform logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't understand the concern here. You don't like including something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Because the Qobj converter is standalone mechanism from assemble, and provider can give their pulse name through a custom converter. If we tie this to pulse class itself this is kind of enforcing the serialization rule to all provider and programs are no longer backend agnostic. I think Gaussian Square is kind of IBM convention, e.g. I more often see Gaussian flat top in literatures. |
||
duration=duration, | ||
parameters=parameters, | ||
name=name, | ||
|
@@ -631,10 +671,12 @@ def __init__( | |
constraints=consts_expr, | ||
valid_amp_conditions=valid_amp_conditions_expr, | ||
) | ||
self.validate_parameters() | ||
instance.validate_parameters() | ||
|
||
return instance | ||
|
||
class GaussianSquare(SymbolicPulse): | ||
|
||
class GaussianSquare(metaclass=_PulseType): | ||
"""A square pulse with a Gaussian shaped risefall on both sides lifted such that | ||
its first sample is zero. | ||
|
||
|
@@ -672,16 +714,18 @@ class GaussianSquare(SymbolicPulse): | |
where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
alias = "GaussianSquare" | ||
|
||
def __new__( | ||
cls, | ||
duration: Union[int, ParameterExpression], | ||
amp: Union[complex, ParameterExpression], | ||
sigma: Union[float, ParameterExpression], | ||
width: Optional[Union[float, ParameterExpression]] = None, | ||
risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None, | ||
name: Optional[str] = None, | ||
limit_amplitude: Optional[bool] = None, | ||
): | ||
) -> SymbolicPulse: | ||
"""Create new pulse instance. | ||
|
||
Args: | ||
|
@@ -695,6 +739,9 @@ def __init__( | |
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. | ||
""" | ||
|
@@ -729,8 +776,8 @@ def __init__( | |
consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) | ||
valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 | ||
|
||
super().__init__( | ||
pulse_type=self.__class__.__name__, | ||
instance = SymbolicPulse( | ||
pulse_type=cls.alias, | ||
duration=duration, | ||
parameters=parameters, | ||
name=name, | ||
|
@@ -739,15 +786,12 @@ def __init__( | |
constraints=consts_expr, | ||
valid_amp_conditions=valid_amp_conditions_expr, | ||
) | ||
self.validate_parameters() | ||
instance.validate_parameters() | ||
|
||
@property | ||
def risefall_sigma_ratio(self): | ||
"""Return risefall_sigma_ratio. This is auxiliary parameter to define width.""" | ||
return (self.duration - self.width) / (2.0 * self.sigma) | ||
return instance | ||
|
||
|
||
class Drag(SymbolicPulse): | ||
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. | ||
|
||
|
@@ -784,15 +828,17 @@ class Drag(SymbolicPulse): | |
Phys. Rev. Lett. 103, 110501 – Published 8 September 2009.* | ||
""" | ||
|
||
def __init__( | ||
self, | ||
alias = "Drag" | ||
|
||
def __new__( | ||
cls, | ||
duration: Union[int, ParameterExpression], | ||
amp: Union[complex, ParameterExpression], | ||
sigma: Union[float, ParameterExpression], | ||
beta: Union[float, ParameterExpression], | ||
name: Optional[str] = None, | ||
limit_amplitude: Optional[bool] = None, | ||
): | ||
) -> SymbolicPulse: | ||
"""Create new pulse instance. | ||
|
||
Args: | ||
|
@@ -804,6 +850,9 @@ def __init__( | |
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. | ||
""" | ||
parameters = {"amp": amp, "sigma": sigma, "beta": beta} | ||
|
||
|
@@ -819,8 +868,8 @@ def __init__( | |
consts_expr = _sigma > 0 | ||
valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) | ||
|
||
super().__init__( | ||
pulse_type=self.__class__.__name__, | ||
instance = SymbolicPulse( | ||
pulse_type="Drag", | ||
duration=duration, | ||
parameters=parameters, | ||
name=name, | ||
|
@@ -829,10 +878,12 @@ def __init__( | |
constraints=consts_expr, | ||
valid_amp_conditions=valid_amp_conditions_expr, | ||
) | ||
self.validate_parameters() | ||
instance.validate_parameters() | ||
|
||
return instance | ||
|
||
|
||
class Constant(SymbolicPulse): | ||
class Constant(metaclass=_PulseType): | ||
"""A simple constant pulse, with an amplitude value and a duration: | ||
|
||
.. math:: | ||
|
@@ -841,13 +892,15 @@ class Constant(SymbolicPulse): | |
f(x) = 0 , elsewhere | ||
""" | ||
|
||
def __init__( | ||
self, | ||
alias = "Constant" | ||
|
||
def __new__( | ||
cls, | ||
duration: Union[int, ParameterExpression], | ||
amp: Union[complex, ParameterExpression], | ||
name: Optional[str] = None, | ||
limit_amplitude: Optional[bool] = None, | ||
): | ||
) -> SymbolicPulse: | ||
"""Create new pulse instance. | ||
|
||
Args: | ||
|
@@ -856,6 +909,9 @@ def __init__( | |
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. | ||
""" | ||
parameters = {"amp": amp} | ||
|
||
|
@@ -872,13 +928,15 @@ def __init__( | |
envelope_expr = _amp * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True)) | ||
valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 | ||
|
||
super().__init__( | ||
pulse_type=self.__class__.__name__, | ||
instance = SymbolicPulse( | ||
pulse_type="Constant", | ||
duration=duration, | ||
parameters=parameters, | ||
name=name, | ||
limit_amplitude=limit_amplitude, | ||
envelope=envelope_expr, | ||
valid_amp_conditions=valid_amp_conditions_expr, | ||
) | ||
self.validate_parameters() | ||
instance.validate_parameters() | ||
|
||
return instance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this return
NotImplemented
instead ofFalse
? I didn't see anything aboutNotImplemented
in https://peps.python.org/pep-3119/.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I changed the logic. The point of this metaclass is to raise UserWarning to tell users subclasses are removed. So this should not return before the warning.