Skip to content

Commit

Permalink
Converting the pulse library from complex amp to amp+angle (#9002)
Browse files Browse the repository at this point in the history
* Converting Gaussian SymbolicPulse from complex amp to amp,angle.

* removed unnecessary import.

* Completed the changes.

* Bug fix and test updates.

* removed commented line.

* black correction.

* Tests correction.

* Bump QPY version, and adjust QPY loader.

* Release Notes.

* Update qiskit/qobj/converters/pulse_instruction.py

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

* Update releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml

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

* Some more corrections.

* QPY load adjustment.

* Removed debug print

* Always add "angle" to envelope

* black

* Update qiskit/qpy/__init__.py

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

* resolve conflict

* Remove outdated test.

* Lint

* Release notes style

* Removed QPY version bump in favor of using qiskit terra version as an indicator.

* bug fix

* bug fix

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Nov 30, 2022
1 parent 0785ab3 commit 4342881
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 126 deletions.
163 changes: 127 additions & 36 deletions qiskit/pulse/library/symbolic_pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,13 +429,6 @@ def __init__(
if parameters is None:
parameters = {}

# TODO remove this.
# This is due to convention in IBM Quantum backends where "amp" is treated as a
# special parameter that must be defined in the form [real, imaginary].
# this check must be removed because Qiskit pulse should be backend agnostic.
if "amp" in parameters and not isinstance(parameters["amp"], ParameterExpression):
parameters["amp"] = complex(parameters["amp"])

self._pulse_type = pulse_type
self._params = parameters

Expand Down Expand Up @@ -614,42 +607,66 @@ class Gaussian(metaclass=_PulseType):
.. math::
f'(x) &= \exp\Bigl( -\frac12 \frac{{(x - \text{duration}/2)}^2}{\text{sigma}^2} \Bigr)\\
f(x) &= \text{amp} \times \frac{f'(x) - f'(-1)}{1-f'(-1)}, \quad 0 \le x < \text{duration}
f(x) &= \text{A} \times \frac{f'(x) - f'(-1)}{1-f'(-1)}, \quad 0 \le x < \text{duration}
where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling.
where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling, and
:math:`\text{A} = \text{amp} \times \exp\left(i\times\text{angle}\right)`.
"""

alias = "Gaussian"

def __new__(
cls,
duration: Union[int, ParameterExpression],
amp: Union[complex, ParameterExpression],
amp: Union[complex, float, ParameterExpression],
sigma: Union[float, ParameterExpression],
angle: Optional[Union[float, ParameterExpression]] = None,
name: Optional[str] = None,
limit_amplitude: Optional[bool] = None,
) -> SymbolicPulse:
"""Create new pulse instance.
Args:
duration: Pulse length in terms of the sampling period `dt`.
amp: The amplitude of the Gaussian envelope.
amp: The magnitude of the amplitude of the Gaussian envelope.
Complex amp support will be deprecated.
sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically
in the class docstring.
angle: The angle of the complex amplitude of the Gaussian envelope. Default value 0.
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.
Raises:
PulseError: If both complex amp and angle are provided as arguments.
"""
parameters = {"amp": amp, "sigma": sigma}
# This should be removed once complex amp support is deprecated.
if isinstance(amp, complex):
if angle is None:
warnings.warn(
"Complex amp will be deprecated. "
"Use float amp (for the magnitude) and float angle instead.",
PendingDeprecationWarning,
)
else:
raise PulseError("amp can't be complex when providing angle")

if angle is None:
angle = 0

parameters = {"amp": amp, "sigma": sigma, "angle": angle}

# Prepare symbolic expressions
_t, _duration, _amp, _sigma = sym.symbols("t, duration, amp, sigma")
_t, _duration, _amp, _sigma, _angle = sym.symbols("t, duration, amp, sigma, angle")
_center = _duration / 2

envelope_expr = _amp * _lifted_gaussian(_t, _center, _duration + 1, _sigma)
envelope_expr = (
_amp * sym.exp(sym.I * _angle) * _lifted_gaussian(_t, _center, _duration + 1, _sigma)
)

consts_expr = _sigma > 0
valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0

Expand Down Expand Up @@ -700,20 +717,22 @@ class GaussianSquare(metaclass=_PulseType):
\\biggr)\
& \\text{risefall} + \\text{width} \\le x\
\\end{cases}\\\\
f(x) &= \\text{amp} \\times \\frac{f'(x) - f'(-1)}{1-f'(-1)},\
f(x) &= \\text{A} \\times \\frac{f'(x) - f'(-1)}{1-f'(-1)},\
\\quad 0 \\le x < \\text{duration}
where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling.
where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling, and
:math:`\\text{A} = \\text{amp} \\times \\exp\\left(i\\times\\text{angle}\\right)`.
"""

alias = "GaussianSquare"

def __new__(
cls,
duration: Union[int, ParameterExpression],
amp: Union[complex, ParameterExpression],
amp: Union[complex, float, ParameterExpression],
sigma: Union[float, ParameterExpression],
width: Optional[Union[float, ParameterExpression]] = None,
angle: Optional[Union[float, ParameterExpression]] = None,
risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None,
name: Optional[str] = None,
limit_amplitude: Optional[bool] = None,
Expand All @@ -722,10 +741,12 @@ def __new__(
Args:
duration: Pulse length in terms of the sampling period `dt`.
amp: The amplitude of the Gaussian and of the square pulse.
amp: The magnitude of the amplitude of the Gaussian and square pulse.
Complex amp support will be deprecated.
sigma: A measure of how wide or narrow the Gaussian risefall is; see the class
docstring for more details.
width: The duration of the embedded square pulse.
angle: The angle of the complex amplitude of the 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
Expand All @@ -736,6 +757,7 @@ def __new__(
Raises:
PulseError: When width and risefall_sigma_ratio are both empty or both non-empty.
PulseError: If both complex amp and angle are provided as arguments.
"""
# Convert risefall_sigma_ratio into width which is defined in OpenPulse spec
if width is None and risefall_sigma_ratio is None:
Expand All @@ -750,10 +772,26 @@ def __new__(
if width is None and risefall_sigma_ratio is not None:
width = duration - 2.0 * risefall_sigma_ratio * sigma

parameters = {"amp": amp, "sigma": sigma, "width": width}
# This should be removed once complex amp support is deprecated.
if isinstance(amp, complex):
if angle is None:
warnings.warn(
"Complex amp will be deprecated. "
"Use float amp (for the magnitude) and float angle instead.",
PendingDeprecationWarning,
)
else:
raise PulseError("amp can't be complex when providing angle")

if angle is None:
angle = 0

parameters = {"amp": amp, "sigma": sigma, "width": width, "angle": angle}

# Prepare symbolic expressions
_t, _duration, _amp, _sigma, _width = sym.symbols("t, duration, amp, sigma, width")
_t, _duration, _amp, _sigma, _width, _angle = sym.symbols(
"t, duration, amp, sigma, width, angle"
)
_center = _duration / 2

_sq_t0 = _center - _width / 2
Expand All @@ -762,9 +800,14 @@ def __new__(
_gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma)
_gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration + 1, _sigma)

envelope_expr = _amp * sym.Piecewise(
(_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True)
envelope_expr = (
_amp
* sym.exp(sym.I * _angle)
* sym.Piecewise(
(_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True)
)
)

consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width)
valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0

Expand Down Expand Up @@ -795,13 +838,14 @@ class Drag(metaclass=_PulseType):
.. math::
g(x) &= \\exp\\Bigl(-\\frac12 \\frac{(x - \\text{duration}/2)^2}{\\text{sigma}^2}\\Bigr)\\\\
g'(x) &= \\text{amp}\\times\\frac{g(x)-g(-1)}{1-g(-1)}\\\\
g'(x) &= \\text{A}\\times\\frac{g(x)-g(-1)}{1-g(-1)}\\\\
f(x) &= g'(x) \\times \\Bigl(1 + 1j \\times \\text{beta} \\times\
\\Bigl(-\\frac{x - \\text{duration}/2}{\\text{sigma}^2}\\Bigr) \\Bigr),
\\quad 0 \\le x < \\text{duration}
where :math:`g(x)` is a standard unlifted Gaussian waveform and
:math:`g'(x)` is the lifted :class:`~qiskit.pulse.library.Gaussian` waveform.
where :math:`g(x)` is a standard unlifted Gaussian waveform, :math:`g'(x)` is the lifted
:class:`~qiskit.pulse.library.Gaussian` waveform, and
:math:`\\text{A} = \\text{amp} \\times \\exp\\left(i\\times\\text{angle}\\right)`.
References:
1. |citation1|_
Expand All @@ -825,37 +869,59 @@ class Drag(metaclass=_PulseType):
def __new__(
cls,
duration: Union[int, ParameterExpression],
amp: Union[complex, ParameterExpression],
amp: Union[complex, float, ParameterExpression],
sigma: Union[float, ParameterExpression],
beta: Union[float, ParameterExpression],
angle: Optional[Union[float, ParameterExpression]] = None,
name: Optional[str] = None,
limit_amplitude: Optional[bool] = None,
) -> SymbolicPulse:
"""Create new pulse instance.
Args:
duration: Pulse length in terms of the sampling period `dt`.
amp: The amplitude of the Drag envelope.
amp: The magnitude of the amplitude of the DRAG envelope.
Complex amp support will be deprecated.
sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically
in the class docstring.
beta: The correction amplitude.
angle: The angle of the complex amplitude of the DRAG envelope. Default value 0.
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.
Raises:
PulseError: If both complex amp and angle are provided as arguments.
"""
parameters = {"amp": amp, "sigma": sigma, "beta": beta}
# This should be removed once complex amp support is deprecated.
if isinstance(amp, complex):
if angle is None:
warnings.warn(
"Complex amp will be deprecated. "
"Use float amp (for the magnitude) and float angle instead.",
PendingDeprecationWarning,
)
else:
raise PulseError("amp can't be complex when providing angle")

if angle is None:
angle = 0

parameters = {"amp": amp, "sigma": sigma, "beta": beta, "angle": angle}

# Prepare symbolic expressions
_t, _duration, _amp, _sigma, _beta = sym.symbols("t, duration, amp, sigma, beta")
_t, _duration, _amp, _sigma, _beta, _angle = sym.symbols(
"t, duration, amp, sigma, beta, angle"
)
_center = _duration / 2

_gauss = _lifted_gaussian(_t, _center, _duration + 1, _sigma)
_deriv = -(_t - _center) / (_sigma**2) * _gauss

envelope_expr = _amp * (_gauss + sym.I * _beta * _deriv)
envelope_expr = _amp * sym.exp(sym.I * _angle) * (_gauss + sym.I * _beta * _deriv)

consts_expr = _sigma > 0
valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma)
Expand All @@ -880,7 +946,7 @@ class Constant(metaclass=_PulseType):
.. math::
f(x) = amp , 0 <= x < duration
f(x) = \\text{amp}\\times\\exp\\left(i\\text{angle}\\right) , 0 <= x < duration
f(x) = 0 , elsewhere
"""

Expand All @@ -889,26 +955,46 @@ class Constant(metaclass=_PulseType):
def __new__(
cls,
duration: Union[int, ParameterExpression],
amp: Union[complex, ParameterExpression],
amp: Union[complex, float, ParameterExpression],
angle: Optional[Union[float, ParameterExpression]] = None,
name: Optional[str] = None,
limit_amplitude: Optional[bool] = None,
) -> SymbolicPulse:
"""Create new pulse instance.
Args:
duration: Pulse length in terms of the sampling period `dt`.
amp: The amplitude of the constant square pulse.
amp: The magnitude of the amplitude of the square envelope.
Complex amp support will be deprecated.
angle: The angle of the complex amplitude of the square envelope. Default value 0.
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.
Raises:
PulseError: If both complex amp and angle are provided as arguments.
"""
parameters = {"amp": amp}
# This should be removed once complex amp support is deprecated.
if isinstance(amp, complex):
if angle is None:
warnings.warn(
"Complex amp will be deprecated. "
"Use float amp (for the magnitude) and float angle instead.",
PendingDeprecationWarning,
)
else:
raise PulseError("amp can't be complex when providing angle")

if angle is None:
angle = 0

parameters = {"amp": amp, "angle": angle}

# Prepare symbolic expressions
_t, _amp, _duration = sym.symbols("t, amp, duration")
_t, _amp, _duration, _angle = sym.symbols("t, amp, duration, angle")

# Note this is implemented using Piecewise instead of just returning amp
# directly because otherwise the expression has no t dependence and sympy's
Expand All @@ -917,7 +1003,12 @@ def __new__(
# ParametricPulse.get_waveform().
#
# See: https://github.com/sympy/sympy/issues/5642
envelope_expr = _amp * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True))
envelope_expr = (
_amp
* sym.exp(sym.I * _angle)
* sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True))
)

valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0

instance = SymbolicPulse(
Expand Down
5 changes: 0 additions & 5 deletions qiskit/pulse/parameter_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,6 @@ def visit_SymbolicPulse(self, node: SymbolicPulse):
pval = node._params[name]
if isinstance(pval, ParameterExpression):
new_val = self._assign_parameter_expression(pval)
if name == "amp" and not isinstance(new_val, ParameterExpression):
# This is due to an odd behavior of IBM Quantum backends.
# When the amplitude is given as a float, then job execution is
# terminated with an error.
new_val = complex(new_val)
node._params[name] = new_val
node.validate_parameters()

Expand Down
17 changes: 13 additions & 4 deletions qiskit/qobj/converters/pulse_instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from enum import Enum
from typing import Union
import numpy as np

from qiskit.pulse import channels, instructions, library
from qiskit.pulse.configuration import Kernel, Discriminator
Expand Down Expand Up @@ -425,12 +426,18 @@ def convert_play(self, shift, instruction):
dict: Dictionary of required parameters.
"""
if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)):
params = dict(instruction.pulse.parameters)
# IBM backends expect "amp" to be the complex amplitude
if "amp" in params and "angle" in params:
params["amp"] = complex(params["amp"] * np.exp(1j * params["angle"]))
del params["angle"]

command_dict = {
"name": "parametric_pulse",
"pulse_shape": ParametricPulseShapes.from_instance(instruction.pulse).name,
"t0": shift + instruction.start_time,
"ch": instruction.channel.name,
"parameters": instruction.pulse.parameters,
"parameters": params,
}
else:
command_dict = {
Expand Down Expand Up @@ -723,10 +730,12 @@ def convert_parametric(self, instruction):
)
short_pulse_id = hashlib.md5(base_str.encode("utf-8")).hexdigest()[:4]
pulse_name = f"{instruction.pulse_shape}_{short_pulse_id}"
params = dict(instruction.parameters)
if "amp" in params and isinstance(params["amp"], complex):
params["angle"] = np.angle(params["amp"])
params["amp"] = np.abs(params["amp"])

pulse = ParametricPulseShapes.to_type(instruction.pulse_shape)(
**instruction.parameters, name=pulse_name
)
pulse = ParametricPulseShapes.to_type(instruction.pulse_shape)(**params, name=pulse_name)
return instructions.Play(pulse, channel) << t0

@bind_name("snapshot")
Expand Down
Loading

0 comments on commit 4342881

Please sign in to comment.