Skip to content

Commit 6bdcb1c

Browse files
authored
Initial T2 experiment (#2725)
* Initial T2 experiment - Creates initial draft experiment to measure T2 and T2* - This experiment sets up a qubit on the Bloch sphere equator, evolves the system (optionally performing a spin echo half way), then measures the system in two bases to determine an approximate length of the Bloch vector.
1 parent c7a8c19 commit 6bdcb1c

File tree

4 files changed

+685
-0
lines changed

4 files changed

+685
-0
lines changed

cirq/experiments/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,8 @@
5252
t1_decay,
5353
T1DecayResult,
5454
)
55+
56+
from cirq.experiments.t2_decay_experiment import (
57+
t2_decay,
58+
T2DecayResult,
59+
)
+346
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# Copyright 2020 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import enum
15+
16+
from typing import Any, Optional, TYPE_CHECKING
17+
18+
import pandas as pd
19+
import sympy
20+
from matplotlib import pyplot as plt
21+
22+
from cirq import circuits, devices, ops, study, value, work
23+
from cirq._compat import proper_repr
24+
25+
if TYPE_CHECKING:
26+
import cirq
27+
28+
29+
class ExperimentType(enum.Enum):
30+
RAMSEY = 1 # Often denoted as t2*
31+
HAHN_ECHO = 2 # Spin echo or t2
32+
CPMG = 3 # Carr-Purcell-Meiboom-Gill sequence
33+
34+
35+
_T2_COLUMNS = ['delay_ns', 0, 1]
36+
37+
38+
def t2_decay(
39+
sampler: work.Sampler,
40+
*,
41+
qubit: devices.GridQubit,
42+
experiment_type: 'ExperimentType' = ExperimentType.RAMSEY,
43+
num_points: int,
44+
max_delay: 'cirq.DURATION_LIKE',
45+
min_delay: 'cirq.DURATION_LIKE' = None,
46+
repetitions: int = 1000,
47+
delay_sweep: Optional[study.Sweep] = None,
48+
) -> 'cirq.experiments.T2DecayResult':
49+
"""Runs a t2 transverse relaxation experiment.
50+
51+
Initializes a qubit into a superposition state, evolves the system using
52+
rules determined by the experiment type and by the delay parameters,
53+
then rotates back for measurement. This will measure the phase decoherence
54+
decay. This experiment has three types of T2 metrics, each which measure
55+
a different slice of the noise spectrum.
56+
57+
For the Ramsey experiment type (often denoted T2*), the state will be
58+
prepared with a square root Y gate (`cirq.Y ** 0.5`) and then waits for
59+
a variable amount of time. After this time, it will do basic state
60+
tomography to measure the expectation of the Pauli-X and Pauli-Y operators
61+
by performing either a `cirq.Y ** -0.5` or `cirq.X ** -0.5`. The square of
62+
these two measurements is summed to determine the length of the Bloch
63+
vector. This experiment measures the phase decoherence of the system under
64+
free evolution.
65+
66+
For the Hahn echo experiment (often denoted T2 or spin echo), the state
67+
will also be prepared with a square root Y gate (`cirq.Y ** 0.5`).
68+
However, during the mid-point of the delay time being measured, a pi-pulse
69+
(`cirq.X`) gate will be applied to cancel out inhomogeneous dephasing.
70+
The same method of measuring the final state as Ramsey experiment is applied
71+
after the second half of the delay period.
72+
73+
CPMG, or the Carr-Purcell-Meiboom-Gill sequence, is currently not
74+
implemented.
75+
76+
Args:
77+
sampler: The quantum engine or simulator to run the circuits.
78+
qubit: The qubit under test.
79+
experiment_type: The type of T2 test to run.
80+
num_points: The number of evenly spaced delays to test.
81+
max_delay: The largest delay to test.
82+
min_delay: The smallest delay to test. Defaults to no delay.
83+
repetitions: The number of repetitions of the circuit
84+
for each delay and for each tomography result.
85+
delay_sweep: Optional range of time delays to sweep across. This should
86+
be a SingleSweep using the 'delay_ns' with values in integer number
87+
of nanoseconds. If specified, this will override the max_delay and
88+
min_delay parameters. If not specified, the experiment will sweep
89+
from min_delay to max_delay with linear steps.
90+
Returns:
91+
A T2DecayResult object that stores and can plot the data.
92+
"""
93+
min_delay_dur = value.Duration(min_delay)
94+
max_delay_dur = value.Duration(max_delay)
95+
96+
# Input validation
97+
if repetitions <= 0:
98+
raise ValueError('repetitions <= 0')
99+
if max_delay_dur < min_delay_dur:
100+
raise ValueError('max_delay < min_delay')
101+
if min_delay_dur < 0:
102+
raise ValueError('min_delay < 0')
103+
104+
# Initialize values used in sweeps
105+
delay_var = sympy.Symbol('delay_ns')
106+
inv_x_var = sympy.Symbol('inv_x')
107+
inv_y_var = sympy.Symbol('inv_y')
108+
109+
if not delay_sweep:
110+
delay_sweep = study.Linspace(delay_var,
111+
start=min_delay_dur.total_nanos(),
112+
stop=max_delay_dur.total_nanos(),
113+
length=num_points)
114+
if delay_sweep.keys != ['delay_ns']:
115+
raise ValueError('delay_sweep must be a SingleSweep '
116+
'with delay_ns parameter')
117+
118+
if experiment_type == ExperimentType.RAMSEY:
119+
# Ramsey T2* experiment
120+
# Use sqrt(Y) to flip to the equator.
121+
# Evolve the state for a given amount of delay time
122+
# Then measure the state in both X and Y bases.
123+
124+
circuit = circuits.Circuit(
125+
ops.Y(qubit)**0.5,
126+
ops.WaitGate(value.Duration(nanos=delay_var))(qubit),
127+
ops.X(qubit)**inv_x_var,
128+
ops.Y(qubit)**inv_y_var,
129+
ops.measure(qubit, key='output'),
130+
)
131+
tomography_sweep = study.Zip(
132+
study.Points('inv_x', [0.0, -0.5]),
133+
study.Points('inv_y', [-0.5, 0.0]),
134+
)
135+
sweep = study.Product(delay_sweep, tomography_sweep)
136+
elif experiment_type == ExperimentType.HAHN_ECHO:
137+
# Hahn / Spin Echo T2 experiment
138+
# Use sqrt(Y) to flip to the equator.
139+
# Evolve the state for half the given amount of delay time
140+
# Flip the state using an X gate
141+
# Evolve the state for half the given amount of delay time
142+
# Then measure the state in both X and Y bases.
143+
144+
circuit = circuits.Circuit(
145+
ops.Y(qubit)**0.5,
146+
ops.WaitGate(value.Duration(nanos=0.5 * delay_var))(qubit),
147+
ops.X(qubit),
148+
ops.WaitGate(value.Duration(nanos=0.5 * delay_var))(qubit),
149+
ops.X(qubit)**inv_x_var,
150+
ops.Y(qubit)**inv_y_var,
151+
ops.measure(qubit, key='output'),
152+
)
153+
tomography_sweep = study.Zip(
154+
study.Points('inv_x', [0.0, 0.5]),
155+
study.Points('inv_y', [-0.5, 0.0]),
156+
)
157+
sweep = study.Product(delay_sweep, tomography_sweep)
158+
else:
159+
raise ValueError(f'Experiment type {experiment_type} not supported')
160+
161+
# Tabulate measurements into a histogram
162+
results = sampler.sample(circuit, params=sweep, repetitions=repetitions)
163+
164+
y_basis_measurements = results[abs(results.inv_y) > 0]
165+
x_basis_measurements = results[abs(results.inv_x) > 0]
166+
x_basis_tabulation = pd.crosstab(x_basis_measurements.delay_ns,
167+
x_basis_measurements.output).reset_index()
168+
y_basis_tabulation = pd.crosstab(y_basis_measurements.delay_ns,
169+
y_basis_measurements.output).reset_index()
170+
171+
# If all measurements are 1 or 0, fill in the missing column with all zeros.
172+
for tab in [x_basis_tabulation, y_basis_tabulation]:
173+
for col_index, name in [(1, 0), (2, 1)]:
174+
if name not in tab:
175+
tab.insert(col_index, name, [0] * tab.shape[0])
176+
177+
# Return the results in a container object
178+
return T2DecayResult(x_basis_tabulation, y_basis_tabulation)
179+
180+
181+
class T2DecayResult:
182+
"""Results from a T2 decay experiment.
183+
184+
This object is a container for the measurement results in each basis
185+
for each amount of delay. These can be used to calculate Pauli
186+
expectation values, length of the Bloch vector, and various fittings of
187+
the data to calculate estimated T2 phase decay times.
188+
"""
189+
190+
def __init__(self, x_basis_data: pd.DataFrame, y_basis_data: pd.DataFrame):
191+
"""
192+
Args:
193+
data: A data frame with three columns:
194+
delay_ns, false_count, true_count.
195+
"""
196+
x_cols = list(x_basis_data.columns)
197+
y_cols = list(y_basis_data.columns)
198+
if any(col not in x_cols for col in _T2_COLUMNS):
199+
raise ValueError(f'x_basis_data must have columns {_T2_COLUMNS} '
200+
f'but had {list(x_basis_data.columns)}')
201+
if any(col not in y_cols for col in _T2_COLUMNS):
202+
raise ValueError(f'y_basis_data must have columns {_T2_COLUMNS} '
203+
f'but had {list(y_basis_data.columns)}')
204+
self._x_basis_data = x_basis_data
205+
self._y_basis_data = y_basis_data
206+
self._expectation_pauli_x = self._expectation(x_basis_data)
207+
self._expectation_pauli_y = self._expectation(y_basis_data)
208+
209+
def _expectation(self, data) -> pd.DataFrame:
210+
"""Calculates the expected value of the Pauli operator.
211+
212+
Assuming that the data is measured in the Pauli basis of the operator,
213+
then the expectation of the Pauli operator would be +1 if the
214+
measurement is all ones and -1 if the measurement is all zeros.
215+
216+
Returns:
217+
Data frame with two columns 'delay_ns' and 'value'
218+
"""
219+
xs = data['delay_ns']
220+
ones = data[1]
221+
zeros = data[0]
222+
pauli_expectation = (2 * (ones / (ones + zeros))) - 1.0
223+
return pd.DataFrame({'delay_ns': xs, 'value': pauli_expectation})
224+
225+
@property
226+
def expectation_pauli_x(self) -> pd.DataFrame:
227+
"""A data frame with delay_ns, value columns.
228+
229+
This value contains the expectation of the Pauli X operator as
230+
estimated by measurement outcomes.
231+
"""
232+
return self._expectation_pauli_x
233+
234+
@property
235+
def expectation_pauli_y(self) -> pd.DataFrame:
236+
"""A data frame with delay_ns, value columns.
237+
238+
This value contains the expectation of the Pauli X operator as
239+
estimated by measurement outcomes.
240+
"""
241+
return self._expectation_pauli_y
242+
243+
def plot_expectations(self,
244+
ax: Optional[plt.Axes] = None,
245+
**plot_kwargs: Any) -> plt.Axes:
246+
"""Plots the expectation values of Pauli operators versus delay time.
247+
248+
Args:
249+
ax: the plt.Axes to plot on. If not given, a new figure is created,
250+
plotted on, and shown.
251+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
252+
253+
Returns:
254+
The plt.Axes containing the plot.
255+
"""
256+
show_plot = not ax
257+
if show_plot:
258+
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
259+
assert ax is not None
260+
ax.set_ylim(ymin=-2, ymax=2)
261+
262+
# Plot different expectation values in different colors.
263+
ax.plot(self._expectation_pauli_x['delay_ns'],
264+
self._expectation_pauli_x['value'],
265+
'bo-',
266+
label='<X>',
267+
**plot_kwargs)
268+
ax.plot(self._expectation_pauli_y['delay_ns'],
269+
self._expectation_pauli_y['value'],
270+
'go-',
271+
label='<Y>',
272+
**plot_kwargs)
273+
274+
ax.set_xlabel(
275+
r"Delay between initialization and measurement (nanoseconds)")
276+
ax.set_ylabel('Pauli Operator Expectation')
277+
ax.set_title('T2 Decay Pauli Expectations')
278+
ax.legend()
279+
if show_plot:
280+
fig.show()
281+
return ax
282+
283+
def plot_bloch_vector(self,
284+
ax: Optional[plt.Axes] = None,
285+
**plot_kwargs: Any) -> plt.Axes:
286+
"""Plots the estimated length of the Bloch vector versus time.
287+
288+
This plot estimates the Bloch Vector by squaring the Pauli expectation
289+
value of X and adding it to the square of the Pauli expectation value of
290+
Y. This essentially projects the state into the XY plane.
291+
292+
Note that Z expectation is not considered, since T1 related amplitude
293+
damping will generally push this value towards |0>
294+
(expectation <Z> = -1) which will significantly distort the T2 numbers.
295+
296+
Args:
297+
ax: the plt.Axes to plot on. If not given, a new figure is created,
298+
plotted on, and shown.
299+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
300+
301+
Returns:
302+
The plt.Axes containing the plot.
303+
"""
304+
show_plot = not ax
305+
if show_plot:
306+
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
307+
assert ax is not None
308+
ax.set_ylim(ymin=0, ymax=1)
309+
310+
# Estimate length of Bloch vector (projected to xy plane)
311+
# by squaring <X> and <Y> expectation values
312+
bloch_vector = (self._expectation_pauli_x**2 +
313+
self._expectation_pauli_y**2)
314+
315+
ax.plot(self._expectation_pauli_x['delay_ns'], bloch_vector, 'r+-',
316+
**plot_kwargs)
317+
ax.set_xlabel(
318+
r"Delay between initialization and measurement (nanoseconds)")
319+
ax.set_ylabel('Bloch Vector X-Y Projection Squared')
320+
ax.set_title('T2 Decay Experiment Data')
321+
if show_plot:
322+
fig.show()
323+
return ax
324+
325+
def __str__(self):
326+
return (f'T2DecayResult with data:\n'
327+
f'<X>\n{self._x_basis_data}\n<Y>\n{self._y_basis_data}')
328+
329+
def __eq__(self, other):
330+
if not isinstance(other, type(self)):
331+
return NotImplemented
332+
return (self._expectation_pauli_x.equals(other._expectation_pauli_x) and
333+
self._expectation_pauli_y.equals(other._expectation_pauli_y))
334+
335+
def __repr__(self):
336+
return (f'cirq.experiments.T2DecayResult('
337+
f'x_basis_data={proper_repr(self._x_basis_data)}, '
338+
f'y_basis_data={proper_repr(self._y_basis_data)})')
339+
340+
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
341+
"""Text output in Jupyter."""
342+
if cycle:
343+
# There should never be a cycle. This is just in case.
344+
p.text('T2DecayResult(...)')
345+
else:
346+
p.text(str(self))

0 commit comments

Comments
 (0)