Converting the pulse library from complex amp to amp+angle (#9002)

* 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>
This commit is contained in:
TsafrirA 2022-11-30 18:27:52 +02:00 committed by GitHub
parent 0785ab3131
commit 4342881e19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 289 additions and 126 deletions

View File

@ -429,13 +429,6 @@ class SymbolicPulse(Pulse):
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
@ -614,9 +607,10 @@ 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"
@ -624,8 +618,9 @@ class Gaussian(metaclass=_PulseType):
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:
@ -633,23 +628,45 @@ class Gaussian(metaclass=_PulseType):
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
@ -700,10 +717,11 @@ 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"
@ -711,9 +729,10 @@ class GaussianSquare(metaclass=_PulseType):
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,
@ -722,10 +741,12 @@ class GaussianSquare(metaclass=_PulseType):
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
@ -736,6 +757,7 @@ class GaussianSquare(metaclass=_PulseType):
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:
@ -750,10 +772,26 @@ class GaussianSquare(metaclass=_PulseType):
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
@ -762,9 +800,14 @@ class GaussianSquare(metaclass=_PulseType):
_gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma)
_gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration + 1, _sigma)
envelope_expr = _amp * sym.Piecewise(
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
@ -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|_
@ -825,9 +869,10 @@ 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:
@ -835,27 +880,48 @@ class Drag(metaclass=_PulseType):
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)
@ -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
"""
@ -889,7 +955,8 @@ 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:
@ -897,18 +964,37 @@ class Constant(metaclass=_PulseType):
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
@ -917,7 +1003,12 @@ class Constant(metaclass=_PulseType):
# 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(

View File

@ -231,11 +231,6 @@ class ParameterSetter(NodeVisitor):
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()

View File

@ -20,6 +20,7 @@ import warnings
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
@ -425,12 +426,18 @@ class InstructionToQobjConverter:
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 = {
@ -723,10 +730,12 @@ class QobjToInstructionConverter:
)
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")

View File

@ -434,7 +434,7 @@ def _read_custom_operations(file_obj, version, vectors):
return custom_operations
def _read_calibrations(file_obj, version, vectors, metadata_deserializer):
def _read_calibrations(file_obj, version, vectors, metadata_deserializer, qiskit_version=None):
calibrations = {}
header = formats.CALIBRATION._make(
@ -452,7 +452,9 @@ def _read_calibrations(file_obj, version, vectors, metadata_deserializer):
params = tuple(
value.read_value(file_obj, version, vectors) for _ in range(defheader.num_params)
)
schedule = schedules.read_schedule_block(file_obj, version, metadata_deserializer)
schedule = schedules.read_schedule_block(
file_obj, version, metadata_deserializer, qiskit_version=qiskit_version
)
if name not in calibrations:
calibrations[name] = {(qubits, params): schedule}
@ -811,7 +813,7 @@ def write_circuit(file_obj, circuit, metadata_serializer=None):
_write_calibrations(file_obj, circuit.calibrations, metadata_serializer)
def read_circuit(file_obj, version, metadata_deserializer=None):
def read_circuit(file_obj, version, metadata_deserializer=None, qiskit_version=None):
"""Read a single QuantumCircuit object from the file like object.
Args:
@ -824,6 +826,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
in the file-like object. If this is not specified the circuit metadata will
be parsed as JSON with the stdlib ``json.load()`` function using
the default ``JSONDecoder`` class.
qiskit_version (tuple): tuple with major, minor and patch versions of qiskit.
Returns:
QuantumCircuit: The circuit object from the file.
@ -874,7 +877,9 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
# Read calibrations
if version >= 5:
circ.calibrations = _read_calibrations(file_obj, version, vectors, metadata_deserializer)
circ.calibrations = _read_calibrations(
file_obj, version, vectors, metadata_deserializer, qiskit_version=qiskit_version
)
for vec_name, (vector, initialized_params) in vectors.items():
if len(initialized_params) != len(vector):

View File

@ -16,6 +16,7 @@
import json
import struct
import zlib
import warnings
import numpy as np
@ -26,6 +27,11 @@ from qiskit.qpy import formats, common, type_keys
from qiskit.qpy.binary_io import value
from qiskit.utils import optionals as _optional
if _optional.HAS_SYMENGINE:
import symengine as sym
else:
import sympy as sym
def _read_channel(file_obj, version):
type_key = common.read_type_key(file_obj)
@ -71,7 +77,38 @@ def _loads_symbolic_expr(expr_bytes):
return expr
def _read_symbolic_pulse(file_obj, version):
def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters):
# In the transition to Qiskit Terra 0.23, the representation of library pulses was changed from
# complex "amp" to float "amp" and "angle". The existing library pulses in previous versions are
# handled here separately to conform with the new representation. To avoid role assumption for
# "amp" for custom pulses, only the library pulses are handled this way.
# Note that parameters is mutated during the function call
# List of pulses in the library in QPY version 5 and below:
legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"]
if pulse_type in legacy_library_pulses:
# Once complex amp support will be deprecated we will need:
# parameters["angle"] = np.angle(parameters["amp"])
# parameters["amp"] = np.abs(parameters["amp"])
# In the meanwhile we simply add:
parameters["angle"] = 0
_amp, _angle = sym.symbols("amp, angle")
envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle))
# And warn that this will change in future releases:
warnings.warn(
"Complex amp support for symbolic library pulses will be deprecated. "
"Once deprecated, library pulses loaded from old QPY files (Terra version <=0.22.2),"
" will be converted automatically to float (amp,angle) representation.",
PendingDeprecationWarning,
)
return envelope
def _read_symbolic_pulse(file_obj, version, qiskit_version):
header = formats.SYMBOLIC_PULSE._make(
struct.unpack(
formats.SYMBOLIC_PULSE_PACK,
@ -88,6 +125,10 @@ def _read_symbolic_pulse(file_obj, version):
version=version,
vectors={},
)
if qiskit_version < (0, 23, 0):
envelope = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters)
# Note that parameters is mutated during the function call
duration = value.read_value(file_obj, version, {})
name = value.read_value(file_obj, version, {})
@ -120,27 +161,29 @@ def _read_alignment_context(file_obj, version):
return instance
def _loads_operand(type_key, data_bytes, version):
def _loads_operand(type_key, data_bytes, version, qiskit_version):
if type_key == type_keys.ScheduleOperand.WAVEFORM:
return common.data_from_binary(data_bytes, _read_waveform, version=version)
if type_key == type_keys.ScheduleOperand.SYMBOLIC_PULSE:
return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version)
return common.data_from_binary(
data_bytes, _read_symbolic_pulse, version=version, qiskit_version=qiskit_version
)
if type_key == type_keys.ScheduleOperand.CHANNEL:
return common.data_from_binary(data_bytes, _read_channel, version=version)
return value.loads_value(type_key, data_bytes, version, {})
def _read_element(file_obj, version, metadata_deserializer):
def _read_element(file_obj, version, metadata_deserializer, qiskit_version=None):
type_key = common.read_type_key(file_obj)
if type_key == type_keys.Program.SCHEDULE_BLOCK:
return read_schedule_block(file_obj, version, metadata_deserializer)
return read_schedule_block(
file_obj, version, metadata_deserializer, qiskit_version=qiskit_version
)
operands = common.read_sequence(
file_obj,
deserializer=_loads_operand,
version=version,
file_obj, deserializer=_loads_operand, version=version, qiskit_version=qiskit_version
)
name = value.read_value(file_obj, version, {})
@ -251,7 +294,7 @@ def _write_element(file_obj, element, metadata_serializer):
value.write_value(file_obj, element.name)
def read_schedule_block(file_obj, version, metadata_deserializer=None):
def read_schedule_block(file_obj, version, metadata_deserializer=None, qiskit_version=None):
"""Read a single ScheduleBlock from the file like object.
Args:
@ -264,6 +307,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None):
in the file-like object. If this is not specified the circuit metadata will
be parsed as JSON with the stdlib ``json.load()`` function using
the default ``JSONDecoder`` class.
qiskit_version (tuple): tuple with major, minor and patch versions of qiskit.
Returns:
ScheduleBlock: The schedule block object from the file.
@ -272,6 +316,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None):
TypeError: If any of the instructions is invalid data format.
QiskitError: QPY version is earlier than block support.
"""
if version < 5:
QiskitError(f"QPY version {version} does not support ScheduleBlock.")
@ -292,7 +337,9 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None):
alignment_context=context,
)
for _ in range(data.num_elements):
block_elm = _read_element(file_obj, version, metadata_deserializer)
block_elm = _read_element(
file_obj, version, metadata_deserializer, qiskit_version=qiskit_version
)
block.append(block_elm, inplace=True)
return block

View File

@ -228,21 +228,19 @@ def load(
if data.preface.decode(common.ENCODE) != "QISKIT":
raise QiskitError("Input file is not a valid QPY file")
version_match = VERSION_PATTERN_REGEX.search(__version__)
version_parts = [int(x) for x in version_match.group("release").split(".")]
header_version_parts = [data.major_version, data.minor_version, data.patch_version]
env_qiskit_version = [int(x) for x in version_match.group("release").split(".")]
qiskit_version = (data.major_version, data.minor_version, data.patch_version)
# pylint: disable=too-many-boolean-expressions
if (
version_parts[0] < header_version_parts[0]
env_qiskit_version[0] < qiskit_version[0]
or (
version_parts[0] == header_version_parts[0]
and header_version_parts[1] > version_parts[1]
env_qiskit_version[0] == qiskit_version[0] and qiskit_version[1] > env_qiskit_version[1]
)
or (
version_parts[0] == header_version_parts[0]
and header_version_parts[1] == version_parts[1]
and header_version_parts[2] > version_parts[2]
env_qiskit_version[0] == qiskit_version[0]
and qiskit_version[1] == env_qiskit_version[1]
and qiskit_version[2] > env_qiskit_version[2]
)
):
warnings.warn(
@ -250,7 +248,7 @@ def load(
"file, %s, is newer than the current qiskit version %s. "
"This may result in an error if the QPY file uses "
"instructions not present in this current qiskit "
"version" % (".".join([str(x) for x in header_version_parts]), __version__)
"version" % (".".join([str(x) for x in qiskit_version]), __version__)
)
if data.qpy_version < 5:
@ -268,6 +266,11 @@ def load(
programs = []
for _ in range(data.num_programs):
programs.append(
loader(file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer)
loader(
file_obj,
data.qpy_version,
metadata_deserializer=metadata_deserializer,
qiskit_version=qiskit_version,
)
)
return programs

View File

@ -0,0 +1,17 @@
---
features:
- |
The pulses in the Qiskit Pulse library
* :class:`~qiskit.pulse.library.Gaussian`
* :class:`~qiskit.pulse.library.GaussianSquare`
* :class:`~qiskit.pulse.library.Drag`
* :class:`~qiskit.pulse.library.Constant`
can be initialized with new parameter angle, such that two float parameters could be provided - `amp`,`angle`.
Initialization with complex `amp` will be supported until it will be deprecated in future version. However,
Providing complex `amp` with a finite `angle` will result in `PulseError`.
For example, instead of calling `Gaussian(duration=100,sigma=20,amp=0.5j)` one
should use `Gaussian(duration=100,sigma=20,amp=0.5,angle=np.pi/2)`. The pulse envelope which used to be
defined as `amp * ...` is in turn defined as `amp * exp(1j * angle) * ...`. This change aims to better support
Qiskit Experiments where the amplitude and angle of pulses are calibrated in separate experiments.

View File

@ -747,20 +747,3 @@ class TestParametrizedBlockOperation(BaseTestBlock):
ref_sched = ref_sched.insert(90, pulse.Delay(10, self.d0))
self.assertScheduleEqual(block, ref_sched)
def test_assigned_amplitude_is_complex(self):
"""Test pulse amp parameter is always complex valued.
Note that IBM backend treats "amp" as a special parameter,
and this should be complex value otherwise IBM backends raise 8042 error.
"Pulse parameter "amp" must be specified as a list of the form [real, imag]"
"""
amp = circuit.Parameter("amp")
block = pulse.ScheduleBlock()
block += pulse.Play(pulse.Constant(100, amp), pulse.DriveChannel(0))
assigned_block = block.assign_parameters({amp: 0.1}, inplace=True)
assigned_amp = assigned_block.blocks[0].pulse.amp
self.assertIsInstance(assigned_amp, complex)

View File

@ -121,6 +121,29 @@ class TestParametricPulses(QiskitTestCase):
Constant(duration=150, amp=0.1 + 0.4j)
Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4)
# This test should be removed once deprecation of complex amp is completed.
def test_complex_amp_deprecation(self):
"""Test that deprecation warnings and errors are raised for complex amp,
and that pulses are equivalent."""
# Test deprecation warnings and errors:
with self.assertWarns(PendingDeprecationWarning):
Gaussian(duration=25, sigma=4, amp=0.5j)
with self.assertWarns(PendingDeprecationWarning):
GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100)
with self.assertRaises(PulseError):
Gaussian(duration=25, sigma=4, amp=0.5j, angle=1)
with self.assertRaises(PulseError):
GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100, angle=0.1)
# Test that new and old API pulses are the same:
gauss_pulse_complex_amp = Gaussian(duration=25, sigma=4, amp=0.5j)
gauss_pulse_amp_angle = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2)
np.testing.assert_almost_equal(
gauss_pulse_amp_angle.get_waveform().samples,
gauss_pulse_complex_amp.get_waveform().samples,
)
def test_gaussian_pulse(self):
"""Test that Gaussian sample pulse matches the pulse library."""
gauss = Gaussian(duration=25, sigma=4, amp=0.5j)
@ -226,32 +249,35 @@ class TestParametricPulses(QiskitTestCase):
def test_parameters(self):
"""Test that the parameters can be extracted as a dict through the `parameters`
attribute."""
drag = Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4)
self.assertEqual(set(drag.parameters.keys()), {"duration", "amp", "sigma", "beta"})
drag = Drag(duration=25, amp=0.2, sigma=7.8, beta=4, angle=0.2)
self.assertEqual(set(drag.parameters.keys()), {"duration", "amp", "sigma", "beta", "angle"})
const = Constant(duration=150, amp=1)
self.assertEqual(set(const.parameters.keys()), {"duration", "amp"})
self.assertEqual(set(const.parameters.keys()), {"duration", "amp", "angle"})
def test_repr(self):
"""Test the repr methods for parametric pulses."""
gaus = Gaussian(duration=25, amp=0.7, sigma=4)
self.assertEqual(repr(gaus), "Gaussian(duration=25, amp=(0.7+0j), sigma=4)")
gaus = Gaussian(duration=25, amp=0.7, sigma=4, angle=0.3)
self.assertEqual(repr(gaus), "Gaussian(duration=25, amp=0.7, sigma=4, angle=0.3)")
gaus = Gaussian(
duration=25, amp=0.1 + 0.7j, sigma=4
) # Should be removed once the deprecation of complex
# amp is completed.
self.assertEqual(repr(gaus), "Gaussian(duration=25, amp=(0.1+0.7j), sigma=4, angle=0)")
gaus_square = GaussianSquare(duration=20, sigma=30, amp=1.0, width=3)
self.assertEqual(
repr(gaus_square), "GaussianSquare(duration=20, amp=(1+0j), sigma=30, width=3)"
repr(gaus_square), "GaussianSquare(duration=20, amp=1.0, sigma=30, width=3, angle=0)"
)
gaus_square = GaussianSquare(
duration=20, sigma=30, amp=1.0, angle=0.2, risefall_sigma_ratio=0.1
)
gaus_square = GaussianSquare(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1)
self.assertEqual(
repr(gaus_square), "GaussianSquare(duration=20, amp=(1+0j), sigma=30, width=14.0)"
repr(gaus_square),
"GaussianSquare(duration=20, amp=1.0, sigma=30, width=14.0, angle=0.2)",
)
drag = Drag(duration=5, amp=0.5, sigma=7, beta=1)
self.assertEqual(repr(drag), "Drag(duration=5, amp=(0.5+0j), sigma=7, beta=1)")
const = Constant(duration=150, amp=0.1 + 0.4j)
self.assertEqual(repr(const), "Constant(duration=150, amp=(0.1+0.4j))")
def test_complex_param_is_complex(self):
"""Check that complex param 'amp' is cast to complex."""
const = Constant(duration=150, amp=1)
self.assertIsInstance(const.amp, complex)
self.assertEqual(repr(drag), "Drag(duration=5, amp=0.5, sigma=7, beta=1, angle=0)")
const = Constant(duration=150, amp=0.1, angle=0.3)
self.assertEqual(repr(const), "Constant(duration=150, amp=0.1, angle=0.3)")
def test_param_validation(self):
"""Test that parametric pulse parameters are validated when initialized."""

View File

@ -92,14 +92,14 @@ class TestInstructionToQobjConverter(QiskitTestCase):
def test_constant_pulse_instruction(self):
"""Test that parametric pulses are correctly converted to PulseQobjInstructions."""
converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2)
instruction = Play(Constant(duration=25, amp=1), ControlChannel(2))
instruction = Play(Constant(duration=25, amp=1, angle=np.pi), ControlChannel(2))
valid_qobj = PulseQobjInstruction(
name="parametric_pulse",
pulse_shape="constant",
ch="u2",
t0=20,
parameters={"duration": 25, "amp": 1},
parameters={"duration": 25, "amp": 1 * np.exp(1j * np.pi)},
)
self.assertEqual(converter(20, instruction), valid_qobj)
@ -200,7 +200,8 @@ class TestQobjToInstructionConverter(QiskitTestCase):
def test_parametric_pulses(self):
"""Test converted qobj from ParametricInstruction."""
instruction = Play(
Gaussian(duration=25, sigma=15, amp=-0.5 + 0.2j, name="pulse1"), DriveChannel(0)
Gaussian(duration=25, sigma=15, amp=0.5, angle=np.pi / 2, name="pulse1"),
DriveChannel(0),
)
qobj = PulseQobjInstruction(
name="parametric_pulse",
@ -208,12 +209,12 @@ class TestQobjToInstructionConverter(QiskitTestCase):
pulse_shape="gaussian",
ch="d0",
t0=0,
parameters={"duration": 25, "sigma": 15, "amp": -0.5 + 0.2j},
parameters={"duration": 25, "sigma": 15, "amp": 0.5j},
)
converted_instruction = self.converter(qobj)
self.assertEqual(converted_instruction.start_time, 0)
self.assertEqual(converted_instruction.duration, 25)
self.assertEqual(converted_instruction.instructions[0][-1], instruction)
self.assertAlmostEqual(converted_instruction.instructions[0][-1], instruction)
self.assertEqual(converted_instruction.instructions[0][-1].pulse.name, "pulse1")
def test_parametric_pulses_no_label(self):

View File

@ -219,11 +219,11 @@ class TestLoadFromQPY(QpyScheduleTestCase):
# ECR
with builder.align_left():
builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1))
builder.play(GaussianSquare(800, 0.1 - 0.2j, 64, 544), ControlChannel(0))
builder.play(GaussianSquare(800, 0.22, 64, 544, 2), ControlChannel(0))
builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0))
with builder.align_left():
builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1))
builder.play(GaussianSquare(800, -0.1 + 0.2j, 64, 544), ControlChannel(0))
builder.play(GaussianSquare(800, -0.22, 64, 544, 2), ControlChannel(0))
builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0))
# Measure
with builder.align_left():

View File

@ -295,21 +295,6 @@ class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder):
self.assertEqual(schedule(test_qc, self.backend), target_qobj_transform(ref_sched))
def test_pulse_amp_typecasted(self):
"""Test if scaled pulse amplitude is complex type."""
fake_play = Play(
GaussianSquare(duration=800, amp=0.1, sigma=64, risefall_sigma_ratio=2),
ControlChannel(0),
)
fake_theta = circuit.Parameter("theta")
assigned_theta = fake_theta.assign(fake_theta, 0.01)
with builder.build() as test_sched:
RZXCalibrationBuilderNoEcho.rescale_cr_inst(instruction=fake_play, theta=assigned_theta)
scaled_pulse = test_sched.blocks[0].blocks[0].pulse
self.assertIsInstance(scaled_pulse.amp, complex)
def test_pass_alive_with_dcx_ish(self):
"""Test if the pass is not terminated by error with direct CX input."""
cx_sched = Schedule()

View File

@ -376,6 +376,7 @@ class TestWaveformGenerators(QiskitTestCase):
"t0 (sec)": 0.5,
"waveform shape": "Gaussian",
"amp": "amp",
"angle": 0,
"sigma": 3,
"phase": np.pi / 2,
"frequency": 5e9,

View File

@ -406,7 +406,7 @@ def generate_schedule_blocks():
builder.set_phase(1.57, channels.DriveChannel(0))
builder.shift_phase(0.1, channels.DriveChannel(1))
builder.barrier(channels.DriveChannel(0), channels.DriveChannel(1))
builder.play(library.Gaussian(160, 0.1, 40), channels.DriveChannel(0))
builder.play(library.Gaussian(160, 0.1j, 40), channels.DriveChannel(0))
builder.play(library.GaussianSquare(800, 0.1, 64, 544), channels.ControlChannel(0))
builder.play(library.Drag(160, 0.1, 40, 1.5), channels.DriveChannel(1))
builder.play(library.Constant(800, 0.1), channels.MeasureChannel(0))