From 03129e22984d72799050c176de81e4b7cf4f5b15 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 26 Oct 2022 14:02:31 +0300 Subject: [PATCH 01/24] Converting Gaussian SymbolicPulse from complex amp to amp,angle. --- qiskit/pulse/library/symbolic_pulses.py | 50 +++++++++++++++++++++++-- test/python/pulse/test_pulse_lib.py | 18 +++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 4f1aa909f0e0..6a49f71eb9d6 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -21,6 +21,7 @@ import functools import warnings from typing import Any, Dict, List, Optional, Union, Callable +from types import MethodType import numpy as np @@ -470,6 +471,28 @@ def valid_amp_conditions(self) -> sym.Expr: """Return symbolic expression for the pulse amplitude constraints.""" return self._valid_amp_conditions + # This should be removed once the complex amp deprecation is completed. + @property + def amp(self) -> Union[ParameterExpression, complex]: + if "amp" in self._params: + if isinstance(self._params["amp"], complex): + warnings.warn( + f"{self.__class__.__name__}.amp returns the complex amplitude which will be deprecated. " + f"Use {self.__class__.__name__}.amp_angle to get the magnitude of the amplitude and the complex angle " + "as a tuple", + PendingDeprecationWarning + ) + return self._params["amp"] + else: + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute 'amp'") + + @property + def amp_angle(self) -> tuple: + if "amp" in self._params: + return np.abs(self._params["amp"]), np.angle(self._params["amp"]) + else: + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute 'amp_angle'") + def get_waveform(self) -> Waveform: r"""Return a Waveform with samples filled according to the formula that the pulse represents and the parameter values it contains. @@ -614,7 +637,7 @@ 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{amp} \times \exp\left(i\text{angle}\right) \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. """ @@ -624,8 +647,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,9 +657,10 @@ def __new__( 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. @@ -643,7 +668,24 @@ def __new__( Returns: SymbolicPulse instance. """ - 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 isinstance(amp, float): + if amp < 0: + raise PulseError("'amp' has to be positive (use 'angle' to set sign)") + + if angle is None: + angle = 0 + + parameters = {"amp": amp*np.exp(1j*angle), "sigma": sigma} # Prepare symbolic expressions _t, _duration, _amp, _sigma = sym.symbols("t, duration, amp, sigma") diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 44e892cc0a27..06c25ed73a2c 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -121,6 +121,24 @@ def test_construction(self): 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""" + with self.assertWarns(PendingDeprecationWarning): + Gaussian(duration=25, sigma=4, amp=0.5j) + with self.assertRaises(PulseError): + Gaussian(duration=25, sigma=4, amp=0.5j, angle=1) + + gauss_pulse_complex_amp = Gaussian(duration=25, sigma=4, amp=0.5j) + with self.assertWarns(PendingDeprecationWarning): + complex_amp = gauss_pulse_complex_amp.amp + amp_magnitude , angle = gauss_pulse_complex_amp.amp_angle + np.testing.assert_almost_equal(complex_amp, amp_magnitude*np.exp(1j*angle)) + + 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) From e7a0bf7c53a18e1fb8f525a3c722e4a8aa9294f6 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 26 Oct 2022 14:06:08 +0300 Subject: [PATCH 02/24] removed unnecessary import. --- qiskit/pulse/library/symbolic_pulses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 6a49f71eb9d6..69a824b23b37 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -21,7 +21,6 @@ import functools import warnings from typing import Any, Dict, List, Optional, Union, Callable -from types import MethodType import numpy as np From 0310fbd769c70577e2fbed0f99194b308f819e01 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 10 Nov 2022 10:16:41 +0200 Subject: [PATCH 03/24] Completed the changes. --- qiskit/pulse/library/symbolic_pulses.py | 166 +++++++++++++------- qiskit/pulse/parameter_manager.py | 12 +- qiskit/qobj/converters/pulse_instruction.py | 16 +- qiskit/qpy/binary_io/schedules.py | 4 + test/python/pulse/test_pulse_lib.py | 56 ++++--- test/python/qobj/test_pulse_converter.py | 4 +- 6 files changed, 164 insertions(+), 94 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 69a824b23b37..1e8d99cc68a7 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -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 @@ -470,28 +463,6 @@ def valid_amp_conditions(self) -> sym.Expr: """Return symbolic expression for the pulse amplitude constraints.""" return self._valid_amp_conditions - # This should be removed once the complex amp deprecation is completed. - @property - def amp(self) -> Union[ParameterExpression, complex]: - if "amp" in self._params: - if isinstance(self._params["amp"], complex): - warnings.warn( - f"{self.__class__.__name__}.amp returns the complex amplitude which will be deprecated. " - f"Use {self.__class__.__name__}.amp_angle to get the magnitude of the amplitude and the complex angle " - "as a tuple", - PendingDeprecationWarning - ) - return self._params["amp"] - else: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute 'amp'") - - @property - def amp_angle(self) -> tuple: - if "amp" in self._params: - return np.abs(self._params["amp"]), np.angle(self._params["amp"]) - else: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute 'amp_angle'") - def get_waveform(self) -> Waveform: r"""Return a Waveform with samples filled according to the formula that the pulse represents and the parameter values it contains. @@ -636,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 \exp\left(i\text{angle}\right) \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" @@ -656,7 +628,8 @@ def __new__( Args: duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the Gaussian envelope. Complex amp support will be deprecated. + 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. @@ -666,31 +639,35 @@ def __new__( Returns: SymbolicPulse instance. + + Raises: + PulseError: If both complex amp and angle are provided as arguments. """ # 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 + "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 isinstance(amp, float): - if amp < 0: - raise PulseError("'amp' has to be positive (use 'angle' to set sign)") - if angle is None: angle = 0 - parameters = {"amp": amp*np.exp(1j*angle), "sigma": sigma} + 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) + # To conform with some old tests, the angle part is inserted only when needed. + if angle != 0: + envelope_expr *= sym.exp(1j * _angle) + consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -741,10 +718,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" @@ -752,9 +730,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, @@ -763,10 +742,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 @@ -777,6 +758,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: @@ -791,10 +773,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 @@ -806,6 +804,9 @@ def __new__( envelope_expr = _amp * sym.Piecewise( (_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True) ) + # To conform with some old tests, the angle part is inserted only when needed. + if angle != 0: + envelope_expr *= sym.exp(1j * _angle) consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -836,13 +837,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|_ @@ -866,9 +868,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: @@ -876,27 +879,51 @@ def __new__( 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) + # To conform with some old tests, the angle part is inserted only when needed. + if angle != 0: + envelope_expr *= sym.exp(1j * _angle) consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) @@ -921,7 +948,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 """ @@ -930,7 +957,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: @@ -938,18 +966,37 @@ def __new__( 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 @@ -959,6 +1006,9 @@ def __new__( # # See: https://github.com/sympy/sympy/issues/5642 envelope_expr = _amp * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True)) + # To conform with some old tests, the angle part is inserted only when needed. + if angle != 0: + envelope_expr *= sym.exp(1j * _angle) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 instance = SymbolicPulse( diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 330df026ee23..68257c5e34e9 100644 --- a/qiskit/pulse/parameter_manager.py +++ b/qiskit/pulse/parameter_manager.py @@ -50,6 +50,7 @@ Note that we don't need to write any parameter management logic for each object, and thus this parameter framework gives greater scalability to the pulse module. """ +import warnings from copy import copy from typing import List, Dict, Set, Any, Union @@ -231,11 +232,12 @@ 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) + if name == "amp" and isinstance(new_val, complex): + warnings.warn( + "Complex amp will be deprecated. " + "Use float amp (for the magnitude) and float angle instead.", + PendingDeprecationWarning, + ) node._params[name] = new_val node.validate_parameters() diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 1ea4d19123a3..402509e7dd99 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -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 @@ -425,12 +426,17 @@ def convert_play(self, shift, instruction): dict: Dictionary of required parameters. """ if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)): + params = dict(instruction.pulse.parameters) + if "amp" in instruction.pulse.parameters and "angle" in instruction.pulse.parameters: + 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 +729,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(["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") diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 223e2e77b4d8..a25bf15deb42 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -88,6 +88,10 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) + if "amp" in parameters and isinstance(parameters["amp"], complex): + parameters["angle"] = np.angle(parameters["amp"]) + parameters["amp"] = np.abs(parameters["amp"]) + duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 06c25ed73a2c..fae82ed00c4c 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -123,21 +123,24 @@ def test_construction(self): # 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""" + """Test that deprecation warnings and errors are raised for complex amp, + and that pulses are equivalent.""" 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) + # gauss_pulse_complex_amp = Gaussian(duration=25, sigma=4, amp=0.5j) - with self.assertWarns(PendingDeprecationWarning): - complex_amp = gauss_pulse_complex_amp.amp - amp_magnitude , angle = gauss_pulse_complex_amp.amp_angle - np.testing.assert_almost_equal(complex_amp, amp_magnitude*np.exp(1j*angle)) - - 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) + 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.""" @@ -244,32 +247,35 @@ def test_constant_samples(self): 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.""" diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index ea5b77e78209..5a5bd7626425 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -92,14 +92,14 @@ def test_gaussian_square_pulse_instruction(self): 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) From 786c0006b1186a882245f464ab9da6a2ad44a3e2 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 14 Nov 2022 00:31:19 +0200 Subject: [PATCH 04/24] Bug fix and test updates. --- qiskit/qobj/converters/pulse_instruction.py | 2 +- test/python/qobj/test_pulse_converter.py | 6 +++--- test/python/qpy/test_block_load_from_qpy.py | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 402509e7dd99..8cb3135083a2 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -730,7 +730,7 @@ 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(["amp"], complex): + if "amp" in params and isinstance(params["amp"], complex): params["angle"] = np.angle(params["amp"]) params["amp"] = np.abs(params["amp"]) diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index 5a5bd7626425..b6e21d10775f 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -200,7 +200,7 @@ def test_drive_instruction(self): 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 +208,12 @@ def test_parametric_pulses(self): 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): diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index ef814aecce61..425bf478fb94 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -198,11 +198,12 @@ def test_bell_schedule(self): # 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 + np.pi), 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(): From a591f66ad0e356cbad4b76df6c0a466d24ed6a17 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 14 Nov 2022 00:35:51 +0200 Subject: [PATCH 05/24] removed commented line. --- test/python/qpy/test_block_load_from_qpy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 425bf478fb94..d34a011d0e6b 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -202,7 +202,6 @@ def test_bell_schedule(self): 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.22, 64, 544, 2 + np.pi), 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 From dbe43fb19c8fd45a2352451fc70fb58072194c2a Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 14 Nov 2022 08:33:17 +0200 Subject: [PATCH 06/24] black correction. --- test/python/qobj/test_pulse_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index b6e21d10775f..95f34aa17d2c 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -200,7 +200,8 @@ def test_drive_instruction(self): def test_parametric_pulses(self): """Test converted qobj from ParametricInstruction.""" instruction = Play( - Gaussian(duration=25, sigma=15, amp=0.5, angle=np.pi/2, 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", From 8a74b70edbe60db99c3cfea35ac2090df0846a7b Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 14 Nov 2022 13:50:56 +0200 Subject: [PATCH 07/24] Tests correction. --- test/python/pulse/test_block.py | 32 +++++++++---------- test/python/pulse/test_calibrationbuilder.py | 30 ++++++++--------- .../visualization/pulse_v2/test_generators.py | 1 + 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index ab5fe33a67ad..0d10faaaca2b 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -748,19 +748,19 @@ def test_parametrized_context(self): 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) + # 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) diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/pulse/test_calibrationbuilder.py index ba108ca9f64b..a0bbcf36992b 100644 --- a/test/python/pulse/test_calibrationbuilder.py +++ b/test/python/pulse/test_calibrationbuilder.py @@ -156,21 +156,21 @@ def test_rzx_calibration_builder(self): # the scaled CR pulse from the CX gate match. self.assertEqual(rzx_qc_duration, duration) - 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) - - scaled = RZXCalibrationBuilderNoEcho.rescale_cr_inst( - instruction=fake_play, theta=assigned_theta - ) - scaled_pulse = scaled.pulse - - self.assertIsInstance(scaled_pulse.amp, complex) + # 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) + # + # scaled = RZXCalibrationBuilderNoEcho.rescale_cr_inst( + # instruction=fake_play, theta=assigned_theta + # ) + # scaled_pulse = scaled.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.""" diff --git a/test/python/visualization/pulse_v2/test_generators.py b/test/python/visualization/pulse_v2/test_generators.py index 40ea21841283..9c9c5110f8bc 100644 --- a/test/python/visualization/pulse_v2/test_generators.py +++ b/test/python/visualization/pulse_v2/test_generators.py @@ -376,6 +376,7 @@ def test_gen_filled_waveform_stepwise_opaque(self): "t0 (sec)": 0.5, "waveform shape": "Gaussian", "amp": "amp", + "angle": 0, "sigma": 3, "phase": np.pi / 2, "frequency": 5e9, From 22c61b9c992072a723c7dfba0a99d25d510af0b0 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 21 Nov 2022 01:57:59 +0200 Subject: [PATCH 08/24] Bump QPY version, and adjust QPY loader. --- qiskit/pulse/parameter_manager.py | 7 -- qiskit/qpy/binary_io/schedules.py | 67 ++++++++++++++++++-- qiskit/qpy/common.py | 2 +- test/python/pulse/test_block.py | 17 ----- test/python/pulse/test_calibrationbuilder.py | 16 ----- test/python/pulse/test_pulse_lib.py | 4 +- 6 files changed, 67 insertions(+), 46 deletions(-) diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 68257c5e34e9..d1db89ab1bed 100644 --- a/qiskit/pulse/parameter_manager.py +++ b/qiskit/pulse/parameter_manager.py @@ -50,7 +50,6 @@ Note that we don't need to write any parameter management logic for each object, and thus this parameter framework gives greater scalability to the pulse module. """ -import warnings from copy import copy from typing import List, Dict, Set, Any, Union @@ -232,12 +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 isinstance(new_val, complex): - warnings.warn( - "Complex amp will be deprecated. " - "Use float amp (for the magnitude) and float angle instead.", - PendingDeprecationWarning, - ) node._params[name] = new_val node.validate_parameters() diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index a25bf15deb42..b97bbe9f807c 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -26,6 +26,11 @@ 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,6 +76,60 @@ def _loads_symbolic_expr(expr_bytes): return expr +def _read_symbolic_pulse_v5(file_obj, version): + # In the transition to Qiskit Terra > 0.22, the representation of library pulses was changed from + # complex "amp" to float "amp" and "angle". To reflect this, QPY version was bumped to 6. The + # existing library pulses in QPY<=5 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. + + # List of pulses in the library in QPY version 5 and below: + v5_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] + + header = formats.SYMBOLIC_PULSE._make( + struct.unpack( + formats.SYMBOLIC_PULSE_PACK, + file_obj.read(formats.SYMBOLIC_PULSE_SIZE), + ) + ) + pulse_type = file_obj.read(header.type_size).decode(common.ENCODE) + envelope = _loads_symbolic_expr(file_obj.read(header.envelope_size)) + constraints = _loads_symbolic_expr(file_obj.read(header.constraints_size)) + valid_amp_conditions = _loads_symbolic_expr(file_obj.read(header.valid_amp_conditions_size)) + parameters = common.read_mapping( + file_obj, + deserializer=value.loads_value, + version=version, + vectors={}, + ) + if pulse_type in v5_library_pulses: + if isinstance( + parameters["amp"], complex + ): # We know that "amp" is in "parameters" for these pulses. + parameters["angle"] = np.angle(parameters["amp"]) + parameters["amp"] = np.abs(parameters["amp"]) + _amp, _angle = sym.symbols("amp, angle") + envelope = envelope.subs(_amp, _amp * sym.exp(1j * _angle)) + constraints = constraints.subs(_amp, _amp * sym.exp(1j * _angle)) + valid_amp_conditions = valid_amp_conditions.subs(_amp, _amp * sym.exp(1j * _angle)) + else: + parameters["angle"] = 0 + + duration = value.read_value(file_obj, version, {}) + name = value.read_value(file_obj, version, {}) + + return library.SymbolicPulse( + pulse_type=pulse_type, + duration=duration, + parameters=parameters, + name=name, + limit_amplitude=header.amp_limited, + envelope=envelope, + constraints=constraints, + valid_amp_conditions=valid_amp_conditions, + ) + + def _read_symbolic_pulse(file_obj, version): header = formats.SYMBOLIC_PULSE._make( struct.unpack( @@ -88,9 +147,6 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) - if "amp" in parameters and isinstance(parameters["amp"], complex): - parameters["angle"] = np.angle(parameters["amp"]) - parameters["amp"] = np.abs(parameters["amp"]) duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) @@ -128,7 +184,10 @@ def _loads_operand(type_key, data_bytes, 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) + if version <= 5: + return common.data_from_binary(data_bytes, _read_symbolic_pulse_v5, version=version) + else: + return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) if type_key == type_keys.ScheduleOperand.CHANNEL: return common.data_from_binary(data_bytes, _read_channel, version=version) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index f20aa2245582..7ecbbe819353 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -21,7 +21,7 @@ from qiskit.qpy import formats -QPY_VERSION = 5 +QPY_VERSION = 6 ENCODE = "utf8" diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index 0d10faaaca2b..d70bcc894aaa 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -747,20 +747,3 @@ def test_parametrized_context(self): 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) diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/pulse/test_calibrationbuilder.py index a0bbcf36992b..76a389e3317f 100644 --- a/test/python/pulse/test_calibrationbuilder.py +++ b/test/python/pulse/test_calibrationbuilder.py @@ -156,22 +156,6 @@ def test_rzx_calibration_builder(self): # the scaled CR pulse from the CX gate match. self.assertEqual(rzx_qc_duration, duration) - # 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) - # - # scaled = RZXCalibrationBuilderNoEcho.rescale_cr_inst( - # instruction=fake_play, theta=assigned_theta - # ) - # scaled_pulse = scaled.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() diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index fae82ed00c4c..428d8094a2f7 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -125,6 +125,8 @@ def test_construction(self): 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): @@ -134,7 +136,7 @@ def test_complex_amp_deprecation(self): 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( From 04dcd0e8cd1ec73630749b136bb9521225f1589a Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 21 Nov 2022 02:00:13 +0200 Subject: [PATCH 09/24] Release Notes. --- ...version-to-amp-angle-0c6bcf742eac8945.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml new file mode 100644 index 000000000000..9fbb8e21576b --- /dev/null +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + The pulses in the Symbolic Pulse Library were converted from a complex amplitude representation, to an (amp,angle) + representation (both floats). For example, instead of calling `pulse.Gaussian(duration=100,sigma=20,amp=0.5j)` one + should use `pulse.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. Note that `amp` + can take negative values. + - | + QPY version bumped to 6, to accommodate the symbolic library pulses (amp,angle) representation. +deprecations: + - | + Providing complex `amp` to pulses of the Symbolic Pulse Library is now pending deprecation. The complex `amp` is + replaced by two float parameters `amp` and `angle`. See features section for more details. Calling, for example, + `pulse.Gaussian(duration=100,sigma=20,amp=0.5j)` will still work until the final deprecation, but one should use + `pulse.Gaussian(duration=100,sigma=20,amp=0.5,angle=np.pi/2)` instead. It should be noted that during the + deprecation period, unusual behaviours may arise. Namely, if one creates a library pulse with the old API (complex + amp), and then goes through a round-trip conversion to `qpy` file and back - the resulting pulse will conform to + the new API (amp,angle duo). However, until the deprecation is completed, both pulses will perform identically. From 4d1ea591a8bdf82be0ba1ba7b0914d9650dc01bc Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Mon, 21 Nov 2022 13:47:59 +0200 Subject: [PATCH 10/24] Update qiskit/qobj/converters/pulse_instruction.py Co-authored-by: Naoki Kanazawa --- qiskit/qobj/converters/pulse_instruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 8cb3135083a2..e756ef9402a8 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -427,7 +427,7 @@ def convert_play(self, shift, instruction): """ if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)): params = dict(instruction.pulse.parameters) - if "amp" in instruction.pulse.parameters and "angle" in instruction.pulse.parameters: + if "amp" in params and "angle" in params: params["amp"] = complex(params["amp"] * np.exp(1j * params["angle"])) del params["angle"] From 4412df63b3b768414ecc2d59a14cc101bb1da718 Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Mon, 21 Nov 2022 13:54:17 +0200 Subject: [PATCH 11/24] Update releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml Co-authored-by: Naoki Kanazawa --- ...ic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml index 9fbb8e21576b..097a06f195e5 100644 --- a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -1,7 +1,12 @@ --- features: - | - The pulses in the Symbolic Pulse Library were converted from a complex amplitude representation, to an (amp,angle) + 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. representation (both floats). For example, instead of calling `pulse.Gaussian(duration=100,sigma=20,amp=0.5j)` one should use `pulse.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 From 54bcdf28883689047599581be340fcdc6f622faf Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 21 Nov 2022 14:32:00 +0200 Subject: [PATCH 12/24] Some more corrections. --- qiskit/pulse/library/symbolic_pulses.py | 8 ++-- qiskit/qobj/converters/pulse_instruction.py | 1 + qiskit/qpy/__init__.py | 8 ++++ qiskit/qpy/binary_io/schedules.py | 44 +++---------------- ...version-to-amp-angle-0c6bcf742eac8945.yaml | 24 ++++------ 5 files changed, 27 insertions(+), 58 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 1e8d99cc68a7..8c56fa659cb8 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -666,7 +666,7 @@ def __new__( envelope_expr = _amp * _lifted_gaussian(_t, _center, _duration + 1, _sigma) # To conform with some old tests, the angle part is inserted only when needed. if angle != 0: - envelope_expr *= sym.exp(1j * _angle) + envelope_expr *= sym.exp(sym.I * _angle) consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -806,7 +806,7 @@ def __new__( ) # To conform with some old tests, the angle part is inserted only when needed. if angle != 0: - envelope_expr *= sym.exp(1j * _angle) + envelope_expr *= sym.exp(sym.I * _angle) consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -923,7 +923,7 @@ def __new__( envelope_expr = _amp * (_gauss + sym.I * _beta * _deriv) # To conform with some old tests, the angle part is inserted only when needed. if angle != 0: - envelope_expr *= sym.exp(1j * _angle) + envelope_expr *= sym.exp(sym.I * _angle) consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) @@ -1008,7 +1008,7 @@ def __new__( envelope_expr = _amp * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True)) # To conform with some old tests, the angle part is inserted only when needed. if angle != 0: - envelope_expr *= sym.exp(1j * _angle) + envelope_expr *= sym.exp(sym.I * _angle) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 instance = SymbolicPulse( diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index e756ef9402a8..55480367dcd4 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -427,6 +427,7 @@ def convert_play(self, shift, instruction): """ 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"] diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 9df5806e5f4e..b16eff6382a6 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -100,6 +100,14 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_6: + +Version 6 +========= +In Version 6, the symbolic library pulses changed from complex `amp` representation to float (`amp` +,`angle`) representation. To accommodate this change, when a QPY file of version 5 or lower is loaded, +these library pulses are converted to the new format. + .. _qpy_version_5: Version 5 diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index b97bbe9f807c..14977ee01602 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -76,7 +76,7 @@ def _loads_symbolic_expr(expr_bytes): return expr -def _read_symbolic_pulse_v5(file_obj, version): +def _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters): # In the transition to Qiskit Terra > 0.22, the representation of library pulses was changed from # complex "amp" to float "amp" and "angle". To reflect this, QPY version was bumped to 6. The # existing library pulses in QPY<=5 are handled here separately to conform with the new @@ -86,22 +86,6 @@ def _read_symbolic_pulse_v5(file_obj, version): # List of pulses in the library in QPY version 5 and below: v5_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] - header = formats.SYMBOLIC_PULSE._make( - struct.unpack( - formats.SYMBOLIC_PULSE_PACK, - file_obj.read(formats.SYMBOLIC_PULSE_SIZE), - ) - ) - pulse_type = file_obj.read(header.type_size).decode(common.ENCODE) - envelope = _loads_symbolic_expr(file_obj.read(header.envelope_size)) - constraints = _loads_symbolic_expr(file_obj.read(header.constraints_size)) - valid_amp_conditions = _loads_symbolic_expr(file_obj.read(header.valid_amp_conditions_size)) - parameters = common.read_mapping( - file_obj, - deserializer=value.loads_value, - version=version, - vectors={}, - ) if pulse_type in v5_library_pulses: if isinstance( parameters["amp"], complex @@ -109,25 +93,10 @@ def _read_symbolic_pulse_v5(file_obj, version): parameters["angle"] = np.angle(parameters["amp"]) parameters["amp"] = np.abs(parameters["amp"]) _amp, _angle = sym.symbols("amp, angle") - envelope = envelope.subs(_amp, _amp * sym.exp(1j * _angle)) - constraints = constraints.subs(_amp, _amp * sym.exp(1j * _angle)) - valid_amp_conditions = valid_amp_conditions.subs(_amp, _amp * sym.exp(1j * _angle)) + envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle)) else: parameters["angle"] = 0 - - duration = value.read_value(file_obj, version, {}) - name = value.read_value(file_obj, version, {}) - - return library.SymbolicPulse( - pulse_type=pulse_type, - duration=duration, - parameters=parameters, - name=name, - limit_amplitude=header.amp_limited, - envelope=envelope, - constraints=constraints, - valid_amp_conditions=valid_amp_conditions, - ) + return envelope def _read_symbolic_pulse(file_obj, version): @@ -147,6 +116,8 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) + if version <= 5: + envelope = _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters) duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) @@ -184,10 +155,7 @@ def _loads_operand(type_key, data_bytes, 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: - if version <= 5: - return common.data_from_binary(data_bytes, _read_symbolic_pulse_v5, version=version) - else: - return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) + return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) if type_key == type_keys.ScheduleOperand.CHANNEL: return common.data_from_binary(data_bytes, _read_channel, version=version) diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml index 097a06f195e5..0dfda23c5f7b 100644 --- a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -2,24 +2,16 @@ features: - | The pulses in the Qiskit Pulse library - * :class:`~qiskit.pulse.library.Gaussian` + * :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. - representation (both floats). For example, instead of calling `pulse.Gaussian(duration=100,sigma=20,amp=0.5j)` one - should use `pulse.Gaussian(duration=100,sigma=20,amp=0.5,angle=np.pi/2)`. The pulse envelope which used to be + * :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. Note that `amp` - can take negative values. + Qiskit Experiments where the amplitude and angle of pulses are calibrated in separate experiments. - | QPY version bumped to 6, to accommodate the symbolic library pulses (amp,angle) representation. -deprecations: - - | - Providing complex `amp` to pulses of the Symbolic Pulse Library is now pending deprecation. The complex `amp` is - replaced by two float parameters `amp` and `angle`. See features section for more details. Calling, for example, - `pulse.Gaussian(duration=100,sigma=20,amp=0.5j)` will still work until the final deprecation, but one should use - `pulse.Gaussian(duration=100,sigma=20,amp=0.5,angle=np.pi/2)` instead. It should be noted that during the - deprecation period, unusual behaviours may arise. Namely, if one creates a library pulse with the old API (complex - amp), and then goes through a round-trip conversion to `qpy` file and back - the resulting pulse will conform to - the new API (amp,angle duo). However, until the deprecation is completed, both pulses will perform identically. From fbe5ead10be3542c60f1c6b79f0a8f16690d8f53 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 23 Nov 2022 14:40:30 +0200 Subject: [PATCH 13/24] QPY load adjustment. --- qiskit/qpy/binary_io/schedules.py | 39 +++++++++++-------- ...version-to-amp-angle-0c6bcf742eac8945.yaml | 1 + test/qpy_compat/test_qpy.py | 2 +- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 14977ee01602..6ffb1bc12b97 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -16,6 +16,7 @@ import json import struct import zlib +import warnings import numpy as np @@ -26,11 +27,6 @@ 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) @@ -76,27 +72,34 @@ def _loads_symbolic_expr(expr_bytes): return expr -def _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters): +def _format_legacy_qiskit_pulse_v5(pulse_type, parameters): # In the transition to Qiskit Terra > 0.22, the representation of library pulses was changed from # complex "amp" to float "amp" and "angle". To reflect this, QPY version was bumped to 6. The # existing library pulses in QPY<=5 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: v5_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] if pulse_type in v5_library_pulses: - if isinstance( - parameters["amp"], complex - ): # We know that "amp" is in "parameters" for these pulses. - parameters["angle"] = np.angle(parameters["amp"]) - parameters["amp"] = np.abs(parameters["amp"]) - _amp, _angle = sym.symbols("amp, angle") - envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle)) - else: - parameters["angle"] = 0 - return envelope + # Once complex amp support will be deprecated we will need: + # parameters["angle"] = np.angle(parameters["amp"]) + # parameters["amp"] = np.abs(parameters["amp"]) + # _amp, _angle = sym.symbols("amp, angle") + # envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle)) + + # In the meanwhile we simply add: + parameters["angle"] = 0 + # 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, " + "QPY version <=5) will be converted automatically to float (amp,angle) representation.", + PendingDeprecationWarning, + ) def _read_symbolic_pulse(file_obj, version): @@ -116,8 +119,10 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) + print(version) if version <= 5: - envelope = _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters) + _format_legacy_qiskit_pulse_v5(pulse_type, parameters) + # Note that parameters is mutated during the function call duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml index 0dfda23c5f7b..2998c76d3659 100644 --- a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -15,3 +15,4 @@ features: Qiskit Experiments where the amplitude and angle of pulses are calibrated in separate experiments. - | QPY version bumped to 6, to accommodate the symbolic library pulses (amp,angle) representation. + diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 310ba871a771..82604f3ddb93 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -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)) From 1a6db4a3ef0cf7b19207452a1e8827e354506eb2 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 24 Nov 2022 08:25:01 +0200 Subject: [PATCH 14/24] Removed debug print --- qiskit/qpy/binary_io/schedules.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 6ffb1bc12b97..318e0af354e2 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -119,7 +119,6 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) - print(version) if version <= 5: _format_legacy_qiskit_pulse_v5(pulse_type, parameters) # Note that parameters is mutated during the function call From d7d4927fa245a595ad9bf66f65905593802bd17d Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 27 Nov 2022 01:13:31 +0200 Subject: [PATCH 15/24] Always add "angle" to envelope --- qiskit/pulse/library/symbolic_pulses.py | 22 ++++++---------------- qiskit/qpy/binary_io/schedules.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 8c56fa659cb8..990ce21359f6 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -663,10 +663,7 @@ def __new__( _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) - # To conform with some old tests, the angle part is inserted only when needed. - if angle != 0: - envelope_expr *= sym.exp(sym.I * _angle) + 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 @@ -801,12 +798,10 @@ 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( + envelope_expr = _amp * sym.exp(sym.I * _angle) * sym.Piecewise( (_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True) ) - # To conform with some old tests, the angle part is inserted only when needed. - if angle != 0: - envelope_expr *= sym.exp(sym.I * _angle) + consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -920,10 +915,7 @@ def __new__( _gauss = _lifted_gaussian(_t, _center, _duration + 1, _sigma) _deriv = -(_t - _center) / (_sigma**2) * _gauss - envelope_expr = _amp * (_gauss + sym.I * _beta * _deriv) - # To conform with some old tests, the angle part is inserted only when needed. - if angle != 0: - envelope_expr *= sym.exp(sym.I * _angle) + 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) @@ -1005,10 +997,8 @@ 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)) - # To conform with some old tests, the angle part is inserted only when needed. - if angle != 0: - envelope_expr *= sym.exp(sym.I * _angle) + 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( diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 318e0af354e2..07c63bea0584 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -27,6 +27,11 @@ 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) @@ -72,7 +77,7 @@ def _loads_symbolic_expr(expr_bytes): return expr -def _format_legacy_qiskit_pulse_v5(pulse_type, parameters): +def _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters): # In the transition to Qiskit Terra > 0.22, the representation of library pulses was changed from # complex "amp" to float "amp" and "angle". To reflect this, QPY version was bumped to 6. The # existing library pulses in QPY<=5 are handled here separately to conform with the new @@ -88,11 +93,12 @@ def _format_legacy_qiskit_pulse_v5(pulse_type, parameters): # Once complex amp support will be deprecated we will need: # parameters["angle"] = np.angle(parameters["amp"]) # parameters["amp"] = np.abs(parameters["amp"]) - # _amp, _angle = sym.symbols("amp, angle") - # envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle)) # 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. " @@ -100,6 +106,7 @@ def _format_legacy_qiskit_pulse_v5(pulse_type, parameters): "QPY version <=5) will be converted automatically to float (amp,angle) representation.", PendingDeprecationWarning, ) + return envelope def _read_symbolic_pulse(file_obj, version): @@ -120,7 +127,7 @@ def _read_symbolic_pulse(file_obj, version): vectors={}, ) if version <= 5: - _format_legacy_qiskit_pulse_v5(pulse_type, parameters) + envelope = _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters) # Note that parameters is mutated during the function call duration = value.read_value(file_obj, version, {}) From 211838a02e36f0d0b9d464153f05ef15a96d695c Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 27 Nov 2022 01:18:57 +0200 Subject: [PATCH 16/24] black --- qiskit/pulse/library/symbolic_pulses.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 990ce21359f6..713265c47a46 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -663,7 +663,9 @@ def __new__( _t, _duration, _amp, _sigma, _angle = sym.symbols("t, duration, amp, sigma, angle") _center = _duration / 2 - envelope_expr = _amp * sym.exp(sym.I * _angle) * _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 @@ -798,8 +800,12 @@ 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.exp(sym.I * _angle) * 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) @@ -997,7 +1003,11 @@ def __new__( # ParametricPulse.get_waveform(). # # See: https://github.com/sympy/sympy/issues/5642 - envelope_expr = _amp * sym.exp(sym.I * _angle) * 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 From e07a2c567e0d7f85a9ded70b26b014438d4e6e58 Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Mon, 28 Nov 2022 14:54:05 +0200 Subject: [PATCH 17/24] Update qiskit/qpy/__init__.py Co-authored-by: Naoki Kanazawa --- qiskit/qpy/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index b16eff6382a6..9ff813b8b1c6 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -104,9 +104,13 @@ Version 6 ========= -In Version 6, the symbolic library pulses changed from complex `amp` representation to float (`amp` -,`angle`) representation. To accommodate this change, when a QPY file of version 5 or lower is loaded, -these library pulses are converted to the new format. + +Version 6 has an internal update to :ref:`qpy_schedule_symbolic_pulse` loader of `previous` QPY +version to adapt in the latest Qiskit library pulse representation. In Qiskit Terra 0.23 and above, +complex `amp` value representation is replaced with float (`amp`, `angle`) pair. +Because a QPY binary file dumped by the QPY version 5 and below implies the data +is still represented by the conventional complex amp format, the loaded pulse parameters +and envelope are immediately converted into new format to instantiate the pulse in new style. .. _qpy_version_5: From 8e1db4fd689330803661b85651f7b585b7f0b53f Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 28 Nov 2022 15:01:41 +0200 Subject: [PATCH 18/24] resolve conflict --- test/python/pulse/test_calibrationbuilder.py | 181 ------------------- 1 file changed, 181 deletions(-) delete mode 100644 test/python/pulse/test_calibrationbuilder.py diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/pulse/test_calibrationbuilder.py deleted file mode 100644 index 76a389e3317f..000000000000 --- a/test/python/pulse/test_calibrationbuilder.py +++ /dev/null @@ -1,181 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test the RZXCalibrationBuilderNoEcho.""" - -from math import erf, pi - -import numpy as np -from ddt import data, ddt - -from qiskit import circuit, schedule -from qiskit.pulse import ( - ControlChannel, - Delay, - DriveChannel, - GaussianSquare, - Waveform, - Play, - ShiftPhase, - InstructionScheduleMap, - Schedule, -) -from qiskit.test import QiskitTestCase -from qiskit.providers.fake_provider import FakeAthens -from qiskit.transpiler import PassManager -from qiskit.transpiler.passes.calibration.builders import ( - RZXCalibrationBuilder, - RZXCalibrationBuilderNoEcho, -) - - -class TestCalibrationBuilder(QiskitTestCase): - """Test the Calibration Builder.""" - - def setUp(self): - super().setUp() - self.backend = FakeAthens() - self.inst_map = self.backend.defaults().instruction_schedule_map - - -@ddt -class TestRZXCalibrationBuilder(TestCalibrationBuilder): - """Test RZXCalibrationBuilder.""" - - @data(np.pi / 4, np.pi / 2, np.pi) - def test_rzx_calibration_builder_duration(self, theta: float): - """Test that pulse durations are computed correctly.""" - width = 512.00000001 - sigma = 64 - n_sigmas = 4 - duration = width + n_sigmas * sigma - sample_mult = 16 - amp = 1.0 - pulse = GaussianSquare(duration=duration, amp=amp, sigma=sigma, width=width) - instruction = Play(pulse, ControlChannel(1)) - scaled = RZXCalibrationBuilder.rescale_cr_inst(instruction, theta, sample_mult=sample_mult) - gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * erf(n_sigmas) - area = gaussian_area + abs(amp) * width - target_area = abs(theta) / (np.pi / 2.0) * area - width = (target_area - gaussian_area) / abs(amp) - expected_duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - self.assertEqual(scaled.duration, expected_duration) - - def test_pass_alive_with_dcx_ish(self): - """Test if the pass is not terminated by error with direct CX input.""" - cx_sched = Schedule() - # Fake direct cr - cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) - # Fake direct compensation tone - # Compensation tone doesn't have dedicated pulse class. - # So it's reported as a waveform now. - compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) - cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - - inst_map = InstructionScheduleMap() - inst_map.add("cx", (1, 0), schedule=cx_sched) - - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta, 1, 0) - - pass_ = RZXCalibrationBuilder(instruction_schedule_map=inst_map) - with self.assertWarns(UserWarning): - # User warning that says q0 q1 is invalid - cal_qc = PassManager(pass_).run(rzx_qc) - self.assertEqual(cal_qc, rzx_qc) - - -class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder): - """Test RZXCalibrationBuilderNoEcho.""" - - def test_rzx_calibration_builder(self): - """Test whether RZXCalibrationBuilderNoEcho scales pulses correctly.""" - - # Define a circuit with one RZX gate and an angle theta. - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta / 2, 1, 0) - - # Verify that there are no calibrations for this circuit yet. - self.assertEqual(rzx_qc.calibrations, {}) - - # apply the RZXCalibrationBuilderNoEcho. - pass_ = RZXCalibrationBuilderNoEcho( - instruction_schedule_map=self.backend.defaults().instruction_schedule_map - ) - cal_qc = PassManager(pass_).run(rzx_qc) - rzx_qc_duration = schedule(cal_qc, self.backend).duration - - # Check that the calibrations contain the correct instructions - # and pulses on the correct channels. - rzx_qc_instructions = cal_qc.calibrations["rzx"][((1, 0), (theta / 2,))].instructions - self.assertEqual(rzx_qc_instructions[0][1].channel, DriveChannel(0)) - self.assertTrue(isinstance(rzx_qc_instructions[0][1], Play)) - self.assertEqual(rzx_qc_instructions[0][1].pulse.pulse_type, "GaussianSquare") - self.assertEqual(rzx_qc_instructions[1][1].channel, DriveChannel(1)) - self.assertTrue(isinstance(rzx_qc_instructions[1][1], Delay)) - self.assertEqual(rzx_qc_instructions[2][1].channel, ControlChannel(1)) - self.assertTrue(isinstance(rzx_qc_instructions[2][1], Play)) - self.assertEqual(rzx_qc_instructions[2][1].pulse.pulse_type, "GaussianSquare") - - # Calculate the duration of one scaled Gaussian square pulse from the CX gate. - cx_sched = self.inst_map.get("cx", qubits=(1, 0)) - - crs = [] - for time, inst in cx_sched.instructions: - - # Identify the CR pulses. - if isinstance(inst, Play) and not isinstance(inst, ShiftPhase): - if isinstance(inst.channel, ControlChannel): - crs.append((time, inst)) - - pulse_ = crs[0][1].pulse - amp = pulse_.amp - width = pulse_.width - sigma = pulse_.sigma - n_sigmas = (pulse_.duration - width) / sigma - sample_mult = 16 - - gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * erf(n_sigmas) - area = gaussian_area + abs(amp) * width - target_area = abs(theta) / (np.pi / 2.0) * area - width = (target_area - gaussian_area) / abs(amp) - duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - - # Check whether the durations of the RZX pulse and - # the scaled CR pulse from the CX gate match. - self.assertEqual(rzx_qc_duration, duration) - - def test_pass_alive_with_dcx_ish(self): - """Test if the pass is not terminated by error with direct CX input.""" - cx_sched = Schedule() - # Fake direct cr - cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) - # Fake direct compensation tone - # Compensation tone doesn't have dedicated pulse class. - # So it's reported as a waveform now. - compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) - cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - - inst_map = InstructionScheduleMap() - inst_map.add("cx", (1, 0), schedule=cx_sched) - - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta, 1, 0) - - pass_ = RZXCalibrationBuilderNoEcho(instruction_schedule_map=inst_map) - with self.assertWarns(UserWarning): - # User warning that says q0 q1 is invalid - cal_qc = PassManager(pass_).run(rzx_qc) - self.assertEqual(cal_qc, rzx_qc) From ee394ef2f2451ad14236acde27bf54bf9d06c5cd Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 28 Nov 2022 15:19:59 +0200 Subject: [PATCH 19/24] Remove outdated test. --- test/python/transpiler/test_calibrationbuilder.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index f70c2b942851..d293ce8957b5 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -295,21 +295,6 @@ def test_native_cr(self): 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() From 0cc531b16e195afd31de1d54c344151efecfd52d Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 28 Nov 2022 16:06:47 +0200 Subject: [PATCH 20/24] Lint --- qiskit/qpy/__init__.py | 4 ++-- ...bolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 4f99d56d60da..dd3a33c01a6b 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -105,8 +105,8 @@ Version 6 ========= -Version 6 has an internal update to :ref:`qpy_schedule_symbolic_pulse` loader of `previous` QPY -version to adapt in the latest Qiskit library pulse representation. In Qiskit Terra 0.23 and above, +Version 6 has an internal update to :ref:`qpy_schedule_symbolic_pulse` loader of `previous` QPY +version to adapt in the latest Qiskit library pulse representation. In Qiskit Terra 0.23 and above, complex `amp` value representation is replaced with float (`amp`, `angle`) pair. Because a QPY binary file dumped by the QPY version 5 and below implies the data is still represented by the conventional complex amp format, the loaded pulse parameters diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml index 2998c76d3659..0dfda23c5f7b 100644 --- a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -15,4 +15,3 @@ features: Qiskit Experiments where the amplitude and angle of pulses are calibrated in separate experiments. - | QPY version bumped to 6, to accommodate the symbolic library pulses (amp,angle) representation. - From 048a53b93d95b5cd25e7a769cc6b9610d377eef6 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Tue, 29 Nov 2022 00:06:06 +0200 Subject: [PATCH 21/24] Release notes style --- ...ymbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml index 0dfda23c5f7b..c92a7b26f793 100644 --- a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -2,10 +2,12 @@ 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`. From b69940bcb94352af9cd8a287524d9d95f9a16823 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 30 Nov 2022 00:47:16 +0200 Subject: [PATCH 22/24] Removed QPY version bump in favor of using qiskit terra version as an indicator. --- qiskit/qpy/__init__.py | 12 ----- qiskit/qpy/binary_io/circuits.py | 13 +++-- qiskit/qpy/binary_io/schedules.py | 49 ++++++++++--------- qiskit/qpy/common.py | 2 +- qiskit/qpy/interface.py | 25 +++++----- ...version-to-amp-angle-0c6bcf742eac8945.yaml | 2 - 6 files changed, 51 insertions(+), 52 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index dd3a33c01a6b..f4279a9b73dc 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -100,18 +100,6 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. -.. _qpy_version_6: - -Version 6 -========= - -Version 6 has an internal update to :ref:`qpy_schedule_symbolic_pulse` loader of `previous` QPY -version to adapt in the latest Qiskit library pulse representation. In Qiskit Terra 0.23 and above, -complex `amp` value representation is replaced with float (`amp`, `angle`) pair. -Because a QPY binary file dumped by the QPY version 5 and below implies the data -is still represented by the conventional complex amp format, the loaded pulse parameters -and envelope are immediately converted into new format to instantiate the pulse in new style. - .. _qpy_version_5: Version 5 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index c0430c4327a9..8b4a4029ec1a 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -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): diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 07c63bea0584..91588af37b41 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -77,19 +77,18 @@ def _loads_symbolic_expr(expr_bytes): return expr -def _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters): - # In the transition to Qiskit Terra > 0.22, the representation of library pulses was changed from - # complex "amp" to float "amp" and "angle". To reflect this, QPY version was bumped to 6. The - # existing library pulses in QPY<=5 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. +def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): + # In the transition from Qiskit Terra 0.22.2, the representation of library pulses was changed from + # complex "amp" to float "amp" and "angle". The existing library pulses in those 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: - v5_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] + legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] - if pulse_type in v5_library_pulses: + 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"]) @@ -102,14 +101,14 @@ def _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters): # 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, " - "QPY version <=5) will be converted automatically to float (amp,angle) representation.", + "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): +def _read_symbolic_pulse(file_obj, version, qiskit_version): header = formats.SYMBOLIC_PULSE._make( struct.unpack( formats.SYMBOLIC_PULSE_PACK, @@ -126,8 +125,8 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) - if version <= 5: - envelope = _format_legacy_qiskit_pulse_v5(pulse_type, envelope, parameters) + if qiskit_version <= (0, 22, 2): + 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, {}) @@ -162,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, {}) @@ -293,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: @@ -306,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. @@ -314,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.") @@ -334,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 diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 7ecbbe819353..f20aa2245582 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -21,7 +21,7 @@ from qiskit.qpy import formats -QPY_VERSION = 6 +QPY_VERSION = 5 ENCODE = "utf8" diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 2e764e68bb70..6c15236274d7 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -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 diff --git a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml index c92a7b26f793..aaad0f59b362 100644 --- a/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -15,5 +15,3 @@ features: 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. - - | - QPY version bumped to 6, to accommodate the symbolic library pulses (amp,angle) representation. From 55262a20e1b4a25d415556a68f88611ae12b2595 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 30 Nov 2022 09:05:17 +0200 Subject: [PATCH 23/24] bug fix --- qiskit/qpy/binary_io/schedules.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 91588af37b41..c27620335a9b 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -78,10 +78,10 @@ def _loads_symbolic_expr(expr_bytes): def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): - # In the transition from Qiskit Terra 0.22.2, the representation of library pulses was changed from - # complex "amp" to float "amp" and "angle". The existing library pulses in those 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. + # 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 @@ -125,7 +125,7 @@ def _read_symbolic_pulse(file_obj, version, qiskit_version): version=version, vectors={}, ) - if qiskit_version <= (0, 22, 2): + if qiskit_version <= (0, 23, 0): envelope = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) # Note that parameters is mutated during the function call From fb07443269f69683079486cd6055b1412234daed Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 30 Nov 2022 10:19:23 +0200 Subject: [PATCH 24/24] bug fix --- qiskit/qpy/binary_io/schedules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index c27620335a9b..247f8f0797cb 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -125,7 +125,7 @@ def _read_symbolic_pulse(file_obj, version, qiskit_version): version=version, vectors={}, ) - if qiskit_version <= (0, 23, 0): + if qiskit_version < (0, 23, 0): envelope = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) # Note that parameters is mutated during the function call