From bd2c463bab171990bf9d9b744513314f60272dc8 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Wed, 21 Dec 2022 01:34:41 +0200 Subject: [PATCH 01/20] Introduced ScalableSymbolicPulses to correctly compare pulses with amp,angle representation. --- qiskit/pulse/library/__init__.py | 9 ++- qiskit/pulse/library/symbolic_pulses.py | 91 ++++++++++++++++++++++--- qiskit/qpy/binary_io/schedules.py | 38 ++++++++--- qiskit/qpy/formats.py | 15 ++++ test/python/pulse/test_pulse_lib.py | 50 ++++++++++---- 5 files changed, 169 insertions(+), 34 deletions(-) diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index e7050dd1949b..6bc3fd92984d 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -111,6 +111,13 @@ drag, ) from .parametric_pulses import ParametricPulse -from .symbolic_pulses import SymbolicPulse, Gaussian, GaussianSquare, Drag, Constant +from .symbolic_pulses import ( + SymbolicPulse, + ScalableSymbolicPulse, + Gaussian, + GaussianSquare, + Drag, + Constant, +) from .pulse import Pulse from .waveform import Waveform diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 713265c47a46..4758c880eae8 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -547,8 +547,11 @@ def __eq__(self, other: "SymbolicPulse") -> bool: if self._envelope != other._envelope: return False - if self.parameters != other.parameters: - return False + # SymbolicPulse requires parameters to be identical, but subclasses of SymbolicPulse + # Have different conditions. + if self.__class__ == SymbolicPulse: + if self.parameters != other.parameters: + return False return True @@ -568,6 +571,74 @@ def __repr__(self) -> str: ) +class ScalableSymbolicPulse(SymbolicPulse): + r"""Subclass of :class:`SymbolicPulse` for pulses with scalable envelope. + + Instance of :class:`ScalableSymbolicPulse` behaves the same as an instance of + :class:`SymbolicPulse`, but its envelope is assumed to have a scalable form + :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)\times\text{F} + \left(t,\text{parameters}\right)`, + where :math:`\text{F}` is some function describing the rest of the envelope. + Parameters `amp` and `angle` are assumed to appear in :attr:`SymbolicPulse.parameters`. + + When two :class:`ScalableSymbolicPulse` objects are equated, instead of comparing + `amp` and `angle` individually, only the complex amplitude + :math:'\text{amp}\times\exp\left(i\times\text{angle}\right)' is compared. + """ + + def __init__( + self, + pulse_type: str, + duration: Union[ParameterExpression, int], + parameters: Optional[Dict[str, Union[ParameterExpression, complex]]] = None, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, + envelope: Optional[sym.Expr] = None, + constraints: Optional[sym.Expr] = None, + valid_amp_conditions: Optional[sym.Expr] = None, + ): + if "amp" not in parameters or "angle" not in parameters: + raise PulseError("ScalableSymbolicPulse must have parameters amp and angle") + + super().__init__( + pulse_type=pulse_type, + duration=duration, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope, + constraints=constraints, + valid_amp_conditions=valid_amp_conditions, + ) + + def __eq__(self, other: "ScalableSymbolicPulse") -> bool: + symbolic_pulse_comparison = super().__eq__(other) + if symbolic_pulse_comparison is not True: + return symbolic_pulse_comparison + + complex_amp1 = self.amp * np.exp(1j * self.angle) + complex_amp2 = other.amp * np.exp(1j * other.angle) + + if isinstance(complex_amp1, ParameterExpression) or isinstance( + complex_amp2, ParameterExpression + ): + if complex_amp1 != complex_amp2: + return False + else: + if not np.isclose(complex_amp1, complex_amp2): + return False + + for key in self.parameters: + if key not in ["amp", "angle"] and self.parameters[key] != other.parameters[key]: + return False + + return True + + # When __eq__ is modified, __hash__ is automatically set to None, and thus needs to be defined + # explicitly. + __hash__ = SymbolicPulse.__hash__ + + class _PulseType(type): """Metaclass to warn at isinstance check.""" @@ -638,7 +709,7 @@ def __new__( waveform to 1. The default is ``True`` and the amplitude is constrained to 1. Returns: - SymbolicPulse instance. + ScalableSymbolicPulse instance. Raises: PulseError: If both complex amp and angle are provided as arguments. @@ -670,7 +741,7 @@ def __new__( consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - instance = SymbolicPulse( + instance = ScalableSymbolicPulse( pulse_type=cls.alias, duration=duration, parameters=parameters, @@ -753,7 +824,7 @@ def __new__( waveform to 1. The default is ``True`` and the amplitude is constrained to 1. Returns: - SymbolicPulse instance. + ScalableSymbolicPulse instance. Raises: PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. @@ -811,7 +882,7 @@ def __new__( consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - instance = SymbolicPulse( + instance = ScalableSymbolicPulse( pulse_type=cls.alias, duration=duration, parameters=parameters, @@ -891,7 +962,7 @@ def __new__( waveform to 1. The default is ``True`` and the amplitude is constrained to 1. Returns: - SymbolicPulse instance. + ScalableSymbolicPulse instance. Raises: PulseError: If both complex amp and angle are provided as arguments. @@ -926,7 +997,7 @@ def __new__( consts_expr = _sigma > 0 valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) - instance = SymbolicPulse( + instance = ScalableSymbolicPulse( pulse_type="Drag", duration=duration, parameters=parameters, @@ -972,7 +1043,7 @@ def __new__( waveform to 1. The default is ``True`` and the amplitude is constrained to 1. Returns: - SymbolicPulse instance. + ScalableSymbolicPulse instance. Raises: PulseError: If both complex amp and angle are provided as arguments. @@ -1011,7 +1082,7 @@ def __new__( valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - instance = SymbolicPulse( + instance = ScalableSymbolicPulse( pulse_type="Constant", duration=duration, parameters=parameters, diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 247f8f0797cb..bfdce37f25f2 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -87,6 +87,7 @@ def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): # List of pulses in the library in QPY version 5 and below: legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] + is_scalable = False if pulse_type in legacy_library_pulses: # Once complex amp support will be deprecated we will need: @@ -101,20 +102,32 @@ def _format_legacy_qiskit_pulse(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.2)," + "Once deprecated, library pulses loaded from old QPY files (Terra version < 0.23)," " will be converted automatically to float (amp,angle) representation.", PendingDeprecationWarning, ) - return envelope + is_scalable = True + + return envelope, is_scalable def _read_symbolic_pulse(file_obj, version, qiskit_version): - header = formats.SYMBOLIC_PULSE._make( + if qiskit_version < (0, 23, 0): + make = formats.SYMBOLIC_PULSE._make + pack = formats.SYMBOLIC_PULSE_PACK + size = formats.SYMBOLIC_PULSE_SIZE + else: + make = formats.SYMBOLIC_PULSE_V2._make + pack = formats.SYMBOLIC_PULSE_PACK_V2 + size = formats.SYMBOLIC_PULSE_SIZE_V2 + + header = make( struct.unpack( - formats.SYMBOLIC_PULSE_PACK, - file_obj.read(formats.SYMBOLIC_PULSE_SIZE), + pack, + file_obj.read(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)) @@ -126,13 +139,20 @@ def _read_symbolic_pulse(file_obj, version, qiskit_version): vectors={}, ) if qiskit_version < (0, 23, 0): - envelope = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) + envelope, is_scalable = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) # Note that parameters is mutated during the function call + else: + is_scalable = header.is_scalable duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) - return library.SymbolicPulse( + if is_scalable: + pulse_generator = library.ScalableSymbolicPulse + else: + pulse_generator = library.SymbolicPulse + + return pulse_generator( pulse_type=pulse_type, duration=duration, parameters=parameters, @@ -230,14 +250,16 @@ def _write_symbolic_pulse(file_obj, data): envelope_bytes = _dumps_symbolic_expr(data.envelope) constraints_bytes = _dumps_symbolic_expr(data.constraints) valid_amp_conditions_bytes = _dumps_symbolic_expr(data.valid_amp_conditions) + is_scalable = isinstance(data, library.ScalableSymbolicPulse) header_bytes = struct.pack( - formats.SYMBOLIC_PULSE_PACK, + formats.SYMBOLIC_PULSE_PACK_V2, len(pulse_type_bytes), len(envelope_bytes), len(constraints_bytes), len(valid_amp_conditions_bytes), data._limit_amplitude, + is_scalable, ) file_obj.write(header_bytes) file_obj.write(pulse_type_bytes) diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 07e121f67705..35469039c67a 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -195,6 +195,21 @@ SYMBOLIC_PULSE_PACK = "!HHHH?" SYMBOLIC_PULSE_SIZE = struct.calcsize(SYMBOLIC_PULSE_PACK) +# SYMBOLIC_PULSE_V2 +SYMBOLIC_PULSE_V2 = namedtuple( + "SYMBOLIC_PULSE", + [ + "type_size", + "envelope_size", + "constraints_size", + "valid_amp_conditions_size", + "amp_limited", + "is_scalable", + ], +) +SYMBOLIC_PULSE_PACK_V2 = "!HHHH??" +SYMBOLIC_PULSE_SIZE_V2 = struct.calcsize(SYMBOLIC_PULSE_PACK_V2) + # INSTRUCTION_PARAM INSTRUCTION_PARAM = namedtuple("INSTRUCTION_PARAM", ["type", "size"]) INSTRUCTION_PARAM_PACK = "!1cQ" diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 428d8094a2f7..7684aa34b22a 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -19,6 +19,7 @@ from qiskit.circuit import Parameter from qiskit.pulse.library import ( SymbolicPulse, + ScalableSymbolicPulse, Waveform, Constant, Gaussian, @@ -444,21 +445,6 @@ def test_custom_pulse(self): reference = np.concatenate([-0.1 * np.ones(30), 0.1j * np.ones(50), -0.1 * np.ones(20)]) np.testing.assert_array_almost_equal(waveform.samples, reference) - def test_no_subclass(self): - """Test no dedicated pulse subclass is created.""" - - gaussian_pulse = Gaussian(160, 0.1, 40) - self.assertIs(type(gaussian_pulse), SymbolicPulse) - - gaussian_square_pulse = GaussianSquare(800, 0.1, 64, 544) - self.assertIs(type(gaussian_square_pulse), SymbolicPulse) - - drag_pulse = Drag(160, 0.1, 40, 1.5) - self.assertIs(type(drag_pulse), SymbolicPulse) - - constant_pulse = Constant(800, 0.1) - self.assertIs(type(constant_pulse), SymbolicPulse) - def test_gaussian_deprecated_type_check(self): """Test isinstance check works with deprecation.""" gaussian_pulse = Gaussian(160, 0.1, 40) @@ -543,5 +529,39 @@ def local_gaussian(duration, amp, t0, sig): self.assertEqual(len(pulse_wf_inst.samples), _duration) +class TestScalableSymbolicPulse(QiskitTestCase): + """ScalableSymbolicPulse tests""" + + # pylint: disable=invalid-name + def test_amp_angle_verification(self): + """Test defining a custom pulse with no amp or angle""" + t, t1, t2, amp1, amp2 = sym.symbols("t, t1, t2, amp1, amp2") + envelope = sym.Piecewise((amp1, sym.And(t > t1, t < t2)), (amp2, sym.true)) + with self.assertRaises(PulseError): + ScalableSymbolicPulse( + pulse_type="Custom", + duration=100, + parameters={"t1": 30, "t2": 80, "amp1": 0.1j, "amp2": 0.1j}, + envelope=envelope, + ) + + def test_scalable_comparison(self): + """Test equating of pulses with comparison_parameters.""" + # amp,angle comparison + gaussian_negamp = Gaussian(duration=25, sigma=4, amp=-0.5, angle=0) + gaussian_piphase = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi) + self.assertEqual(gaussian_negamp, gaussian_piphase) + + # Parameterized library pulses + amp = Parameter("amp") + gaussian1 = Gaussian(duration=25, sigma=4, amp=amp, angle=0) + gaussian2 = Gaussian(duration=25, sigma=4, amp=amp, angle=0) + self.assertEqual(gaussian1, gaussian2) + + # pulses with different parameters + gaussian1._params["sigma"] = 10 + self.assertNotEqual(gaussian1, gaussian2) + + if __name__ == "__main__": unittest.main() From 019057dd431aadff1fadbd7a2714175968640167 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 12:05:29 +0200 Subject: [PATCH 02/20] Modified ScalableSymbolicPulse, bumped QPY version to 6. --- qiskit/pulse/library/symbolic_pulses.py | 159 ++++++++---------- qiskit/qpy/__init__.py | 10 ++ qiskit/qpy/binary_io/circuits.py | 13 +- qiskit/qpy/binary_io/schedules.py | 86 +++++----- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 4 +- qiskit/qpy/interface.py | 1 - .../new_pulse_subclass-44da774612699312.yaml | 18 ++ test/python/pulse/test_pulse_lib.py | 26 +-- 9 files changed, 159 insertions(+), 160 deletions(-) create mode 100644 releasenotes/notes/new_pulse_subclass-44da774612699312.yaml diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 4758c880eae8..43c6374a66e5 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 copy import deepcopy import numpy as np @@ -547,11 +548,8 @@ def __eq__(self, other: "SymbolicPulse") -> bool: if self._envelope != other._envelope: return False - # SymbolicPulse requires parameters to be identical, but subclasses of SymbolicPulse - # Have different conditions. - if self.__class__ == SymbolicPulse: - if self.parameters != other.parameters: - return False + if self.parameters != other.parameters: + return False return True @@ -578,8 +576,10 @@ class ScalableSymbolicPulse(SymbolicPulse): :class:`SymbolicPulse`, but its envelope is assumed to have a scalable form :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)\times\text{F} \left(t,\text{parameters}\right)`, - where :math:`\text{F}` is some function describing the rest of the envelope. - Parameters `amp` and `angle` are assumed to appear in :attr:`SymbolicPulse.parameters`. + where :math:`\text{F}` is some function describing the rest of the envelope, + and both `amp` and `angle` are real (float). Note that both `amp` and `angle` are + stored in the :attr:`parameters` dictionary of the :class:`ScalableSymbolicPulse` + instance. When two :class:`ScalableSymbolicPulse` objects are equated, instead of comparing `amp` and `angle` individually, only the complex amplitude @@ -590,6 +590,8 @@ def __init__( self, pulse_type: str, duration: Union[ParameterExpression, int], + amp: Union[ParameterExpression, float, complex], + angle: Union[ParameterExpression, float], parameters: Optional[Dict[str, Union[ParameterExpression, complex]]] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, @@ -597,8 +599,50 @@ def __init__( constraints: Optional[sym.Expr] = None, valid_amp_conditions: Optional[sym.Expr] = None, ): - if "amp" not in parameters or "angle" not in parameters: - raise PulseError("ScalableSymbolicPulse must have parameters amp and angle") + """Create a scalable symbolic pulse. + + Args: + pulse_type: Display name of this pulse shape. + duration: Duration of pulse. + amp: The magnitude of the complex amplitude of the pulse. + angle: The phase of the complex amplitude of the pulse. + parameters: Dictionary of pulse parameters that defines the pulse envelope. + name: Display name for this particular pulse envelope. + limit_amplitude: If ``True``, then limit the absolute value of the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + envelope: Pulse envelope expression. + constraints: Pulse parameter constraint expression. + valid_amp_conditions: Extra conditions to skip a full-waveform check for the + amplitude limit. If this condition is not met, then the validation routine + will investigate the full-waveform and raise an error when the amplitude norm + of any data point exceeds 1.0. If not provided, the validation always + creates a full-waveform. + + Raises: + PulseError: When not all parameters are listed in the attribute :attr:`PARAM_DEF`. + PulseError: If both `amp` is complex and `angle` is not `None`. + + """ + # 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 with angle!=None") + + if angle is None: + angle = 0 + + if not isinstance(parameters, Dict): + parameters = {"amp": amp, "angle": angle} + else: + parameters = deepcopy(parameters) + parameters["amp"] = amp + parameters["angle"] = angle super().__init__( pulse_type=pulse_type, @@ -611,10 +655,16 @@ def __init__( valid_amp_conditions=valid_amp_conditions, ) + # pylint: disable=too-many-return-statements def __eq__(self, other: "ScalableSymbolicPulse") -> bool: - symbolic_pulse_comparison = super().__eq__(other) - if symbolic_pulse_comparison is not True: - return symbolic_pulse_comparison + if not isinstance(other, ScalableSymbolicPulse): + return NotImplemented + + if self._pulse_type != other._pulse_type: + return False + + if self._envelope != other._envelope: + return False complex_amp1 = self.amp * np.exp(1j * self.angle) complex_amp2 = other.amp * np.exp(1j * other.angle) @@ -634,7 +684,7 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: return True - # When __eq__ is modified, __hash__ is automatically set to None, and thus needs to be defined + # When __eq__ is modified, __hash__ is automatically set to None, and thus needs to be inherited # explicitly. __hash__ = SymbolicPulse.__hash__ @@ -710,25 +760,8 @@ def __new__( Returns: ScalableSymbolicPulse 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, - ) - else: - raise PulseError("amp can't be complex when providing angle") - - if angle is None: - angle = 0 - - parameters = {"amp": amp, "sigma": sigma, "angle": angle} + parameters = {"sigma": sigma} # Prepare symbolic expressions _t, _duration, _amp, _sigma, _angle = sym.symbols("t, duration, amp, sigma, angle") @@ -744,6 +777,8 @@ def __new__( instance = ScalableSymbolicPulse( pulse_type=cls.alias, duration=duration, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, @@ -828,7 +863,6 @@ 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: @@ -843,21 +877,7 @@ def __new__( if width is None and risefall_sigma_ratio is not None: width = duration - 2.0 * risefall_sigma_ratio * 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, "width": width, "angle": angle} + parameters = {"sigma": sigma, "width": width} # Prepare symbolic expressions _t, _duration, _amp, _sigma, _width, _angle = sym.symbols( @@ -885,6 +905,8 @@ def __new__( instance = ScalableSymbolicPulse( pulse_type=cls.alias, duration=duration, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, @@ -963,25 +985,8 @@ def __new__( Returns: ScalableSymbolicPulse 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, - ) - 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} + parameters = {"sigma": sigma, "beta": beta} # Prepare symbolic expressions _t, _duration, _amp, _sigma, _beta, _angle = sym.symbols( @@ -1000,6 +1005,8 @@ def __new__( instance = ScalableSymbolicPulse( pulse_type="Drag", duration=duration, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, @@ -1044,26 +1051,7 @@ def __new__( Returns: ScalableSymbolicPulse 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, - ) - 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, _angle = sym.symbols("t, amp, duration, angle") @@ -1085,7 +1073,8 @@ def __new__( instance = ScalableSymbolicPulse( pulse_type="Constant", duration=duration, - parameters=parameters, + amp=amp, + angle=angle, name=name, limit_amplitude=limit_amplitude, envelope=envelope_expr, diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 8366d2b1a321..ca37e4e25033 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -126,6 +126,16 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_6: + +Version 6 +========= + +Version 6 adds support for :class:`.~ScalableSymbolicPulse`. These objects are saved and read +like `SymbolicPulse` objects, and the class name is added to the meta-data to correctly handle +the class selection. + + .. _qpy_version_5: Version 5 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 8b4a4029ec1a..c0430c4327a9 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, qiskit_version=None): +def _read_calibrations(file_obj, version, vectors, metadata_deserializer): calibrations = {} header = formats.CALIBRATION._make( @@ -452,9 +452,7 @@ def _read_calibrations(file_obj, version, vectors, metadata_deserializer, qiskit 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, qiskit_version=qiskit_version - ) + schedule = schedules.read_schedule_block(file_obj, version, metadata_deserializer) if name not in calibrations: calibrations[name] = {(qubits, params): schedule} @@ -813,7 +811,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, qiskit_version=None): +def read_circuit(file_obj, version, metadata_deserializer=None): """Read a single QuantumCircuit object from the file like object. Args: @@ -826,7 +824,6 @@ def read_circuit(file_obj, version, metadata_deserializer=None, qiskit_version=N 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. @@ -877,9 +874,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, qiskit_version=N # Read calibrations if version >= 5: - circ.calibrations = _read_calibrations( - file_obj, version, vectors, metadata_deserializer, qiskit_version=qiskit_version - ) + circ.calibrations = _read_calibrations(file_obj, version, vectors, metadata_deserializer) 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 bfdce37f25f2..76215e633a79 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -87,7 +87,7 @@ def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): # List of pulses in the library in QPY version 5 and below: legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] - is_scalable = False + class_name = "SymbolicPulse" if pulse_type in legacy_library_pulses: # Once complex amp support will be deprecated we will need: @@ -106,13 +106,13 @@ def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): " will be converted automatically to float (amp,angle) representation.", PendingDeprecationWarning, ) - is_scalable = True + class_name = "ScalableSymbolicPulse" - return envelope, is_scalable + return envelope, class_name -def _read_symbolic_pulse(file_obj, version, qiskit_version): - if qiskit_version < (0, 23, 0): +def _read_symbolic_pulse(file_obj, version): + if version < 6: make = formats.SYMBOLIC_PULSE._make pack = formats.SYMBOLIC_PULSE_PACK size = formats.SYMBOLIC_PULSE_SIZE @@ -127,7 +127,8 @@ def _read_symbolic_pulse(file_obj, version, qiskit_version): file_obj.read(size), ) ) - + if version >= 6: + class_name = file_obj.read(header.class_name_size).decode(common.ENCODE) 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)) @@ -138,30 +139,39 @@ def _read_symbolic_pulse(file_obj, version, qiskit_version): version=version, vectors={}, ) - if qiskit_version < (0, 23, 0): - envelope, is_scalable = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) + if version < 6: + envelope, class_name = _format_legacy_qiskit_pulse(pulse_type, envelope, parameters) # Note that parameters is mutated during the function call - else: - is_scalable = header.is_scalable duration = value.read_value(file_obj, version, {}) name = value.read_value(file_obj, version, {}) - if is_scalable: - pulse_generator = library.ScalableSymbolicPulse + if class_name == "SymbolicPulse": + 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, + ) + elif class_name == "ScalableSymbolicPulse": + return library.ScalableSymbolicPulse( + pulse_type=pulse_type, + duration=duration, + amp=parameters["amp"], + angle=parameters["angle"], + parameters=parameters, + name=name, + limit_amplitude=header.amp_limited, + envelope=envelope, + constraints=constraints, + valid_amp_conditions=valid_amp_conditions, + ) else: - pulse_generator = library.SymbolicPulse - - return pulse_generator( - 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, - ) + raise NotImplementedError(f"Unknown class '{class_name}'") def _read_alignment_context(file_obj, version): @@ -181,30 +191,24 @@ def _read_alignment_context(file_obj, version): return instance -def _loads_operand(type_key, data_bytes, version, qiskit_version): +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, qiskit_version=qiskit_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) return value.loads_value(type_key, data_bytes, version, {}) -def _read_element(file_obj, version, metadata_deserializer, qiskit_version=None): +def _read_element(file_obj, version, metadata_deserializer): 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, qiskit_version=qiskit_version - ) + return read_schedule_block(file_obj, version, metadata_deserializer) - operands = common.read_sequence( - file_obj, deserializer=_loads_operand, version=version, qiskit_version=qiskit_version - ) + operands = common.read_sequence(file_obj, deserializer=_loads_operand, version=version) name = value.read_value(file_obj, version, {}) instance = object.__new__(type_keys.ScheduleInstruction.retrieve(type_key)) @@ -246,22 +250,23 @@ def _dumps_symbolic_expr(expr): def _write_symbolic_pulse(file_obj, data): + class_name_bytes = data.__class__.__name__.encode(common.ENCODE) pulse_type_bytes = data.pulse_type.encode(common.ENCODE) envelope_bytes = _dumps_symbolic_expr(data.envelope) constraints_bytes = _dumps_symbolic_expr(data.constraints) valid_amp_conditions_bytes = _dumps_symbolic_expr(data.valid_amp_conditions) - is_scalable = isinstance(data, library.ScalableSymbolicPulse) header_bytes = struct.pack( formats.SYMBOLIC_PULSE_PACK_V2, + len(class_name_bytes), len(pulse_type_bytes), len(envelope_bytes), len(constraints_bytes), len(valid_amp_conditions_bytes), data._limit_amplitude, - is_scalable, ) file_obj.write(header_bytes) + file_obj.write(class_name_bytes) file_obj.write(pulse_type_bytes) file_obj.write(envelope_bytes) file_obj.write(constraints_bytes) @@ -316,7 +321,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, qiskit_version=None): +def read_schedule_block(file_obj, version, metadata_deserializer=None): """Read a single ScheduleBlock from the file like object. Args: @@ -329,7 +334,6 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None, qiskit_ve 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. @@ -359,9 +363,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None, qiskit_ve alignment_context=context, ) for _ in range(data.num_elements): - block_elm = _read_element( - file_obj, version, metadata_deserializer, qiskit_version=qiskit_version - ) + block_elm = _read_element(file_obj, version, metadata_deserializer) block.append(block_elm, inplace=True) return block 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/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 35469039c67a..31cc32a9c405 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -199,15 +199,15 @@ SYMBOLIC_PULSE_V2 = namedtuple( "SYMBOLIC_PULSE", [ + "class_name_size", "type_size", "envelope_size", "constraints_size", "valid_amp_conditions_size", "amp_limited", - "is_scalable", ], ) -SYMBOLIC_PULSE_PACK_V2 = "!HHHH??" +SYMBOLIC_PULSE_PACK_V2 = "!HHHHH?" SYMBOLIC_PULSE_SIZE_V2 = struct.calcsize(SYMBOLIC_PULSE_PACK_V2) # INSTRUCTION_PARAM diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 6c15236274d7..075a6435050b 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -270,7 +270,6 @@ def load( file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer, - qiskit_version=qiskit_version, ) ) return programs diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml new file mode 100644 index 000000000000..067bbb5b97eb --- /dev/null +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Introduced a new subclass :class:`ScalabaleSymbolicPulse`, as a sub class of + :class:`SymbolicPulse`. The new subclass behaves the same way as :class:`SymbolicPulse`, + except that it assumes that the envelope of the pulse includes a complex amplitude + pre-factor of the form :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)'. + This envelope shape matches many common pulses, including all of the pulses in + the Qiskit Pulse library (which were also converted to 'amp','angle' representation in + this release). + + The new subclass removes the non-unique nature of the `amp`,`angle` representation, + and correctly compares pulses according to their complex amplitude. +other: + - | + QPY version was bumped to 6. Following the introduction of :class:`ScalabaleSymbolicPulse` + the header of saved :class:`SymbolicPulse` objects was modified to include + a `class_name` attribute, and the QPY version was bumped to reflect this. diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 7684aa34b22a..b23ae32b8763 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -19,7 +19,6 @@ from qiskit.circuit import Parameter from qiskit.pulse.library import ( SymbolicPulse, - ScalableSymbolicPulse, Waveform, Constant, Gaussian, @@ -258,25 +257,25 @@ def test_parameters(self): def test_repr(self): """Test the repr methods for parametric pulses.""" 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)") + self.assertEqual(repr(gaus), "Gaussian(duration=25, sigma=4, amp=0.7, 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)") + self.assertEqual(repr(gaus), "Gaussian(duration=25, sigma=4, amp=(0.1+0.7j), angle=0)") gaus_square = GaussianSquare(duration=20, sigma=30, amp=1.0, width=3) self.assertEqual( - repr(gaus_square), "GaussianSquare(duration=20, amp=1.0, sigma=30, width=3, angle=0)" + repr(gaus_square), "GaussianSquare(duration=20, sigma=30, width=3, amp=1.0, angle=0)" ) gaus_square = GaussianSquare( duration=20, sigma=30, amp=1.0, angle=0.2, risefall_sigma_ratio=0.1 ) self.assertEqual( repr(gaus_square), - "GaussianSquare(duration=20, amp=1.0, sigma=30, width=14.0, angle=0.2)", + "GaussianSquare(duration=20, sigma=30, width=14.0, amp=1.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, sigma=7, beta=1, angle=0)") + self.assertEqual(repr(drag), "Drag(duration=5, sigma=7, beta=1, amp=0.5, 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)") @@ -532,21 +531,8 @@ def local_gaussian(duration, amp, t0, sig): class TestScalableSymbolicPulse(QiskitTestCase): """ScalableSymbolicPulse tests""" - # pylint: disable=invalid-name - def test_amp_angle_verification(self): - """Test defining a custom pulse with no amp or angle""" - t, t1, t2, amp1, amp2 = sym.symbols("t, t1, t2, amp1, amp2") - envelope = sym.Piecewise((amp1, sym.And(t > t1, t < t2)), (amp2, sym.true)) - with self.assertRaises(PulseError): - ScalableSymbolicPulse( - pulse_type="Custom", - duration=100, - parameters={"t1": 30, "t2": 80, "amp1": 0.1j, "amp2": 0.1j}, - envelope=envelope, - ) - def test_scalable_comparison(self): - """Test equating of pulses with comparison_parameters.""" + """Test equating of pulses""" # amp,angle comparison gaussian_negamp = Gaussian(duration=25, sigma=4, amp=-0.5, angle=0) gaussian_piphase = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi) From d25c1bf7a8430abbf7a7272b42c5a76da8490895 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 12:49:43 +0200 Subject: [PATCH 03/20] Bug fix --- qiskit/pulse/library/symbolic_pulses.py | 4 ++-- .../notes/new_pulse_subclass-44da774612699312.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 43c6374a66e5..9d4297e8c7b7 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -625,14 +625,14 @@ def __init__( """ # This should be removed once complex amp support is deprecated. if isinstance(amp, complex): - if angle is None: + if angle is None or angle == 0: 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 with angle!=None") + raise PulseError("amp can't be complex with non zero angle") if angle is None: angle = 0 diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml index 067bbb5b97eb..2324f6dae819 100644 --- a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -1,8 +1,8 @@ --- features: - | - Introduced a new subclass :class:`ScalabaleSymbolicPulse`, as a sub class of - :class:`SymbolicPulse`. The new subclass behaves the same way as :class:`SymbolicPulse`, + Introduced a new subclass :class:`~ScalabaleSymbolicPulse`, as a sub class of + :class:`~SymbolicPulse`. The new subclass behaves the same way as :class:`~SymbolicPulse`, except that it assumes that the envelope of the pulse includes a complex amplitude pre-factor of the form :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)'. This envelope shape matches many common pulses, including all of the pulses in @@ -13,6 +13,6 @@ features: and correctly compares pulses according to their complex amplitude. other: - | - QPY version was bumped to 6. Following the introduction of :class:`ScalabaleSymbolicPulse` - the header of saved :class:`SymbolicPulse` objects was modified to include + QPY version was bumped to 6. Following the introduction of :class:`~ScalabaleSymbolicPulse` + the header of saved :class:`~SymbolicPulse` objects was modified to include a `class_name` attribute, and the QPY version was bumped to reflect this. From 4d4071f8c3058119cf676c7de00efb782f198153 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 14:15:39 +0200 Subject: [PATCH 04/20] Documentation --- qiskit/pulse/library/symbolic_pulses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 9d4297e8c7b7..8b07624ef645 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -620,7 +620,7 @@ def __init__( Raises: PulseError: When not all parameters are listed in the attribute :attr:`PARAM_DEF`. - PulseError: If both `amp` is complex and `angle` is not `None`. + PulseError: If both `amp` is complex and `angle` is not `None` or 0. """ # This should be removed once complex amp support is deprecated. @@ -632,7 +632,7 @@ def __init__( PendingDeprecationWarning, ) else: - raise PulseError("amp can't be complex with non zero angle") + raise PulseError("amp can't be complex with angle not None or 0") if angle is None: angle = 0 From fcfaafe0318e1e807a8dcc97d78c6097315e2823 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 15:36:50 +0200 Subject: [PATCH 05/20] Release Notes --- .../notes/new_pulse_subclass-44da774612699312.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml index 2324f6dae819..aec65f871c0b 100644 --- a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -1,8 +1,9 @@ --- features: - | - Introduced a new subclass :class:`~ScalabaleSymbolicPulse`, as a sub class of - :class:`~SymbolicPulse`. The new subclass behaves the same way as :class:`~SymbolicPulse`, + Introduced a new subclass :class:`~qiskit.pulse.library.ScalableSymbolicPulse`, as a + sub class of :class:`~qiskit.pulse.library.SymbolicPulse`. The new subclass behaves + the same way as :class:`~qiskit.pulse.library.SymbolicPulse`, except that it assumes that the envelope of the pulse includes a complex amplitude pre-factor of the form :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)'. This envelope shape matches many common pulses, including all of the pulses in @@ -13,6 +14,6 @@ features: and correctly compares pulses according to their complex amplitude. other: - | - QPY version was bumped to 6. Following the introduction of :class:`~ScalabaleSymbolicPulse` - the header of saved :class:`~SymbolicPulse` objects was modified to include + QPY version was bumped to 6. Following the introduction of :class:`~qiskit.pulse.library.ScalableSymbolicPulse` + the header of saved :class:`~qiskit.pulse.library.SymbolicPulse` objects was modified to include a `class_name` attribute, and the QPY version was bumped to reflect this. From 56e6ffc1cb67f0c4765f7be3c47a9ec546497450 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 16:44:08 +0200 Subject: [PATCH 06/20] Release Notes --- releasenotes/notes/new_pulse_subclass-44da774612699312.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml index aec65f871c0b..31e4ef9dbb04 100644 --- a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -12,6 +12,7 @@ features: The new subclass removes the non-unique nature of the `amp`,`angle` representation, and correctly compares pulses according to their complex amplitude. + other: - | QPY version was bumped to 6. Following the introduction of :class:`~qiskit.pulse.library.ScalableSymbolicPulse` From 277d500b7eace5a9d71b81d520e16b9dc7a4fa3a Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 21:18:36 +0200 Subject: [PATCH 07/20] Release Notes --- releasenotes/notes/new_pulse_subclass-44da774612699312.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml index 31e4ef9dbb04..803cc03aec0e 100644 --- a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -3,7 +3,7 @@ features: - | Introduced a new subclass :class:`~qiskit.pulse.library.ScalableSymbolicPulse`, as a sub class of :class:`~qiskit.pulse.library.SymbolicPulse`. The new subclass behaves - the same way as :class:`~qiskit.pulse.library.SymbolicPulse`, + the same as :class:`~qiskit.pulse.library.SymbolicPulse`, except that it assumes that the envelope of the pulse includes a complex amplitude pre-factor of the form :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)'. This envelope shape matches many common pulses, including all of the pulses in From 20e6dba1b565f6017aab3bb5573bfc88a699eb7a Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Thu, 22 Dec 2022 22:35:12 +0200 Subject: [PATCH 08/20] Release Notes --- releasenotes/notes/new_pulse_subclass-44da774612699312.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml index 803cc03aec0e..b7cd9e29aff5 100644 --- a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -5,14 +5,13 @@ features: sub class of :class:`~qiskit.pulse.library.SymbolicPulse`. The new subclass behaves the same as :class:`~qiskit.pulse.library.SymbolicPulse`, except that it assumes that the envelope of the pulse includes a complex amplitude - pre-factor of the form :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)'. + pre-factor of the form `amp * exp(1j * angle)`. This envelope shape matches many common pulses, including all of the pulses in the Qiskit Pulse library (which were also converted to 'amp','angle' representation in this release). The new subclass removes the non-unique nature of the `amp`,`angle` representation, and correctly compares pulses according to their complex amplitude. - other: - | QPY version was bumped to 6. Following the introduction of :class:`~qiskit.pulse.library.ScalableSymbolicPulse` From 4bcc7c07cf05bfa95bf36eb9b6b69275337339d2 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 25 Dec 2022 09:28:18 +0200 Subject: [PATCH 09/20] Release Notes --- qiskit/pulse/library/symbolic_pulses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 8b07624ef645..4cb10592e7df 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -685,7 +685,7 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: return True # When __eq__ is modified, __hash__ is automatically set to None, and thus needs to be inherited - # explicitly. + # explicitly __hash__ = SymbolicPulse.__hash__ From 376dda1ceb0e4c51febda8d6d6f7c023929015df Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 1 Jan 2023 21:41:02 +0200 Subject: [PATCH 10/20] Hash correction --- qiskit/pulse/library/symbolic_pulses.py | 19 ++++++++++++++----- test/python/pulse/test_pulse_lib.py | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 4cb10592e7df..aa04d46c92d6 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -17,7 +17,7 @@ These are pulses which are described by symbolic equations for their envelopes and for their parameter constraints. """ - +import copy import functools import warnings from typing import Any, Dict, List, Optional, Union, Callable @@ -675,7 +675,10 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: if complex_amp1 != complex_amp2: return False else: - if not np.isclose(complex_amp1, complex_amp2): + # Because the complex amp is calculated, numerical accuracy becomes an issue. + # We can't use np.isclose(), because we must have that equal pulses have the same hash, + # which requires us to bring the pulses to some agreed upon representation. + if np.round(complex_amp1, 6) != np.round(complex_amp2, 6): return False for key in self.parameters: @@ -684,9 +687,15 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: return True - # When __eq__ is modified, __hash__ is automatically set to None, and thus needs to be inherited - # explicitly - __hash__ = SymbolicPulse.__hash__ + def __hash__(self) -> int: + if self.is_parameterized(): + raise NotImplementedError( + "Hashing a scalable symbolic pulse with unassigned parameter is not supported." + ) + params = copy.copy(self._params) + params["amp"] = np.round(params["amp"] * np.exp(1j * params["angle"]), 6) + del params["angle"] + return hash((self._pulse_type, self._envelope, self.duration, *tuple(params.items()))) class _PulseType(type): diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index b23ae32b8763..d5398e0390b0 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -537,6 +537,7 @@ def test_scalable_comparison(self): gaussian_negamp = Gaussian(duration=25, sigma=4, amp=-0.5, angle=0) gaussian_piphase = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi) self.assertEqual(gaussian_negamp, gaussian_piphase) + self.assertEqual(hash(gaussian_negamp), hash(gaussian_piphase)) # Parameterized library pulses amp = Parameter("amp") From d29b74c994cf49414d3cb4040def0cf365a96ba2 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 8 Jan 2023 12:09:24 +0200 Subject: [PATCH 11/20] Resolve GaussianSquareDrag conflict. --- qiskit/pulse/library/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 6bc3fd92984d..3857501bd3ce 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -116,6 +116,7 @@ ScalableSymbolicPulse, Gaussian, GaussianSquare, + GaussianSquareDrag, Drag, Constant, ) From 89b99839f4fbbeec3785874f2c49b712362db8fe Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 8 Jan 2023 12:12:01 +0200 Subject: [PATCH 12/20] Resolve GaussianSquareDrag conflict. --- qiskit/pulse/library/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 3857501bd3ce..a2b3c34a07b9 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -113,7 +113,6 @@ from .parametric_pulses import ParametricPulse from .symbolic_pulses import ( SymbolicPulse, - ScalableSymbolicPulse, Gaussian, GaussianSquare, GaussianSquareDrag, From c20c1e61c8ee69f94ecdc9dd97e8a84bb2f20d7d Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 8 Jan 2023 12:22:05 +0200 Subject: [PATCH 13/20] Resolve GaussianSquareDrag conflict. --- qiskit/pulse/library/__init__.py | 1 + qiskit/pulse/library/symbolic_pulses.py | 33 ++++++------------------- test/python/pulse/test_pulse_lib.py | 12 --------- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 5a5f42fd505f..bb68dca4a9fa 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -114,6 +114,7 @@ from .parametric_pulses import ParametricPulse from .symbolic_pulses import ( SymbolicPulse, + ScalableSymbolicPulse, Gaussian, GaussianSquare, GaussianSquareDrag, diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index cd38ab50d8a7..e8ca8f288cd0 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -84,20 +84,23 @@ def _lifted_gaussian( @functools.lru_cache(maxsize=None) -def _is_amplitude_valid(symbolic_pulse: "SymbolicPulse") -> bool: +def _is_amplitude_valid(envelope_lam: Callable, envelope: sym.Expr, parameters: tuple) -> bool: """A helper function to validate maximum amplitude limit. Result is cached for better performance. Args: - symbolic_pulse: A pulse to validate. + envelope_lam: The SymbolicPulse's lambdified envelope_lam expression. + envelope: The SymbolicPulse's envelope expressions. + parameters: The SymbolicPulse's parameters.items() (assumed to be binded) converted to tuple (for hashability). Returns: Return True if no sample point exceeds 1.0 in absolute value. """ try: + fargs = _get_expression_args(envelope, dict(parameters)) # Instantiation of Waveform does automatic amplitude validation. - symbolic_pulse.get_waveform() + Waveform(samples=envelope_lam(*fargs)) return True except PulseError: return False @@ -522,7 +525,7 @@ def validate_parameters(self) -> None: # Check full waveform only when the condition is satisified or # evaluation condition is not provided. # This operation is slower due to overhead of 'get_waveform'. - if not _is_amplitude_valid(self): + if not _is_amplitude_valid(self._envelope_lam, self._envelope, tuple(self.parameters.items())): param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) raise PulseError( f"Maximum pulse amplitude norm exceeds 1.0 with parameters {param_repr}." @@ -555,13 +558,6 @@ def __eq__(self, other: "SymbolicPulse") -> bool: return True - def __hash__(self) -> int: - if self.is_parameterized(): - raise NotImplementedError( - "Hashing a symbolic pulse with unassigned parameter is not supported." - ) - return hash((self._pulse_type, self._envelope, self.duration, *tuple(self._params.items()))) - def __repr__(self) -> str: param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) return "{}({}{})".format( @@ -677,10 +673,7 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: if complex_amp1 != complex_amp2: return False else: - # Because the complex amp is calculated, numerical accuracy becomes an issue. - # We can't use np.isclose(), because we must have that equal pulses have the same hash, - # which requires us to bring the pulses to some agreed upon representation. - if np.round(complex_amp1, 6) != np.round(complex_amp2, 6): + if not np.isclose(complex_amp1, complex_amp2): return False for key in self.parameters: @@ -689,16 +682,6 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: return True - def __hash__(self) -> int: - if self.is_parameterized(): - raise NotImplementedError( - "Hashing a scalable symbolic pulse with unassigned parameter is not supported." - ) - params = copy.copy(self._params) - params["amp"] = np.round(params["amp"] * np.exp(1j * params["angle"]), 6) - del params["angle"] - return hash((self._pulse_type, self._envelope, self.duration, *tuple(params.items()))) - class _PulseType(type): """Metaclass to warn at isinstance check.""" diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index c1488e1f2ac9..a1ecf4148a08 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -388,17 +388,6 @@ def test_param_validation(self): with self.assertRaises(PulseError): Drag(duration=25, amp=0.2 + 0.3j, sigma=-7.8, beta=4) - def test_hash_generation(self): - """Test if pulse generate unique hash.""" - test_hash = [ - hash(GaussianSquare(duration=688, amp=0.1 + 0.1j, sigma=64, width=432)) - for _ in range(10) - ] - - ref_hash = [test_hash[0] for _ in range(10)] - - self.assertListEqual(test_hash, ref_hash) - def test_gaussian_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): @@ -648,7 +637,6 @@ def test_scalable_comparison(self): gaussian_negamp = Gaussian(duration=25, sigma=4, amp=-0.5, angle=0) gaussian_piphase = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi) self.assertEqual(gaussian_negamp, gaussian_piphase) - self.assertEqual(hash(gaussian_negamp), hash(gaussian_piphase)) # Parameterized library pulses amp = Parameter("amp") From b03f821ba097965be67b1f85adc3032641cbcf71 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 8 Jan 2023 12:56:13 +0200 Subject: [PATCH 14/20] GaussianSquareDrag conversion to scalable and minor fixes. --- qiskit/pulse/library/symbolic_pulses.py | 23 ++++++++++++++++------- test/python/pulse/test_pulse_lib.py | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index e8ca8f288cd0..37c2c89856b8 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -17,7 +17,6 @@ These are pulses which are described by symbolic equations for their envelopes and for their parameter constraints. """ -import copy import functools import warnings from typing import Any, Dict, List, Optional, Union, Callable @@ -92,7 +91,8 @@ def _is_amplitude_valid(envelope_lam: Callable, envelope: sym.Expr, parameters: Args: envelope_lam: The SymbolicPulse's lambdified envelope_lam expression. envelope: The SymbolicPulse's envelope expressions. - parameters: The SymbolicPulse's parameters.items() (assumed to be binded) converted to tuple (for hashability). + parameters: The SymbolicPulse's parameters.items() (assumed to be binded) converted + to tuple (for hashability). Returns: Return True if no sample point exceeds 1.0 in absolute value. @@ -525,7 +525,9 @@ def validate_parameters(self) -> None: # Check full waveform only when the condition is satisified or # evaluation condition is not provided. # This operation is slower due to overhead of 'get_waveform'. - if not _is_amplitude_valid(self._envelope_lam, self._envelope, tuple(self.parameters.items())): + if not _is_amplitude_valid( + self._envelope_lam, self._envelope, tuple(self.parameters.items()) + ): param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) raise PulseError( f"Maximum pulse amplitude norm exceeds 1.0 with parameters {param_repr}." @@ -566,6 +568,9 @@ def __repr__(self) -> str: f", name='{self.name}'" if self.name is not None else "", ) + def __hash__(self) -> int: + raise NotImplementedError + class ScalableSymbolicPulse(SymbolicPulse): r"""Subclass of :class:`SymbolicPulse` for pulses with scalable envelope. @@ -617,7 +622,6 @@ def __init__( creates a full-waveform. Raises: - PulseError: When not all parameters are listed in the attribute :attr:`PARAM_DEF`. PulseError: If both `amp` is complex and `angle` is not `None` or 0. """ @@ -682,6 +686,9 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: return True + def __hash__(self) -> int: + raise NotImplementedError + class _PulseType(type): """Metaclass to warn at isinstance check.""" @@ -999,7 +1006,7 @@ def GaussianSquareDrag( waveform to 1. The default is ``True`` and the amplitude is constrained to 1. Returns: - SymbolicPulse instance. + ScalableSymbolicPulse instance. Raises: PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. @@ -1017,7 +1024,7 @@ def GaussianSquareDrag( 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, "beta": beta, "angle": angle} + parameters = {"sigma": sigma, "width": width, "beta": beta} # Prepare symbolic expressions _t, _duration, _amp, _sigma, _beta, _width, _angle = sym.symbols( @@ -1045,9 +1052,11 @@ def GaussianSquareDrag( consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) - instance = SymbolicPulse( + instance = ScalableSymbolicPulse( pulse_type="GaussianSquareDrag", duration=duration, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index a1ecf4148a08..8b8e4771731b 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -351,12 +351,12 @@ def test_repr(self): gsd = GaussianSquareDrag(duration=20, sigma=30, amp=1.0, width=3, beta=1) self.assertEqual( repr(gsd), - "GaussianSquareDrag(duration=20, amp=1.0, sigma=30, width=3, beta=1, angle=0.0)", + "GaussianSquareDrag(duration=20, sigma=30, width=3, beta=1, amp=1.0, angle=0.0)", ) gsd = GaussianSquareDrag(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1, beta=1) self.assertEqual( repr(gsd), - "GaussianSquareDrag(duration=20, amp=1.0, sigma=30, width=14.0, beta=1, angle=0.0)", + "GaussianSquareDrag(duration=20, sigma=30, width=14.0, beta=1, amp=1.0, angle=0.0)", ) drag = Drag(duration=5, amp=0.5, sigma=7, beta=1) self.assertEqual(repr(drag), "Drag(duration=5, sigma=7, beta=1, amp=0.5, angle=0)") From e28e46402796e2611036bc8617d7c37364479499 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 8 Jan 2023 12:56:48 +0200 Subject: [PATCH 15/20] added _read_symbolic_pulse_v6, and updated _loads_operand --- qiskit/qpy/binary_io/schedules.py | 77 +++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 76215e633a79..240470ab128a 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -87,7 +87,7 @@ def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): # List of pulses in the library in QPY version 5 and below: legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] - class_name = "SymbolicPulse" + class_name = "SymbolicPulse" # Default class name, if not in the library if pulse_type in legacy_library_pulses: # Once complex amp support will be deprecated we will need: @@ -112,14 +112,64 @@ def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): def _read_symbolic_pulse(file_obj, version): - if version < 6: - make = formats.SYMBOLIC_PULSE._make - pack = formats.SYMBOLIC_PULSE_PACK - size = formats.SYMBOLIC_PULSE_SIZE + make = formats.SYMBOLIC_PULSE._make + pack = formats.SYMBOLIC_PULSE_PACK + size = formats.SYMBOLIC_PULSE_SIZE + + header = make( + struct.unpack( + pack, + file_obj.read(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={}, + ) + envelope, class_name = _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, {}) + + if class_name == "SymbolicPulse": + 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, + ) + elif class_name == "ScalableSymbolicPulse": + return library.ScalableSymbolicPulse( + pulse_type=pulse_type, + duration=duration, + amp=parameters["amp"], + angle=parameters["angle"], + parameters=parameters, + name=name, + limit_amplitude=header.amp_limited, + envelope=envelope, + constraints=constraints, + valid_amp_conditions=valid_amp_conditions, + ) else: - make = formats.SYMBOLIC_PULSE_V2._make - pack = formats.SYMBOLIC_PULSE_PACK_V2 - size = formats.SYMBOLIC_PULSE_SIZE_V2 + raise NotImplementedError(f"Unknown class '{class_name}'") + + +def _read_symbolic_pulse_v6(file_obj, version): + make = formats.SYMBOLIC_PULSE_V2._make + pack = formats.SYMBOLIC_PULSE_PACK_V2 + size = formats.SYMBOLIC_PULSE_SIZE_V2 header = make( struct.unpack( @@ -127,8 +177,7 @@ def _read_symbolic_pulse(file_obj, version): file_obj.read(size), ) ) - if version >= 6: - class_name = file_obj.read(header.class_name_size).decode(common.ENCODE) + class_name = file_obj.read(header.class_name_size).decode(common.ENCODE) 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)) @@ -139,9 +188,6 @@ def _read_symbolic_pulse(file_obj, version): version=version, vectors={}, ) - if version < 6: - envelope, class_name = _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, {}) @@ -195,7 +241,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 < 6: + return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) + else: + return common.data_from_binary(data_bytes, _read_symbolic_pulse_v6, version=version) if type_key == type_keys.ScheduleOperand.CHANNEL: return common.data_from_binary(data_bytes, _read_channel, version=version) From 89be67616f455b537108f514909890cf33aa42f1 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Sun, 8 Jan 2023 22:15:50 +0200 Subject: [PATCH 16/20] Added ScalableSymbolicPulse to init --- qiskit/pulse/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index f8c88f6308a2..8902b12c5429 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -144,6 +144,7 @@ GaussianSquareDrag, ParametricPulse, SymbolicPulse, + ScalableSymbolicPulse, Waveform, ) from qiskit.pulse.library.samplers.decorators import functional_pulse From 466a4945d522b608adb9ad53b8069cdf69ed740c Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 9 Jan 2023 14:07:23 +0200 Subject: [PATCH 17/20] Minor fixes --- qiskit/pulse/library/symbolic_pulses.py | 20 ++++----- qiskit/qpy/__init__.py | 18 +++++++- qiskit/qpy/binary_io/schedules.py | 59 +++++++++++-------------- 3 files changed, 51 insertions(+), 46 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 37c2c89856b8..e9026529def3 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -19,7 +19,7 @@ """ import functools import warnings -from typing import Any, Dict, List, Optional, Union, Callable +from typing import Any, Dict, List, Optional, Union, Callable, Tuple from copy import deepcopy import numpy as np @@ -83,7 +83,7 @@ def _lifted_gaussian( @functools.lru_cache(maxsize=None) -def _is_amplitude_valid(envelope_lam: Callable, envelope: sym.Expr, parameters: tuple) -> bool: +def _is_amplitude_valid(envelope_lam: Callable, time: Tuple[float, ...], *fargs: float) -> bool: """A helper function to validate maximum amplitude limit. Result is cached for better performance. @@ -98,9 +98,9 @@ def _is_amplitude_valid(envelope_lam: Callable, envelope: sym.Expr, parameters: Return True if no sample point exceeds 1.0 in absolute value. """ try: - fargs = _get_expression_args(envelope, dict(parameters)) + time = np.asarray(time, dtype=float) # Instantiation of Waveform does automatic amplitude validation. - Waveform(samples=envelope_lam(*fargs)) + Waveform(samples=envelope_lam(time, *fargs)) return True except PulseError: return False @@ -525,9 +525,9 @@ def validate_parameters(self) -> None: # Check full waveform only when the condition is satisified or # evaluation condition is not provided. # This operation is slower due to overhead of 'get_waveform'. - if not _is_amplitude_valid( - self._envelope_lam, self._envelope, tuple(self.parameters.items()) - ): + fargs = _get_expression_args(self._envelope, self.parameters) + + if not _is_amplitude_valid(self._envelope_lam, tuple(fargs.pop(0)), *fargs): param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) raise PulseError( f"Maximum pulse amplitude norm exceeds 1.0 with parameters {param_repr}." @@ -568,8 +568,7 @@ def __repr__(self) -> str: f", name='{self.name}'" if self.name is not None else "", ) - def __hash__(self) -> int: - raise NotImplementedError + __hash__ = None class ScalableSymbolicPulse(SymbolicPulse): @@ -686,9 +685,6 @@ def __eq__(self, other: "ScalableSymbolicPulse") -> bool: return True - def __hash__(self) -> int: - raise NotImplementedError - class _PulseType(type): """Metaclass to warn at isinstance check.""" diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index ca37e4e25033..cc53a037b742 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -132,9 +132,25 @@ ========= Version 6 adds support for :class:`.~ScalableSymbolicPulse`. These objects are saved and read -like `SymbolicPulse` objects, and the class name is added to the meta-data to correctly handle +like `SymbolicPulse` objects, and the class name is added to the data to correctly handle the class selection. +`SymbolicPulse` block now starts with SYMBOLIC_PULSE_V2 header: + +.. code-block:: c + struct { + uint16_t class_name_size; + uint16_t type_size; + uint16_t envelope_size; + uint16_t constraints_size; + uint16_t valid_amp_conditions_size; + _bool amp_limited; + } + +The only change compared to :ref:`qpy_version_5` is the addition of `class_name_size`. The header +is then immediately followed by ``class_name_size`` utf8 bytes with the name of the class. Currently, +either `SymbolicPulse` or `ScalableSymbolicPulse` are supported. The rest of the data is then +identical to :ref:`qpy_version_5`. .. _qpy_version_5: diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 240470ab128a..db48fe3658f6 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -77,13 +77,33 @@ def _loads_symbolic_expr(expr_bytes): return expr -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. +def _read_symbolic_pulse(file_obj, version): + make = formats.SYMBOLIC_PULSE._make + pack = formats.SYMBOLIC_PULSE_PACK + size = formats.SYMBOLIC_PULSE_SIZE - # Note that parameters is mutated during the function call + header = make( + struct.unpack( + pack, + file_obj.read(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={}, + ) + + # In the transition to Qiskit Terra 0.23 (QPY version 6), 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. # List of pulses in the library in QPY version 5 and below: legacy_library_pulses = ["Gaussian", "GaussianSquare", "Drag", "Constant"] @@ -108,33 +128,6 @@ def _format_legacy_qiskit_pulse(pulse_type, envelope, parameters): ) class_name = "ScalableSymbolicPulse" - return envelope, class_name - - -def _read_symbolic_pulse(file_obj, version): - make = formats.SYMBOLIC_PULSE._make - pack = formats.SYMBOLIC_PULSE_PACK - size = formats.SYMBOLIC_PULSE_SIZE - - header = make( - struct.unpack( - pack, - file_obj.read(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={}, - ) - envelope, class_name = _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, {}) From 2f68f94e172e05fdfcc5b930a9eed159535edc38 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Mon, 9 Jan 2023 14:56:07 +0200 Subject: [PATCH 18/20] Documentation fix --- qiskit/qpy/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index cc53a037b742..9d5c4cb5becb 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -138,6 +138,7 @@ `SymbolicPulse` block now starts with SYMBOLIC_PULSE_V2 header: .. code-block:: c + struct { uint16_t class_name_size; uint16_t type_size; From 35b454c991d355dc2463d248a5fbb4993d5cb97f Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Tue, 10 Jan 2023 09:08:28 +0200 Subject: [PATCH 19/20] Minor fix to amplitude validation and release notes. --- qiskit/pulse/library/symbolic_pulses.py | 18 +++++++++--------- .../new_pulse_subclass-44da774612699312.yaml | 15 +++++++++++---- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index e9026529def3..f25602e5e736 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -90,20 +90,20 @@ def _is_amplitude_valid(envelope_lam: Callable, time: Tuple[float, ...], *fargs: Args: envelope_lam: The SymbolicPulse's lambdified envelope_lam expression. - envelope: The SymbolicPulse's envelope expressions. - parameters: The SymbolicPulse's parameters.items() (assumed to be binded) converted - to tuple (for hashability). + time: The SymbolicPulse's time array, given as a tuple for hashability. + fargs: The arguments for the lambdified envelope_lam, as given by `_get_expression_args`, + except for the time array. Returns: Return True if no sample point exceeds 1.0 in absolute value. """ - try: - time = np.asarray(time, dtype=float) - # Instantiation of Waveform does automatic amplitude validation. - Waveform(samples=envelope_lam(time, *fargs)) - return True - except PulseError: + + time = np.asarray(time, dtype=float) + samples_norm = np.abs(envelope_lam(time, *fargs)) + if np.any(samples_norm > 1.0): return False + else: + return True def _get_expression_args(expr: sym.Expr, params: Dict[str, float]) -> List[float]: diff --git a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml index b7cd9e29aff5..31630b058cc1 100644 --- a/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -12,8 +12,15 @@ features: The new subclass removes the non-unique nature of the `amp`,`angle` representation, and correctly compares pulses according to their complex amplitude. -other: +upgrade: - | - QPY version was bumped to 6. Following the introduction of :class:`~qiskit.pulse.library.ScalableSymbolicPulse` - the header of saved :class:`~qiskit.pulse.library.SymbolicPulse` objects was modified to include - a `class_name` attribute, and the QPY version was bumped to reflect this. + The QPY version format version emitted by :func:.qpy.dump has been + increased to version 6. This new format version is incompatible with the + previous versions and will result in an error when trying to load it with + a deserializer that isn't able to handle QPY version 6. This change was + necessary to support the introduction of :class:`~qiskit.pulse.library.ScalableSymbolicPulse` + which was handled by adding a `class_name_size` attribute to the header + of dumped :class:`~qiskit.pulse.library.SymbolicPulse` objects. + - | + The hashing option for :class:`~qiskit.pulse.library.SymbolicPulse` was removed + to better reflect their mutable nature (via parameter assignment). From 92922bb56cace2675b966068052e042550cd3022 Mon Sep 17 00:00:00 2001 From: TsafrirA Date: Tue, 10 Jan 2023 09:39:05 +0200 Subject: [PATCH 20/20] Add epsilon to amp validation. --- qiskit/pulse/library/symbolic_pulses.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index f25602e5e736..1f65b4572516 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -100,10 +100,8 @@ def _is_amplitude_valid(envelope_lam: Callable, time: Tuple[float, ...], *fargs: time = np.asarray(time, dtype=float) samples_norm = np.abs(envelope_lam(time, *fargs)) - if np.any(samples_norm > 1.0): - return False - else: - return True + epsilon = 1e-7 # The value of epsilon mimics that of Waveform._clip() + return np.all(samples_norm < 1.0 + epsilon) def _get_expression_args(expr: sym.Expr, params: Dict[str, float]) -> List[float]: