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

Ramsey unrolling #666

Merged
merged 10 commits into from
Feb 1, 2024
140 changes: 91 additions & 49 deletions src/qibocal/protocols/characterization/ramsey.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class RamseyParameters(Parameters):
n_osc: Optional[int] = 0
"""Number of oscillations to induce detuning (optional).
If 0 standard Ramsey experiment is performed."""
unrolling: bool = False
"""If ``True`` it uses sequence unrolling to deploy multiple sequences in a single instrument call.
Defaults to ``False``."""


@dataclass
Expand Down Expand Up @@ -95,25 +98,6 @@ def _acquisition(
"""Data acquisition for Ramsey Experiment (detuned)."""
# create a sequence of pulses for the experiment
# RX90 - t - RX90 - MZ
ro_pulses = {}
RX90_pulses1 = {}
RX90_pulses2 = {}
freqs = {}
sequence = PulseSequence()
for qubit in qubits:
RX90_pulses1[qubit] = platform.create_RX90_pulse(qubit, start=0)
RX90_pulses2[qubit] = platform.create_RX90_pulse(
qubit,
start=RX90_pulses1[qubit].finish,
)
ro_pulses[qubit] = platform.create_qubit_readout_pulse(
qubit, start=RX90_pulses2[qubit].finish
)
freqs[qubit] = qubits[qubit].drive_frequency
sequence.add(RX90_pulses1[qubit])
sequence.add(RX90_pulses2[qubit])
sequence.add(ro_pulses[qubit])

# define the parameter to sweep and its range:
waits = np.arange(
# wait time between RX90 pulses
Expand All @@ -122,14 +106,40 @@ def _acquisition(
params.delay_between_pulses_step,
)

data = RamseyData(
n_osc=params.n_osc,
t_max=params.delay_between_pulses_end,
detuning_sign=+1,
qubit_freqs=freqs,
options = ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=AveragingMode.SINGLESHOT,
)

if params.n_osc == 0:
ro_pulses = {}
RX90_pulses1 = {}
RX90_pulses2 = {}
freqs = {}
sequence = PulseSequence()
for qubit in qubits:
RX90_pulses1[qubit] = platform.create_RX90_pulse(qubit, start=0)
RX90_pulses2[qubit] = platform.create_RX90_pulse(
qubit,
start=RX90_pulses1[qubit].finish,
)
ro_pulses[qubit] = platform.create_qubit_readout_pulse(
qubit, start=RX90_pulses2[qubit].finish
)
freqs[qubit] = qubits[qubit].drive_frequency
sequence.add(RX90_pulses1[qubit])
sequence.add(RX90_pulses2[qubit])
sequence.add(ro_pulses[qubit])

data = RamseyData(
n_osc=params.n_osc,
t_max=params.delay_between_pulses_end,
detuning_sign=+1,
qubit_freqs=freqs,
)

sweeper = Sweeper(
Parameter.start,
waits,
Expand All @@ -140,12 +150,7 @@ def _acquisition(
# execute the sweep
results = platform.sweep(
sequence,
ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=AveragingMode.SINGLESHOT,
),
options,
sweeper,
)
for qubit in qubits:
Expand All @@ -161,31 +166,67 @@ def _acquisition(
errors=errors,
),
)
else:
if params.n_osc != 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a lot of repeated code under the cases n_osc == 0 and n_osc != 0. I would like to suggest unifying them, or moving the repeated functionality into a function. It would be even nicer if the ramsey_signal also benefits from such unification. Unfortunately I am not able to understand what is the meaning of n_osc from its documentation, so cannot suggest a more concrete refactoring. Let's discuss.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks @hay-k. We should definitely find a way to improve it. Just for reference n_osc stands for number of oscillations. In the ramsey protocol by putting a non zero integer value we include a detuning through the relative phase that will generate n_osc number of oscillations in the plot.

RX90_pulses2[qubit].relative_phase = (
RX90_pulses2[qubit].start
* (-2 * np.pi)
* (params.n_osc)
/ params.delay_between_pulses_end
)

Having said this, I believe that in ramsey we differentiate between the n_osc==0 and the n_osc!=0 case because in the first case the protocol can be implemented through a simple sweeper on the start of the second RX90 pulse while in the second case, if I remember correctly, there are some limitations in our drivers that prevents us from sweeping start and relative phase at the same time. @stavros11 feel free to correct me if I said something wrong.

Of course the best solution for qibocal should be to keep the code as clean as possible and let the drivers handle these specific cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, thanks for the explanation! I think, in general, the drivers (and their developers, of course) will benefit a lot from a more unified and consistent implementation. Anyways, that's a story for another day. I believe we should still simplify the code in this particular instance, even without improving anything else anywhere else. That should be doable, and I am here to assist. @Jacfomg probably best if we discuss AFK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, it's sounds like a good plan.

sequences, all_ro_pulses = [], []
for wait in waits:
ro_pulses = {}
RX90_pulses1 = {}
RX90_pulses2 = {}
freqs = {}
sequence = PulseSequence()
for qubit in qubits:
RX90_pulses1[qubit] = platform.create_RX90_pulse(qubit, start=0)
RX90_pulses2[qubit] = platform.create_RX90_pulse(
qubit,
start=RX90_pulses1[qubit].finish,
)
ro_pulses[qubit] = platform.create_qubit_readout_pulse(
qubit, start=RX90_pulses2[qubit].finish
)

RX90_pulses2[qubit].start = RX90_pulses1[qubit].finish + wait
ro_pulses[qubit].start = RX90_pulses2[qubit].finish
if params.n_osc != 0:
RX90_pulses2[qubit].relative_phase = (
RX90_pulses2[qubit].start
* (-2 * np.pi)
* (params.n_osc)
/ params.delay_between_pulses_end
)

results = platform.execute_pulse_sequence(
sequence,
ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.DISCRIMINATION,
averaging_mode=(AveragingMode.SINGLESHOT),
),
)

RX90_pulses2[qubit].relative_phase = (
RX90_pulses2[qubit].start
* (-2 * np.pi)
* (params.n_osc)
/ params.delay_between_pulses_end
)

freqs[qubit] = qubits[qubit].drive_frequency
sequence.add(RX90_pulses1[qubit])
sequence.add(RX90_pulses2[qubit])
sequence.add(ro_pulses[qubit])

sequences.append(sequence)
all_ro_pulses.append(ro_pulses)

data = RamseyData(
n_osc=params.n_osc,
t_max=params.delay_between_pulses_end,
detuning_sign=+1,
qubit_freqs=freqs,
)

if params.unrolling:
results = platform.execute_pulse_sequences(sequences, options)

elif not params.unrolling:
results = [
platform.execute_pulse_sequence(sequence, options)
for sequence in sequences
]

# We dont need ig as everty serial is different
for ig, (wait, ro_pulses) in enumerate(zip(waits, all_ro_pulses)):
for qubit in qubits:
prob = results[qubit].probability()
serial = ro_pulses[qubit].serial
if params.unrolling:
result = results[serial][0]
else:
result = results[ig][serial]
prob = result.probability()
error = np.sqrt(prob * (1 - prob) / params.nshots)
data.register_qubit(
RamseyType,
Expand All @@ -196,6 +237,7 @@ def _acquisition(
errors=np.array([error]),
),
)

return data


Expand Down
135 changes: 86 additions & 49 deletions src/qibocal/protocols/characterization/ramsey_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,6 @@ def _acquisition(
"""Data acquisition for Ramsey Experiment (detuned)."""
# create a sequence of pulses for the experiment
# RX90 - t - RX90 - MZ
ro_pulses = {}
RX90_pulses1 = {}
RX90_pulses2 = {}
freqs = {}
sequence = PulseSequence()
for qubit in qubits:
RX90_pulses1[qubit] = platform.create_RX90_pulse(qubit, start=0)
RX90_pulses2[qubit] = platform.create_RX90_pulse(
qubit,
start=RX90_pulses1[qubit].finish,
)
ro_pulses[qubit] = platform.create_qubit_readout_pulse(
qubit, start=RX90_pulses2[qubit].finish
)
freqs[qubit] = qubits[qubit].drive_frequency
sequence.add(RX90_pulses1[qubit])
sequence.add(RX90_pulses2[qubit])
sequence.add(ro_pulses[qubit])

# define the parameter to sweep and its range:
waits = np.arange(
# wait time between RX90 pulses
Expand All @@ -106,30 +87,50 @@ def _acquisition(
params.delay_between_pulses_step,
)

data = RamseySignalData(
n_osc=params.n_osc,
t_max=params.delay_between_pulses_end,
detuning_sign=+1,
qubit_freqs=freqs,
options = ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.INTEGRATION,
averaging_mode=AveragingMode.CYCLIC,
)

if params.n_osc == 0:
ro_pulses = {}
RX90_pulses1 = {}
RX90_pulses2 = {}
freqs = {}
sequence = PulseSequence()
for qubit in qubits:
RX90_pulses1[qubit] = platform.create_RX90_pulse(qubit, start=0)
RX90_pulses2[qubit] = platform.create_RX90_pulse(
qubit,
start=RX90_pulses1[qubit].finish,
)
ro_pulses[qubit] = platform.create_qubit_readout_pulse(
qubit, start=RX90_pulses2[qubit].finish
)
freqs[qubit] = qubits[qubit].drive_frequency
sequence.add(RX90_pulses1[qubit])
sequence.add(RX90_pulses2[qubit])
sequence.add(ro_pulses[qubit])

sweeper = Sweeper(
Parameter.start,
waits,
[RX90_pulses2[qubit] for qubit in qubits],
type=SweeperType.ABSOLUTE,
)

data = RamseySignalData(
n_osc=params.n_osc,
t_max=params.delay_between_pulses_end,
detuning_sign=+1,
qubit_freqs=freqs,
)
# execute the sweep
results = platform.sweep(
sequence,
ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.INTEGRATION,
averaging_mode=AveragingMode.CYCLIC,
),
options,
sweeper,
)
for qubit in qubits:
Expand All @@ -141,36 +142,72 @@ def _acquisition(
signal=result.magnitude,
)

else:
if params.n_osc != 0:
sequences, all_ro_pulses = [], []
for wait in waits:
ro_pulses = {}
RX90_pulses1 = {}
RX90_pulses2 = {}
freqs = {}
sequence = PulseSequence()
for qubit in qubits:
RX90_pulses1[qubit] = platform.create_RX90_pulse(qubit, start=0)
RX90_pulses2[qubit] = platform.create_RX90_pulse(
qubit,
start=RX90_pulses1[qubit].finish,
)
ro_pulses[qubit] = platform.create_qubit_readout_pulse(
qubit, start=RX90_pulses2[qubit].finish
)

RX90_pulses2[qubit].start = RX90_pulses1[qubit].finish + wait
ro_pulses[qubit].start = RX90_pulses2[qubit].finish
if params.n_osc != 0:
RX90_pulses2[qubit].relative_phase = (
RX90_pulses2[qubit].start
* (-2 * np.pi)
* (params.n_osc)
/ params.delay_between_pulses_end
)

results = platform.execute_pulse_sequence(
sequence,
ExecutionParameters(
nshots=params.nshots,
relaxation_time=params.relaxation_time,
acquisition_type=AcquisitionType.INTEGRATION,
averaging_mode=(AveragingMode.CYCLIC),
),
)

RX90_pulses2[qubit].relative_phase = (
RX90_pulses2[qubit].start
* (-2 * np.pi)
* (params.n_osc)
/ params.delay_between_pulses_end
)

freqs[qubit] = qubits[qubit].drive_frequency
sequence.add(RX90_pulses1[qubit])
sequence.add(RX90_pulses2[qubit])
sequence.add(ro_pulses[qubit])

sequences.append(sequence)
all_ro_pulses.append(ro_pulses)

data = RamseySignalData(
n_osc=params.n_osc,
t_max=params.delay_between_pulses_end,
detuning_sign=+1,
qubit_freqs=freqs,
)

if params.unrolling:
results = platform.execute_pulse_sequences(sequences, options)

elif not params.unrolling:
results = [
platform.execute_pulse_sequence(sequence, options)
for sequence in sequences
]

# We dont need ig as everty serial is different
for ig, (wait, ro_pulses) in enumerate(zip(waits, all_ro_pulses)):
for qubit in qubits:
result = results[ro_pulses[qubit].serial]
serial = ro_pulses[qubit].serial
if params.unrolling:
result = results[serial][0]
else:
result = results[ig][serial]
data.register_qubit(
qubit,
wait=wait,
signal=result.magnitude,
)

return data


Expand Down
Loading
Loading