From fef06009a471b20aebd9962a13d91cfa831958e3 Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Wed, 11 Jan 2023 22:50:28 +0200 Subject: [PATCH] Introducing ScalableSymbolicPulse to correctly compare pulses of amp,angle representation (#9314) * Introduced ScalableSymbolicPulses to correctly compare pulses with amp,angle representation. * Modified ScalableSymbolicPulse, bumped QPY version to 6. * Bug fix * Documentation * Release Notes * Release Notes * Release Notes * Release Notes * Release Notes * Hash correction * Resolve GaussianSquareDrag conflict. * Resolve GaussianSquareDrag conflict. * Resolve GaussianSquareDrag conflict. * GaussianSquareDrag conversion to scalable and minor fixes. * added _read_symbolic_pulse_v6, and updated _loads_operand * Added ScalableSymbolicPulse to init * Minor fixes * Documentation fix * Minor fix to amplitude validation and release notes. * Add epsilon to amp validation. Co-authored-by: Naoki Kanazawa Co-authored-by: Will Shanks Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/pulse/__init__.py | 1 + qiskit/pulse/library/__init__.py | 1 + qiskit/pulse/library/symbolic_pulses.py | 257 +++++++++++------- qiskit/qpy/__init__.py | 27 ++ qiskit/qpy/binary_io/circuits.py | 13 +- qiskit/qpy/binary_io/schedules.py | 150 +++++++--- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 15 + qiskit/qpy/interface.py | 1 - .../new_pulse_subclass-44da774612699312.yaml | 26 ++ test/python/pulse/test_pulse_lib.py | 61 ++--- 11 files changed, 367 insertions(+), 187 deletions(-) create mode 100644 releasenotes/notes/new_pulse_subclass-44da774612699312.yaml 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 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 c85ab3c43717..1f65b4572516 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -17,10 +17,10 @@ These are pulses which are described by symbolic equations for their envelopes and for their parameter constraints. """ - 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,23 +83,25 @@ def _lifted_gaussian( @functools.lru_cache(maxsize=None) -def _is_amplitude_valid(symbolic_pulse: "SymbolicPulse") -> 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. Args: - symbolic_pulse: A pulse to validate. + envelope_lam: The SymbolicPulse's lambdified envelope_lam expression. + 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: - # Instantiation of Waveform does automatic amplitude validation. - symbolic_pulse.get_waveform() - return True - except PulseError: - return False + + time = np.asarray(time, dtype=float) + samples_norm = np.abs(envelope_lam(time, *fargs)) + 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]: @@ -521,7 +523,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): + 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}." @@ -554,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( @@ -569,6 +566,123 @@ def __repr__(self) -> str: f", name='{self.name}'" if self.name is not None else "", ) + __hash__ = None + + +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, + 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 + :math:'\text{amp}\times\exp\left(i\times\text{angle}\right)' is compared. + """ + + 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, + envelope: Optional[sym.Expr] = None, + constraints: Optional[sym.Expr] = None, + valid_amp_conditions: Optional[sym.Expr] = None, + ): + """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: If both `amp` is complex and `angle` is not `None` or 0. + + """ + # This should be removed once complex amp support is deprecated. + if isinstance(amp, complex): + 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 not None or 0") + + 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, + duration=duration, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope, + constraints=constraints, + valid_amp_conditions=valid_amp_conditions, + ) + + # pylint: disable=too-many-return-statements + def __eq__(self, other: "ScalableSymbolicPulse") -> bool: + 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) + + 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 + class _PulseType(type): """Metaclass to warn at isinstance check.""" @@ -640,26 +754,9 @@ def __new__( 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. + ScalableSymbolicPulse instance. """ - # 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") @@ -672,9 +769,11 @@ 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, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, @@ -755,11 +854,10 @@ 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. - 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: @@ -774,21 +872,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( @@ -813,9 +897,11 @@ 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, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, @@ -914,7 +1000,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. @@ -932,7 +1018,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( @@ -960,9 +1046,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, @@ -1040,26 +1128,9 @@ def __new__( 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. + ScalableSymbolicPulse instance. """ - # 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( @@ -1075,9 +1146,11 @@ 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, + amp=amp, + angle=angle, parameters=parameters, name=name, limit_amplitude=limit_amplitude, @@ -1121,27 +1194,8 @@ def __new__( 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. + ScalableSymbolicPulse instance. """ - # 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") @@ -1160,10 +1214,11 @@ def __new__( valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - instance = SymbolicPulse( + 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..9d5c4cb5becb 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -126,6 +126,33 @@ 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 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: 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 247f8f0797cb..db48fe3658f6 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -77,16 +77,37 @@ 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"] + 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: @@ -101,20 +122,55 @@ 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 + class_name = "ScalableSymbolicPulse" + + 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: + 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 -def _read_symbolic_pulse(file_obj, version, qiskit_version): - header = formats.SYMBOLIC_PULSE._make( + header = make( struct.unpack( - formats.SYMBOLIC_PULSE_PACK, - file_obj.read(formats.SYMBOLIC_PULSE_SIZE), + pack, + file_obj.read(size), ) ) + 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)) @@ -125,23 +181,36 @@ def _read_symbolic_pulse(file_obj, version, qiskit_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, {}) - 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, - ) + 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: + raise NotImplementedError(f"Unknown class '{class_name}'") def _read_alignment_context(file_obj, version): @@ -161,30 +230,27 @@ 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 - ) + 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) 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)) @@ -226,13 +292,15 @@ 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) header_bytes = struct.pack( - formats.SYMBOLIC_PULSE_PACK, + formats.SYMBOLIC_PULSE_PACK_V2, + len(class_name_bytes), len(pulse_type_bytes), len(envelope_bytes), len(constraints_bytes), @@ -240,6 +308,7 @@ def _write_symbolic_pulse(file_obj, data): data._limit_amplitude, ) 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) @@ -294,7 +363,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: @@ -307,7 +376,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. @@ -337,9 +405,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 07e121f67705..31cc32a9c405 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", + [ + "class_name_size", + "type_size", + "envelope_size", + "constraints_size", + "valid_amp_conditions_size", + "amp_limited", + ], +) +SYMBOLIC_PULSE_PACK_V2 = "!HHHHH?" +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/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..31630b058cc1 --- /dev/null +++ b/releasenotes/notes/new_pulse_subclass-44da774612699312.yaml @@ -0,0 +1,26 @@ +--- +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 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 `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. +upgrade: + - | + 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). diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 74fad36a5368..8b8e4771731b 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -331,35 +331,35 @@ 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)", ) 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, 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)") @@ -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): @@ -555,21 +544,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) @@ -654,5 +628,26 @@ def local_gaussian(duration, amp, t0, sig): self.assertEqual(len(pulse_wf_inst.samples), _duration) +class TestScalableSymbolicPulse(QiskitTestCase): + """ScalableSymbolicPulse tests""" + + def test_scalable_comparison(self): + """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) + 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()