Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flux-Drive timing #1056

Open
wants to merge 16 commits into
base: 0.2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/protocols/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@ In this section we introduce the basics of all protocols supported by ``qibocal`
standard_rb
chevron
virtual_z
xyz_timing
state_tomographies

references
Binary file added doc/source/protocols/xyz_timing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions doc/source/protocols/xyz_timing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
XYZ-TIMING
==========

This routine is evaluating the different time of arrival of the flux and drive
Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved
pulse to the qubit. These delays are usually originated by the differences in
Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved
cable length or by the electronics.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With "by the electronics" what do you mean? An asynchronization between the drive and the flux signals on the electronics themselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An asynchronization between the drive and the flux signals on the electronics themselves?

Yes


In this procedure, we vary the relative start times of the flux and drive pulses,
keeping their durations identical.
By measuring the probability of the qubit being in the excited state,
we identify the point where the two pulses arrive simultaneously.
At this point, the probability drops to zero because the qubit is off-resonance,
allowing us to determine the delay.

Parameters
^^^^^^^^^^

.. autoclass::
qibocal.protocols.xyz_timing.XYZTimingParameters
:noindex:

Example
^^^^^^^
It follows a runcard example of this experiment and the report.

.. code-block:: yaml

- id: xyz_timing
operation: xyz_timing
parameters:
delay_step: 1
delay_stop: 100
flux_amplitude: 0.3

.. image:: xyz_timing.png
2 changes: 2 additions & 0 deletions src/qibocal/protocols/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
optimize_two_qubit_gate,
)
from .two_qubit_state_tomography import two_qubit_state_tomography
from .xyz_timing import xyz_timing

__all__ = [
"allxy",
Expand Down Expand Up @@ -104,4 +105,5 @@
"standard_rb_2q_inter",
"optimize_two_qubit_gate",
"ramsey_zz",
"xyz_timing",
]
255 changes: 255 additions & 0 deletions src/qibocal/protocols/xyz_timing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
from dataclasses import dataclass, field

import numpy as np
import numpy.typing as npt
import plotly.graph_objects as go
from qibo.models.error_mitigation import curve_fit
from qibolab import AcquisitionType, AveragingMode, Custom, Delay, Pulse, PulseSequence
from scipy import special

from qibocal.auto.operation import Data, Parameters, Results, Routine
from qibocal.calibration.calibration import QubitId
from qibocal.calibration.platform import CalibrationPlatform
from qibocal.result import probability

from .utils import table_dict, table_html

COLORBAND = "rgba(0,100,80,0.2)"
COLORBAND_LINE = "rgba(255,255,255,0)"
PADDING_FLUX = 10
Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved


@dataclass
class XYZTimingResults(Results):
"""Outputs of the xyz-timing protocol."""

fitted_parameters: dict[QubitId, list[float]]
"""Output parameters of the fitted function."""
fitted_errors: dict[QubitId, list[float]]
"""Errors of the fit parameters."""
delays: dict[QubitId, float]
"""Flux-drive delays."""


@dataclass
class XYZTimingParameters(Parameters):
"""XYZ-timing runcard inputs."""

flux_amplitude: float
"""Amplitude of the flux pulse."""
delay_step: int
"""Sweeper step value of the relative starts."""
delay_stop: int
"""Sweeper stop value of the relative starts."""


XYZTimingType = np.dtype(
[("delay", np.float64), ("prob", np.float64), ("errors", np.float64)]
)


@dataclass
class XYZTimingData(Data):

pulse_duration: dict[QubitId, int]
"""Duration of the drive and flux pulse"""
data: dict[QubitId, npt.NDArray] = field(default_factory=dict)
"""Raw data acquired."""


def _acquisition(
params: XYZTimingParameters,
platform: CalibrationPlatform,
targets: list[QubitId],
) -> XYZTimingData:
"""Data acquisition for drive-flux channels timing"""
natives = platform.natives.single_qubit
durations = np.arange(
1,
params.delay_stop,
params.delay_step,
)
sequences = []
ro_pulses = []
drive_durations = {}
for qubit in targets:
drive_pulse = natives[qubit].RX()[0]
drive_durations[qubit] = int(drive_pulse[1].duration)

data = XYZTimingData(pulse_duration=drive_durations)
for duration in durations:
ro_pulses.append([])
for qubit in targets:
sequence = PulseSequence()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you create the sequence at each iteration of the for loop over targets only the last qubit in target will be present in the sequence.

drive_channel = platform.qubits[qubit].drive
flux_channel = platform.qubits[qubit].flux
ro_channel = platform.qubits[qubit].acquisition
drive_pulse = natives[qubit].RX()[0]
readout_pulse = natives[qubit].MZ()[0]
Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved
drive_duration = int(drive_pulse[1].duration)
total_flux_duration = duration + drive_duration + PADDING_FLUX
flux_pulse = Pulse(
duration=total_flux_duration,
amplitude=params.flux_amplitude,
relative_phase=0,
envelope=Custom(
i_=np.concatenate(
[
np.zeros(duration),
np.ones(drive_duration),
np.zeros(PADDING_FLUX),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we already discussed about this, but what is the purpose of this padding?
I tried to remove it when running and I got the exact same results.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have put it there for testing and forgot to remove it.

Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved
]
),
q_=np.zeros(total_flux_duration),
),
)
qd_delay = Delay(duration=drive_duration)

sequence.extend(
[
(drive_channel, qd_delay),
drive_pulse,
(flux_channel, flux_pulse),
]
)
sequence.align([drive_channel, flux_channel, ro_channel])
sequence.append(readout_pulse)
sequences.append(sequence)
ro_pulses[-1].append(readout_pulse)

results = platform.execute(
sequences,
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=AveragingMode.SINGLESHOT,
)
for i, duration in enumerate(durations):
for j, qubit in enumerate(targets):
ro_pulse = ro_pulses[i][j][1]
prob = probability(results[ro_pulse.id], state=1)
error = np.sqrt(prob * (1 - prob) / params.nshots)
data.register_qubit(
XYZTimingType,
(qubit),
dict(
delay=np.array([duration]),
prob=np.array([prob]),
errors=np.array([error]),
),
)
return data


def fit_function(x, a, b, c, d, e):
return a + b * (special.erf(e * x - c) - special.erf(e * x + d))
Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved


def _fit(data: XYZTimingData) -> XYZTimingResults:
"""Post-processing for drive-flux channels timing"""
qubits = data.qubits
params = {}
errors = {}
delays = {}
for qubit in qubits:
Edoardo-Pedicillo marked this conversation as resolved.
Show resolved Hide resolved
data_qubit = data.data[qubit]
delay = data_qubit.delay
prob = data_qubit.prob
err = data_qubit.errors
initial_pars = [
1,
0.5,
data.pulse_duration[qubit] / 2,
data.pulse_duration[qubit] / 2,
1,
]
fit_parameters, perr = curve_fit(
fit_function,
delay - data.pulse_duration[qubit],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point I would just store directly this value in the data structure given that you also repeat the same line in the plotting function.

prob,
p0=initial_pars,
sigma=err,
)

err = np.sqrt(np.diag(perr)).tolist()
params[qubit] = fit_parameters.tolist()
errors[qubit] = err
delays[qubit] = [
(fit_parameters[2] - fit_parameters[3]) / (2 * fit_parameters[4]),
float(np.linalg.norm([err[2], err[3]]) / 2) / fit_parameters[4] ** 2,
]
return XYZTimingResults(
fitted_parameters=params,
fitted_errors=errors,
delays=delays,
)


def _plot(data: XYZTimingData, target: QubitId, fit: XYZTimingResults = None):
"""Plotting function for drive-flux channels timing"""
figures = []
qubit_data = data.data[target]
delays = qubit_data.delay
probs = qubit_data.prob
error_bars = qubit_data.errors
x = delays - data.pulse_duration[target]
fitting_report = ""
fig = go.Figure(
[
go.Scatter(
x=x,
y=probs,
opacity=1,
name="Probability of State 1",
showlegend=True,
legendgroup="Probability of State 1",
mode="lines",
),
go.Scatter(
x=np.concatenate((x, x[::-1])),
y=np.concatenate((probs + error_bars, (probs - error_bars)[::-1])),
fill="toself",
fillcolor=COLORBAND,
line=dict(color=COLORBAND_LINE),
showlegend=True,
name="Errors",
),
]
)
if fit is not None:
fig.add_trace(
go.Scatter(
x=x,
y=fit_function(x, *fit.fitted_parameters[target]),
opacity=1,
name="Fit",
showlegend=True,
legendgroup="Probability of State 0",
mode="lines",
),
)
fig.add_vline(
x=fit.delays[target][0],
line=dict(color="grey", width=3, dash="dash"),
)
fitting_report = table_html(
table_dict(
target,
["Flux pulse delay [ns]"],
[fit.delays[target]],
display_error=True,
)
)

fig.update_layout(
showlegend=True,
xaxis_title="Time [ns]",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put a bit more details here.

yaxis_title="Excited state probability",
)

figures.append(fig)

return figures, fitting_report


xyz_timing = Routine(_acquisition, _fit, _plot)
7 changes: 7 additions & 0 deletions tests/runcards/protocols.yml
Original file line number Diff line number Diff line change
Expand Up @@ -850,3 +850,10 @@ actions:
parameters:
nshots: 1024
circuit: tests/circuit2q.json

- id: xyz_timing
operation: xyz_timing
parameters:
delay_step: 1
delay_stop: 10
flux_amplitude: 0.3
Loading