diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 4f1aa909f0e0..713265c47a46 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 @@ -614,9 +607,10 @@ class Gaussian(metaclass=_PulseType): .. math:: f'(x) &= \exp\Bigl( -\frac12 \frac{{(x - \text{duration}/2)}^2}{\text{sigma}^2} \Bigr)\\ - f(x) &= \text{amp} \times \frac{f'(x) - f'(-1)}{1-f'(-1)}, \quad 0 \le x < \text{duration} + f(x) &= \text{A} \times \frac{f'(x) - f'(-1)}{1-f'(-1)}, \quad 0 \le x < \text{duration} - where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling. + where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling, and + :math:`\text{A} = \text{amp} \times \exp\left(i\times\text{angle}\right)`. """ alias = "Gaussian" @@ -624,8 +618,9 @@ class Gaussian(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, ParameterExpression], + amp: Union[complex, float, ParameterExpression], sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> SymbolicPulse: @@ -633,23 +628,45 @@ 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. Returns: SymbolicPulse instance. + + Raises: + PulseError: If both complex amp and angle are provided as arguments. """ - parameters = {"amp": amp, "sigma": sigma} + # This should be removed once complex amp support is deprecated. + if isinstance(amp, complex): + if angle is None: + warnings.warn( + "Complex amp will be deprecated. " + "Use float amp (for the magnitude) and float angle instead.", + PendingDeprecationWarning, + ) + else: + raise PulseError("amp can't be complex when providing angle") + + if angle is None: + angle = 0 + + parameters = {"amp": amp, "sigma": sigma, "angle": angle} # Prepare symbolic expressions - _t, _duration, _amp, _sigma = sym.symbols("t, duration, amp, sigma") + _t, _duration, _amp, _sigma, _angle = sym.symbols("t, duration, amp, sigma, angle") _center = _duration / 2 - envelope_expr = _amp * _lifted_gaussian(_t, _center, _duration + 1, _sigma) + envelope_expr = ( + _amp * sym.exp(sym.I * _angle) * _lifted_gaussian(_t, _center, _duration + 1, _sigma) + ) + consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -700,10 +717,11 @@ class GaussianSquare(metaclass=_PulseType): \\biggr)\ & \\text{risefall} + \\text{width} \\le x\ \\end{cases}\\\\ - f(x) &= \\text{amp} \\times \\frac{f'(x) - f'(-1)}{1-f'(-1)},\ + f(x) &= \\text{A} \\times \\frac{f'(x) - f'(-1)}{1-f'(-1)},\ \\quad 0 \\le x < \\text{duration} - where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling. + where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling, and + :math:`\\text{A} = \\text{amp} \\times \\exp\\left(i\\times\\text{angle}\\right)`. """ alias = "GaussianSquare" @@ -711,9 +729,10 @@ class GaussianSquare(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, ParameterExpression], + amp: Union[complex, float, ParameterExpression], sigma: Union[float, ParameterExpression], width: Optional[Union[float, ParameterExpression]] = None, + angle: Optional[Union[float, ParameterExpression]] = None, risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, @@ -722,10 +741,12 @@ 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 @@ -736,6 +757,7 @@ def __new__( Raises: PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. + PulseError: If both complex amp and angle are provided as arguments. """ # Convert risefall_sigma_ratio into width which is defined in OpenPulse spec if width is None and risefall_sigma_ratio is None: @@ -750,10 +772,26 @@ def __new__( if width is None and risefall_sigma_ratio is not None: width = duration - 2.0 * risefall_sigma_ratio * sigma - parameters = {"amp": amp, "sigma": sigma, "width": width} + # This should be removed once complex amp support is deprecated. + if isinstance(amp, complex): + if angle is None: + warnings.warn( + "Complex amp will be deprecated. " + "Use float amp (for the magnitude) and float angle instead.", + PendingDeprecationWarning, + ) + else: + raise PulseError("amp can't be complex when providing angle") + + if angle is None: + angle = 0 + + parameters = {"amp": amp, "sigma": sigma, "width": width, "angle": angle} # Prepare symbolic expressions - _t, _duration, _amp, _sigma, _width = sym.symbols("t, duration, amp, sigma, width") + _t, _duration, _amp, _sigma, _width, _angle = sym.symbols( + "t, duration, amp, sigma, width, angle" + ) _center = _duration / 2 _sq_t0 = _center - _width / 2 @@ -762,9 +800,14 @@ def __new__( _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration + 1, _sigma) - envelope_expr = _amp * sym.Piecewise( - (_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True) + envelope_expr = ( + _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise( + (_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True) + ) ) + consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 @@ -795,13 +838,14 @@ class Drag(metaclass=_PulseType): .. math:: g(x) &= \\exp\\Bigl(-\\frac12 \\frac{(x - \\text{duration}/2)^2}{\\text{sigma}^2}\\Bigr)\\\\ - g'(x) &= \\text{amp}\\times\\frac{g(x)-g(-1)}{1-g(-1)}\\\\ + g'(x) &= \\text{A}\\times\\frac{g(x)-g(-1)}{1-g(-1)}\\\\ f(x) &= g'(x) \\times \\Bigl(1 + 1j \\times \\text{beta} \\times\ \\Bigl(-\\frac{x - \\text{duration}/2}{\\text{sigma}^2}\\Bigr) \\Bigr), \\quad 0 \\le x < \\text{duration} - where :math:`g(x)` is a standard unlifted Gaussian waveform and - :math:`g'(x)` is the lifted :class:`~qiskit.pulse.library.Gaussian` waveform. + where :math:`g(x)` is a standard unlifted Gaussian waveform, :math:`g'(x)` is the lifted + :class:`~qiskit.pulse.library.Gaussian` waveform, and + :math:`\\text{A} = \\text{amp} \\times \\exp\\left(i\\times\\text{angle}\\right)`. References: 1. |citation1|_ @@ -825,9 +869,10 @@ class Drag(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, ParameterExpression], + amp: Union[complex, float, ParameterExpression], sigma: Union[float, ParameterExpression], beta: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> SymbolicPulse: @@ -835,27 +880,48 @@ 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) + envelope_expr = _amp * sym.exp(sym.I * _angle) * (_gauss + sym.I * _beta * _deriv) consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) @@ -880,7 +946,7 @@ class Constant(metaclass=_PulseType): .. math:: - f(x) = amp , 0 <= x < duration + f(x) = \\text{amp}\\times\\exp\\left(i\\text{angle}\\right) , 0 <= x < duration f(x) = 0 , elsewhere """ @@ -889,7 +955,8 @@ class Constant(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, ParameterExpression], + amp: Union[complex, float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> SymbolicPulse: @@ -897,18 +964,37 @@ 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 @@ -917,7 +1003,12 @@ def __new__( # ParametricPulse.get_waveform(). # # See: https://github.com/sympy/sympy/issues/5642 - envelope_expr = _amp * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True)) + envelope_expr = ( + _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True)) + ) + valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 instance = SymbolicPulse( diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 330df026ee23..d1db89ab1bed 100644 --- a/qiskit/pulse/parameter_manager.py +++ b/qiskit/pulse/parameter_manager.py @@ -231,11 +231,6 @@ def visit_SymbolicPulse(self, node: SymbolicPulse): pval = node._params[name] if isinstance(pval, ParameterExpression): new_val = self._assign_parameter_expression(pval) - if name == "amp" and not isinstance(new_val, ParameterExpression): - # This is due to an odd behavior of IBM Quantum backends. - # When the amplitude is given as a float, then job execution is - # terminated with an error. - new_val = complex(new_val) node._params[name] = new_val node.validate_parameters() diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 1ea4d19123a3..55480367dcd4 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,18 @@ def convert_play(self, shift, instruction): dict: Dictionary of required parameters. """ if isinstance(instruction.pulse, (library.ParametricPulse, library.SymbolicPulse)): + params = dict(instruction.pulse.parameters) + # IBM backends expect "amp" to be the complex amplitude + if "amp" in params and "angle" in params: + params["amp"] = complex(params["amp"] * np.exp(1j * params["angle"])) + del params["angle"] + command_dict = { "name": "parametric_pulse", "pulse_shape": ParametricPulseShapes.from_instance(instruction.pulse).name, "t0": shift + instruction.start_time, "ch": instruction.channel.name, - "parameters": instruction.pulse.parameters, + "parameters": params, } else: command_dict = { @@ -723,10 +730,12 @@ def convert_parametric(self, instruction): ) short_pulse_id = hashlib.md5(base_str.encode("utf-8")).hexdigest()[:4] pulse_name = f"{instruction.pulse_shape}_{short_pulse_id}" + params = dict(instruction.parameters) + if "amp" in params and isinstance(params["amp"], complex): + params["angle"] = np.angle(params["amp"]) + params["amp"] = np.abs(params["amp"]) - pulse = ParametricPulseShapes.to_type(instruction.pulse_shape)( - **instruction.parameters, name=pulse_name - ) + pulse = ParametricPulseShapes.to_type(instruction.pulse_shape)(**params, name=pulse_name) return instructions.Play(pulse, channel) << t0 @bind_name("snapshot") 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 223e2e77b4d8..247f8f0797cb 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,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) @@ -71,7 +77,38 @@ def _loads_symbolic_expr(expr_bytes): return expr -def _read_symbolic_pulse(file_obj, version): +def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): + # In the transition to Qiskit Terra 0.23, the representation of library pulses was changed from + # complex "amp" to float "amp" and "angle". The existing library pulses in previous versions are + # handled here separately to conform with the new representation. To avoid role assumption for + # "amp" for custom pulses, only the library pulses are handled this way. + + # Note that parameters is mutated during the function call + + # List of pulses in the library in QPY version 5 and below: + legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] + + if pulse_type in legacy_library_pulses: + # Once complex amp support will be deprecated we will need: + # parameters["angle"] = np.angle(parameters["amp"]) + # parameters["amp"] = np.abs(parameters["amp"]) + + # In the meanwhile we simply add: + parameters["angle"] = 0 + _amp, _angle = sym.symbols("amp, angle") + envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle)) + + # And warn that this will change in future releases: + warnings.warn( + "Complex amp support for symbolic library pulses will be deprecated. " + "Once deprecated, library pulses loaded from old QPY files (Terra version <=0.22.2)," + " will be converted automatically to float (amp,angle) representation.", + PendingDeprecationWarning, + ) + return envelope + + +def _read_symbolic_pulse(file_obj, version, qiskit_version): header = formats.SYMBOLIC_PULSE._make( struct.unpack( formats.SYMBOLIC_PULSE_PACK, @@ -88,6 +125,10 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) + if qiskit_version < (0, 23, 0): + envelope = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) + # Note that parameters is mutated during the function call + duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) @@ -120,27 +161,29 @@ def _read_alignment_context(file_obj, version): return instance -def _loads_operand(type_key, data_bytes, version): +def _loads_operand(type_key, data_bytes, version, qiskit_version): if type_key == type_keys.ScheduleOperand.WAVEFORM: return common.data_from_binary(data_bytes, _read_waveform, version=version) if type_key == type_keys.ScheduleOperand.SYMBOLIC_PULSE: - return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) + return common.data_from_binary( + data_bytes, _read_symbolic_pulse, version=version, qiskit_version=qiskit_version + ) if type_key == type_keys.ScheduleOperand.CHANNEL: return common.data_from_binary(data_bytes, _read_channel, version=version) return value.loads_value(type_key, data_bytes, version, {}) -def _read_element(file_obj, version, metadata_deserializer): +def _read_element(file_obj, version, metadata_deserializer, qiskit_version=None): type_key = common.read_type_key(file_obj) if type_key == type_keys.Program.SCHEDULE_BLOCK: - return read_schedule_block(file_obj, version, metadata_deserializer) + return read_schedule_block( + file_obj, version, metadata_deserializer, qiskit_version=qiskit_version + ) operands = common.read_sequence( - file_obj, - deserializer=_loads_operand, - version=version, + file_obj, deserializer=_loads_operand, version=version, qiskit_version=qiskit_version ) name = value.read_value(file_obj, version, {}) @@ -251,7 +294,7 @@ def _write_element(file_obj, element, metadata_serializer): value.write_value(file_obj, element.name) -def read_schedule_block(file_obj, version, metadata_deserializer=None): +def read_schedule_block(file_obj, version, metadata_deserializer=None, qiskit_version=None): """Read a single ScheduleBlock from the file like object. Args: @@ -264,6 +307,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): in the file-like object. If this is not specified the circuit metadata will be parsed as JSON with the stdlib ``json.load()`` function using the default ``JSONDecoder`` class. + qiskit_version (tuple): tuple with major, minor and patch versions of qiskit. Returns: ScheduleBlock: The schedule block object from the file. @@ -272,6 +316,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): TypeError: If any of the instructions is invalid data format. QiskitError: QPY version is earlier than block support. """ + if version < 5: QiskitError(f"QPY version {version} does not support ScheduleBlock.") @@ -292,7 +337,9 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): alignment_context=context, ) for _ in range(data.num_elements): - block_elm = _read_element(file_obj, version, metadata_deserializer) + block_elm = _read_element( + file_obj, version, metadata_deserializer, qiskit_version=qiskit_version + ) block.append(block_elm, inplace=True) return block 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 new file mode 100644 index 000000000000..aaad0f59b362 --- /dev/null +++ b/releasenotes/notes/Symbolic-Pulses-conversion-to-amp-angle-0c6bcf742eac8945.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + The pulses in the Qiskit Pulse library + + * :class:`~qiskit.pulse.library.Gaussian` + * :class:`~qiskit.pulse.library.GaussianSquare` + * :class:`~qiskit.pulse.library.Drag` + * :class:`~qiskit.pulse.library.Constant` + + can be initialized with new parameter angle, such that two float parameters could be provided - `amp`,`angle`. + Initialization with complex `amp` will be supported until it will be deprecated in future version. However, + Providing complex `amp` with a finite `angle` will result in `PulseError`. + For example, instead of calling `Gaussian(duration=100,sigma=20,amp=0.5j)` one + should use `Gaussian(duration=100,sigma=20,amp=0.5,angle=np.pi/2)`. The pulse envelope which used to be + defined as `amp * ...` is in turn defined as `amp * exp(1j * angle) * ...`. This change aims to better support + Qiskit Experiments where the amplitude and angle of pulses are calibrated in separate experiments. diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index ab5fe33a67ad..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_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 44e892cc0a27..428d8094a2f7 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -121,6 +121,29 @@ 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, + and that pulses are equivalent.""" + + # Test deprecation warnings and errors: + with self.assertWarns(PendingDeprecationWarning): + Gaussian(duration=25, sigma=4, amp=0.5j) + with self.assertWarns(PendingDeprecationWarning): + GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100) + with self.assertRaises(PulseError): + Gaussian(duration=25, sigma=4, amp=0.5j, angle=1) + with self.assertRaises(PulseError): + GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100, angle=0.1) + + # Test that new and old API pulses are the same: + gauss_pulse_complex_amp = Gaussian(duration=25, sigma=4, amp=0.5j) + gauss_pulse_amp_angle = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2) + np.testing.assert_almost_equal( + gauss_pulse_amp_angle.get_waveform().samples, + gauss_pulse_complex_amp.get_waveform().samples, + ) + def test_gaussian_pulse(self): """Test that Gaussian sample pulse matches the pulse library.""" gauss = Gaussian(duration=25, sigma=4, amp=0.5j) @@ -226,32 +249,35 @@ 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..95f34aa17d2c 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) @@ -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 + 0.2j, name="pulse1"), DriveChannel(0) + Gaussian(duration=25, sigma=15, amp=0.5, angle=np.pi / 2, name="pulse1"), + DriveChannel(0), ) qobj = PulseQobjInstruction( name="parametric_pulse", @@ -208,12 +209,12 @@ 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 ea5dac578cc2..88451917440d 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -219,11 +219,11 @@ 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), ControlChannel(0)) builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) # Measure with builder.align_left(): 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() 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, 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))