mirror of https://github.com/Qiskit/qiskit.git
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:
parent
0785ab3131
commit
4342881e19
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue