Skip to content

Commit

Permalink
Removing complex values support from Calibrations (#1067)
Browse files Browse the repository at this point in the history
### Summary
This PR aims to remove the use of complex amplitude for `SymbolicPulse`
in calibration experiments. Most notable changes are to the
`HalfAngleCal` experiment and the `FixedFrquencyTransmon` library. With
these changes, support of complex values in general raises a
`PendingDeprecationWarning`.

### Details and comments
Qiskit Terra recently changed the representation of `SymbolicPulse` from
complex amplitude to (`amp`,`angle`). Once the deprecation is completed,
some calibration experiments will fail. Additionally, assignment of
complex parameters in general has caused problems recently (See
Qiskit-Terra issue
[9187](Qiskit/qiskit#9187)), and is also
being phased out (See Qiskit-Terra PR
[9735](Qiskit/qiskit#9735)).

Most calibration experiments are oblivious to these changes, with the
exception of `HalfAngleCal` and `RoughAmplitudeCal`. The library
`FixedFrequencyTransmon` also has to conform with the new
representation.

To create as little breaking changes as possible, the following were
changed:

- `FixedFrequencyTransmon` library was converted to the new
representation. All experiments will work as they have been with it.
- `RoughAmplitudeCal` was changed such that it will work for both real
or complex `amp`, without changing the type of the value.
- `HalfAngleCal` was changed to calibrate 'angle' instead of the complex
amplitude. A user which uses the `FixedFrequencyTransmon` library will
experience no change (except for the added parameters). A user which
uses custom built schedules will experience an error. To simplify the
transition, most likely scenarios (schedule with no `angle` parameter,
`cal_parameter_name="amp"`) will raise an informative error with
explanation about the needed changes.

A `PendingDeprecationWarning` is raised with every initialization of
`ParameterValue` with complex type value (which also covers addition of
parameter value to a calibration). Note that Qiskit-Terra PR
[9897](Qiskit/qiskit#9897) will also raise
a warning from the Terra side, for all assignment of complex parameters.

Handling of loaded calibrations which do not conform to the new
representation will be sorted out once PR #1120 is merged, as it
introduces a major change in calibration loading.

---------

Co-authored-by: Naoki Kanazawa <nkanazawa1989@gmail.com>
Co-authored-by: Will Shanks <wshaos@posteo.net>
Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com>
  • Loading branch information
4 people authored May 9, 2023
1 parent e19e45c commit 5459cc1
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 35 deletions.
70 changes: 50 additions & 20 deletions qiskit_experiments/calibration_management/basis_gate_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from collections.abc import Mapping
from typing import Any, Dict, List, Optional, Set
from warnings import warn
import numpy as np

from qiskit.circuit import Parameter
from qiskit import pulse
Expand Down Expand Up @@ -194,18 +195,20 @@ class FixedFrequencyTransmon(BasisGateLibrary):
- duration: Duration of the pulses Default value: 160 samples.
- σ: Standard deviation of the pulses Default value: ``duration / 4``.
- β: DRAG parameter of the pulses Default value: 0.
- amp: Amplitude of the pulses. If the parameters are linked then ``x`` and ``y``
- amp: Magnitude of the complex amplitude of the pulses. If the parameters are
linked then ``x`` and ``y``
share the same parameter and ``sx`` and ``sy`` share the same parameter.
Default value: 50% of the maximum output for ``x`` and ``y`` and 25% of the
maximum output for ``sx`` and ``sy``. Note that the user provided default amplitude
in the ``__init__`` method sets the default amplitude of the ``x`` and ``y`` pulses.
The amplitude of the ``sx`` and ``sy`` pulses is half the provided value.
- angle: The phase of the complex amplitude of the pulses.
Note that the β and amp parameters may be linked between the x and y as well as between
the sx and sy pulses. All pulses share the same duration and σ parameters.
"""

__default_values__ = {"duration": 160, "amp": 0.5, "β": 0.0}
__default_values__ = {"duration": 160, "amp": 0.5, "β": 0.0, "angle": 0.0}

def __init__(
self,
Expand Down Expand Up @@ -240,25 +243,25 @@ def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]:
dur = Parameter("duration")
sigma = Parameter("σ")

x_amp, x_beta = Parameter("amp"), Parameter("β")
x_amp, x_beta, x_angle = Parameter("amp"), Parameter("), Parameter("angle")

if self._link_parameters:
y_amp, y_beta = 1.0j * x_amp, x_beta
y_amp, y_beta, y_angle = x_amp, x_beta, x_angle + np.pi / 2
else:
y_amp, y_beta = Parameter("amp"), Parameter("β")
y_amp, y_beta, y_angle = Parameter("amp"), Parameter("), Parameter("angle")

sx_amp, sx_beta = Parameter("amp"), Parameter("β")
sx_amp, sx_beta, sx_angle = Parameter("amp"), Parameter("), Parameter("angle")

if self._link_parameters:
sy_amp, sy_beta = 1.0j * sx_amp, sx_beta
sy_amp, sy_beta, sy_angle = sx_amp, sx_beta, sx_angle + np.pi / 2
else:
sy_amp, sy_beta = Parameter("amp"), Parameter("β")
sy_amp, sy_beta, sy_angle = Parameter("amp"), Parameter("), Parameter("angle")

# Create the schedules for the gates
sched_x = self._single_qubit_schedule("x", dur, x_amp, sigma, x_beta)
sched_y = self._single_qubit_schedule("y", dur, y_amp, sigma, y_beta)
sched_sx = self._single_qubit_schedule("sx", dur, sx_amp, sigma, sx_beta)
sched_sy = self._single_qubit_schedule("sy", dur, sy_amp, sigma, sy_beta)
sched_x = self._single_qubit_schedule("x", dur, x_amp, sigma, x_beta, x_angle)
sched_y = self._single_qubit_schedule("y", dur, y_amp, sigma, y_beta, y_angle)
sched_sx = self._single_qubit_schedule("sx", dur, sx_amp, sigma, sx_beta, sx_angle)
sched_sy = self._single_qubit_schedule("sy", dur, sy_amp, sigma, sy_beta, sy_angle)

schedules = {}
for sched in [sched_x, sched_y, sched_sx, sched_sy]:
Expand All @@ -274,13 +277,14 @@ def _single_qubit_schedule(
amp: Parameter,
sigma: Parameter,
beta: Parameter,
angle: Parameter,
) -> ScheduleBlock:
"""Build a single qubit pulse."""

chan = pulse.DriveChannel(Parameter("ch0"))

with pulse.build(name=name) as sched:
pulse.play(pulse.Drag(duration=dur, amp=amp, sigma=sigma, beta=beta), chan)
pulse.play(pulse.Drag(duration=dur, amp=amp, sigma=sigma, beta=beta, angle=angle), chan)

return sched

Expand All @@ -307,8 +311,8 @@ def default_values(self) -> List[DefaultCalValue]:
if name in {"sx", "sy"} and param.name == "amp":
value /= 2.0

if "y" in name and param.name == "amp":
value *= 1.0j
if "y" in name and param.name == "angle":
value += np.pi / 2

defaults.append(DefaultCalValue(value, param.name, tuple(), name))

Expand Down Expand Up @@ -338,7 +342,15 @@ class EchoedCrossResonance(BasisGateLibrary):
- risefall: The number of σ's in the flanks of the pulses. Default value: 2.
"""

__default_values__ = {"tgt_amp": 0.0, "amp": 0.5, "σ": 64, "risefall": 2, "duration": 1168}
__default_values__ = {
"tgt_amp": 0.0,
"tgt_angle": 0.0,
"amp": 0.5,
"angle": 0.0,
"σ": 64,
"risefall": 2,
"duration": 1168,
}

def __init__(
self,
Expand Down Expand Up @@ -379,8 +391,10 @@ def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]:
schedules = {}

tgt_amp = Parameter("tgt_amp")
tgt_angle = Parameter("tgt_angle")
sigma = Parameter("σ")
cr_amp = Parameter("amp")
cr_angle = Parameter("angle")
cr_dur = Parameter("duration")
cr_rf = Parameter("risefall")
t_chan_idx = Parameter("ch1")
Expand All @@ -391,14 +405,20 @@ def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]:
if "cr45p" in basis_gates:
with pulse.build(name="cr45p") as cr45p:
pulse.play(
pulse.GaussianSquare(cr_dur, cr_amp, risefall_sigma_ratio=cr_rf, sigma=sigma),
pulse.GaussianSquare(
cr_dur, cr_amp, angle=cr_angle, risefall_sigma_ratio=cr_rf, sigma=sigma
),
u_chan,
)

if self._target_pulses:
pulse.play(
pulse.GaussianSquare(
cr_dur, tgt_amp, risefall_sigma_ratio=cr_rf, sigma=sigma
cr_dur,
tgt_amp,
angle=tgt_angle,
risefall_sigma_ratio=cr_rf,
sigma=sigma,
),
t_chan,
)
Expand All @@ -408,14 +428,24 @@ def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]:
if "cr45m" in basis_gates:
with pulse.build(name="cr45m") as cr45m:
pulse.play(
pulse.GaussianSquare(cr_dur, -cr_amp, risefall_sigma_ratio=cr_rf, sigma=sigma),
pulse.GaussianSquare(
cr_dur,
cr_amp,
angle=cr_angle + np.pi,
risefall_sigma_ratio=cr_rf,
sigma=sigma,
),
u_chan,
)

if self._target_pulses:
pulse.play(
pulse.GaussianSquare(
cr_dur, -tgt_amp, risefall_sigma_ratio=cr_rf, sigma=sigma
cr_dur,
tgt_amp,
angle=tgt_angle + np.pi,
risefall_sigma_ratio=cr_rf,
sigma=sigma,
),
t_chan,
)
Expand Down
12 changes: 12 additions & 0 deletions qiskit_experiments/calibration_management/parameter_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Union
import warnings

from qiskit_experiments.exceptions import CalibrationError

Expand Down Expand Up @@ -71,6 +72,17 @@ def __post_init__(self):

self.date_time = self.date_time.astimezone()

if isinstance(self.value, complex):
warnings.warn(
"Support of complex parameters is now pending deprecation, following the"
"same transition in Qiskit Terra's Pulse module."
"The main use of complex parameters was the complex amplitude in SymbolicPulse"
"instances. This use could be removed by converting the pulses to the"
"ScalableSymbolicPulse class which uses two floats (amp,angle) for the"
"complex amplitude.",
PendingDeprecationWarning,
)

if not isinstance(self.value, (int, float, complex)):
raise CalibrationError(f"Values {self.value} must be int, float or complex.")

Expand Down
50 changes: 43 additions & 7 deletions qiskit_experiments/library/calibration/half_angle_cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
"""Half angle calibration."""

from typing import Dict, Optional, Sequence
import numpy as np

from qiskit.circuit import QuantumCircuit
from qiskit.providers.backend import Backend

from qiskit_experiments.framework import ExperimentData
from qiskit_experiments.exceptions import CalibrationError
from qiskit_experiments.calibration_management import (
BaseCalibrationExperiment,
Calibrations,
Expand All @@ -38,10 +38,10 @@ def __init__(
calibrations: Calibrations,
backend: Optional[Backend] = None,
schedule_name: str = "sx",
cal_parameter_name: Optional[str] = "amp",
cal_parameter_name: Optional[str] = "angle",
auto_update: bool = True,
):
"""see class :class:`HalfAngle` for details.
"""The experiment to update angle of half-pi rotation gates.
Args:
physical_qubits: Sequence containing the qubit for which to run the
Expand All @@ -50,10 +50,46 @@ def __init__(
backend: Optional, the backend to run the experiment on.
schedule_name: The name of the schedule to calibrate which defaults to sx.
cal_parameter_name: The name of the parameter in the schedule to update. This will
default to amp since the complex amplitude contains the phase of the pulse.
default to 'angle' in accordance with the naming convention of the
:class:`~qiskit.pulse.ScalableSymbolicPulse` class.
auto_update: Whether or not to automatically update the calibrations. By
default this variable is set to True.
Raises:
CalibrationError: if cal_parameter_name is set to ``amp``, to reflect the
transition from calibrating complex amplitude to calibrating the phase.
CalibrationError: if the default cal_parameter_name is used, and it is not
a valid parameter of the calibrated schedule.
"""
if cal_parameter_name == "amp":
raise CalibrationError(
"The Half-Angle calibration experiment was changed from calibrating"
" the pulse's complex amplitude, to calibrating the angle parameter "
"in the real (amp,angle) representation. Setting cal_parameter_name to "
"'amp' thus indicates that you are probably using the experiment in "
"an inconsistent way. If your pulse does in fact use a complex amplitude,"
"you need to convert it to (amp,angle) representation, preferably using"
"the ScalableSymbolicPulse class. Note that all library pulses now use "
"this representation."
)
# If the default cal_parameter_name is used, validate that it is in fact a parameter
if cal_parameter_name == "angle":
try:
calibrations.calibration_parameter("angle", schedule_name=schedule_name)
except CalibrationError as err:
raise CalibrationError(
"The Half-Angle calibration experiment was changed from calibrating"
" the pulse's complex amplitude, to calibrating the angle parameter "
"in the real (amp,angle) representation. The default cal_parameter_name "
"was thus changed to angle, which is not a valid parameter of the "
"calibrated schedule. It is likely that you are trying to calibrate "
"a schedule which is defined by a complex amplitude. To use the "
"Half-Angle experiment you need to convert the pulses in the schedule "
"to (amp,angle) representation (preferably, using the "
"ScalableSymbolicPulse class), and have a parameter associated with "
"the angle. Note that all library pulses now use this representation."
) from err

super().__init__(
calibrations,
physical_qubits,
Expand Down Expand Up @@ -112,15 +148,15 @@ def update_calibrations(self, experiment_data: ExperimentData):

result_index = self.experiment_options.result_index
group = experiment_data.metadata["cal_group"]
prev_amp = experiment_data.metadata["cal_param_value"]
prev_angle = experiment_data.metadata["cal_param_value"]

d_theta = BaseUpdater.get_value(experiment_data, "d_hac", result_index)
new_amp = prev_amp * np.exp(-1.0j * d_theta / 2)
new_angle = prev_angle - (d_theta / 2)

BaseUpdater.add_parameter_value(
self._cals,
experiment_data,
new_amp,
new_angle,
self._param_name,
self._sched_name,
group,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def update_calibrations(self, experiment_data: ExperimentData):

for angle, param, schedule, prev_amp in experiment_data.metadata["angles_schedules"]:

value = np.round(angle / rate, decimals=8) * np.exp(1.0j * np.angle(prev_amp))
# This implementation conserves the type, while working for both real and complex prev_amp
value = np.round(angle / rate, decimals=8) * prev_amp / np.abs(prev_amp)

BaseUpdater.add_parameter_value(
self._cals, experiment_data, value, param, schedule, group
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
other:
- |
Qiskit Terra 0.23.0 began phasing out support of complex parameters
in the Pulse module. Mainly, all library symbolic pulses were converted
from complex amplitude representation to a duo of real (float) parameters
(``amp``, ``angle``). To avoid problems, Qiskit Experiments adopted this convention.
Changes were made to :class:`.FixedFrequencyTransmon` and :class:`.HalfAngleCal`
(see upgrade section). With the exception of :class:`.HalfAngleCal`, all
library experiments should continue to function as they did before (even with
complex ``amp``). When used with the :class:`.FixedFrequencyTransmon` library,
:class:`.HalfAngleCal` will also continue working as before.
Eventually, support for complex parameters will be dropped altogether, and it is
thus pending deprecation - including for saving and loading calibration data with
complex values.
upgrade:
- |
The minimal required Qiskit-Terra version was bumped to 0.23 in order to use
the (``amp``, ``angle``) representation of library pulses in Qiskit Pulse.
- |
The representation of pulses in the :class:`.FixedFrequencyTransmon` library
was changed from complex amplitude to (``amp``,``angle``) representation. All pulses
now include an ``angle`` parameter, and the default values of ``amp`` are set
as type ``float`` instead of ``complex``.
- |
:class:`.HalfAngleCal` was changed from updating the complex amplitude of
the pulse, to updating the angle in the (``amp``, ``angle``) representation. When used with
the :class:`.FixedFrequencyTransmon` library, it will continue to work seamlessly
in the new representation. However, when the experiment is used with custom
built pulses, which rely on the old convention of complex ``amp`` (with no
angle parameter) - the experiment will fail. Most reasonable cases will raise
a detailed ``CalibrationError`` explaining the change and the way to adjust
to it. Some edge cases - like a custom built pulse with an ``angle`` parameter
which doesn't conform to the naming convention of Qiskit Terra's
``ScalableSymbolicPulse`` class, or using a loaded calibration with ``complex``
``amp`` - will result in updating the wrong parameter.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
numpy>=1.17
scipy>=1.4
qiskit-terra>=0.22
qiskit-terra>=0.23
qiskit-ibm-experiment>=0.2.5
qiskit_dynamics>=0.3.0
matplotlib>=3.4
Expand Down
7 changes: 7 additions & 0 deletions test/calibration/test_calibrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,13 @@ def test_parameter_value_adding_and_filtering(self):
self.assertEqual(params[0]["value"], 0.25)
self.assertEqual(params[0]["qubits"], (3,))

def test_complex_parameter_value_deprecation_warning(self):
"""Test that complex parameter values raise PendingDeprecationWarning"""
with self.assertWarns(PendingDeprecationWarning):
ParameterValue(40j, self.date_time)
with self.assertWarns(PendingDeprecationWarning):
self.cals.add_parameter_value(40j, "amp", schedule="xp")

def _add_parameters(self):
"""Helper function."""

Expand Down
Loading

0 comments on commit 5459cc1

Please sign in to comment.