Repository: quantumlib/Cirq

Issue:
Add WaitOperation, T1 decay experiment, and an example
Resolving this issue requires doing the following parts (not all in the same PR):

1. Introduce a 0-qubit `WaitOperation` with a `cirq.value.Duration` attribute. Modify circuit drawing code to draw it (see `_draw_moment_in_diagram` in `cirq/circuits/circuit.py`). It's not immediately clear to me the best way to define the concept of waiting, e.g. maybe it should be applied to specific qubits; there could be debate around it (e.g. should it allow a symbolic duration?). But a 0-qubit operation seems like a reasonable way.

2. Create a `cirq.t1_decay` method (crib from `cirq.rabi_oscillations`) which runs the circuit `X(a), wait(t), measure(a)` with increasing values of `t`.

3. Add an example to `examples/` that defines a noise model that looks for wait gates and adds proportional decay, and then uses the `t1_decay` to infer the parameter passed into that noise model.


Discussion:
It feels like this is what scheduled operation was built for?
Yes, but it would be nice if you could do a delay without having to go full schedule.

Resolving patch:
diff --git a/cirq/experiments/__init__.py b/cirq/experiments/__init__.py
--- a/cirq/experiments/__init__.py
+++ b/cirq/experiments/__init__.py
@@ -25,3 +25,8 @@
     log_xeb_fidelity_from_probabilities,
     xeb_fidelity,
 )
+
+from cirq.experiments.t1_decay_experiment import (
+    t1_decay,
+    T1DecayResult,
+)
diff --git a/cirq/experiments/t1_decay_experiment.py b/cirq/experiments/t1_decay_experiment.py
new file mode 100644
--- /dev/null
+++ b/cirq/experiments/t1_decay_experiment.py
@@ -0,0 +1,155 @@
+# Copyright 2019 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Any, Optional, TYPE_CHECKING
+
+import pandas as pd
+import sympy
+from matplotlib import pyplot as plt
+
+from cirq import circuits, devices, ops, study, value, work
+from cirq._compat import proper_repr
+
+if TYPE_CHECKING:
+    import cirq
+
+
+def t1_decay(sampler: work.Sampler,
+             *,
+             qubit: devices.GridQubit,
+             num_points: int,
+             max_delay: 'cirq.DURATION_LIKE',
+             min_delay: 'cirq.DURATION_LIKE' = None,
+             repetitions: int = 1000) -> 'cirq.experiments.T1DecayResult':
+    """Runs a t1 decay experiment.
+
+    Initializes a qubit into the |1⟩ state, waits for a variable amount of time,
+    and measures the qubit. Plots how often the |1⟩ state is observed for each
+    amount of waiting.
+
+    Args:
+        sampler: The quantum engine or simulator to run the circuits.
+        qubit: The qubit under test.
+        num_points: The number of evenly spaced delays to test.
+        max_delay: The largest delay to test.
+        min_delay: The smallest delay to test. Defaults to no delay.
+        repetitions: The number of repetitions of the circuit for each delay.
+
+    Returns:
+        A T1DecayResult object that stores and can plot the data.
+    """
+    min_delay_dur = value.Duration(min_delay)
+    max_delay_dur = value.Duration(max_delay)
+
+    if repetitions <= 0:
+        raise ValueError('repetitions <= 0')
+    if max_delay_dur < min_delay_dur:
+        raise ValueError('max_delay < min_delay')
+    if min_delay_dur < 0:
+        raise ValueError('min_delay < 0')
+    var = sympy.Symbol('delay_ns')
+
+    sweep = study.Linspace(var,
+                           start=min_delay_dur.total_nanos(),
+                           stop=max_delay_dur.total_nanos(),
+                           length=num_points)
+
+    circuit = circuits.Circuit(
+        ops.X(qubit),
+        ops.WaitGate(value.Duration(nanos=var)).on(qubit),
+        ops.measure(qubit, key='output'),
+    )
+
+    results = sampler.sample(circuit, params=sweep, repetitions=repetitions)
+
+    # Cross tabulate into a delay_ns, false_count, true_count table.
+    tab = pd.crosstab(results.delay_ns, results.output)
+    del tab.columns.name
+    tab = tab.rename(columns={0: 'false_count', 1: 'true_count'}).reset_index()
+    for col_index, name in [(1, 'false_count'), (2, 'true_count')]:
+        if name not in tab:
+            tab.insert(col_index, name, [0] * tab.shape[0])
+
+    return T1DecayResult(tab)
+
+
+class T1DecayResult:
+    """Results from a Rabi oscillation experiment."""
+
+    def __init__(self, data: pd.DataFrame):
+        """
+        Args:
+            data: A data frame with three columns:
+                delay_ns, false_count, true_count.
+        """
+        assert list(data.columns) == ['delay_ns', 'false_count', 'true_count']
+        self._data = data
+
+    @property
+    def data(self) -> pd.DataFrame:
+        """A data frame with delay_ns, false_count, true_count columns."""
+        return self._data
+
+    def plot(self, ax: Optional[plt.Axes] = None,
+             **plot_kwargs: Any) -> plt.Axes:
+        """Plots the excited state probability vs the amount of delay.
+
+        Args:
+            ax: the plt.Axes to plot on. If not given, a new figure is created,
+                plotted on, and shown.
+            **plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
+
+        Returns:
+            The plt.Axes containing the plot.
+        """
+        show_plot = not ax
+        if show_plot:
+            fig, ax = plt.subplots(1, 1, figsize=(8, 8))
+        assert ax is not None
+        ax.set_ylim(ymin=0, ymax=1)
+
+        xs = self._data['delay_ns']
+        ts = self._data['true_count']
+        fs = self._data['false_count']
+
+        ax.plot(xs, ts / (fs + ts), 'ro-', **plot_kwargs)
+        ax.set_xlabel(
+            r"Delay between initialization and measurement (nanoseconds)")
+        ax.set_ylabel('Excited State Probability')
+        ax.set_title('T1 Decay Experiment Data')
+        if show_plot:
+            fig.show()
+        return ax
+
+    def __str__(self):
+        return f'T1DecayResult with data:\n{self.data}'
+
+    def __eq__(self, other):
+        if not isinstance(other, type(self)):
+            return NotImplemented
+        return self.data.equals(other.data)
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __repr__(self):
+        return f'cirq.experiments.T1DecayResult(data={proper_repr(self.data)})'
+
+    def _repr_pretty_(self, p: Any, cycle: bool) -> None:
+        """Text output in Jupyter."""
+        if cycle:
+            # There should never be a cycle.  This is just in case.
+            p.text('T1DecayResult(...)')
+        else:
+            p.text(str(self))


Test patch:
diff --git a/cirq/experiments/t1_decay_experiment_test.py b/cirq/experiments/t1_decay_experiment_test.py
new file mode 100644
--- /dev/null
+++ b/cirq/experiments/t1_decay_experiment_test.py
@@ -0,0 +1,195 @@
+# Copyright 2019 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import pandas as pd
+
+import cirq
+
+
+def test_init_result():
+    data = pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                        index=range(2),
+                        data=[
+                            [100.0, 0, 10],
+                            [1000.0, 10, 0],
+                        ])
+    result = cirq.experiments.T1DecayResult(data)
+    assert result.data is data
+
+
+def test_plot_does_not_raise_error():
+
+    class _TimeDependentDecay(cirq.NoiseModel):
+
+        def noisy_moment(self, moment, system_qubits):
+            duration = max((op.gate.duration
+                            for op in moment.operations
+                            if isinstance(op.gate, cirq.WaitGate)),
+                           default=cirq.Duration(nanos=1))
+            yield cirq.amplitude_damp(1 - 0.99**duration.total_nanos()).on_each(
+                system_qubits)
+            yield moment
+
+    results = cirq.experiments.t1_decay(
+        sampler=cirq.DensityMatrixSimulator(noise=_TimeDependentDecay()),
+        qubit=cirq.GridQubit(0, 0),
+        num_points=3,
+        repetitions=10,
+        max_delay=cirq.Duration(nanos=500))
+    results.plot()
+
+
+def test_result_eq():
+    eq = cirq.testing.EqualsTester()
+    eq.make_equality_group(lambda: cirq.experiments.T1DecayResult(
+        data=pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                          index=[0],
+                          data=[
+                              [100.0, 2, 8],
+                          ])))
+    eq.add_equality_group(
+        cirq.experiments.T1DecayResult(
+            data=pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                              index=[0],
+                              data=[
+                                  [100.0, 2, 50002],
+                              ])))
+
+
+def test_sudden_decay_results():
+
+    class _SuddenDecay(cirq.NoiseModel):
+
+        def noisy_moment(self, moment, system_qubits):
+            duration = max((op.gate.duration
+                            for op in moment.operations
+                            if isinstance(op.gate, cirq.WaitGate)),
+                           default=cirq.Duration())
+            if duration > cirq.Duration(nanos=500):
+                yield cirq.amplitude_damp(1).on_each(system_qubits)
+            yield moment
+
+    results = cirq.experiments.t1_decay(
+        sampler=cirq.DensityMatrixSimulator(noise=_SuddenDecay()),
+        qubit=cirq.GridQubit(0, 0),
+        num_points=4,
+        repetitions=10,
+        min_delay=cirq.Duration(nanos=100),
+        max_delay=cirq.Duration(micros=1))
+    assert results == cirq.experiments.T1DecayResult(
+        data=pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                          index=range(4),
+                          data=[
+                              [100.0, 0, 10],
+                              [400.0, 0, 10],
+                              [700.0, 10, 0],
+                              [1000.0, 10, 0],
+                          ]))
+
+
+def test_all_on_results():
+    results = cirq.experiments.t1_decay(sampler=cirq.Simulator(),
+                                        qubit=cirq.GridQubit(0, 0),
+                                        num_points=4,
+                                        repetitions=10,
+                                        min_delay=cirq.Duration(nanos=100),
+                                        max_delay=cirq.Duration(micros=1))
+    assert results == cirq.experiments.T1DecayResult(
+        data=pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                          index=range(4),
+                          data=[
+                              [100.0, 0, 10],
+                              [400.0, 0, 10],
+                              [700.0, 0, 10],
+                              [1000.0, 0, 10],
+                          ]))
+
+
+def test_all_off_results():
+    results = cirq.experiments.t1_decay(
+        sampler=cirq.DensityMatrixSimulator(noise=cirq.amplitude_damp(1)),
+        qubit=cirq.GridQubit(0, 0),
+        num_points=4,
+        repetitions=10,
+        min_delay=cirq.Duration(nanos=100),
+        max_delay=cirq.Duration(micros=1))
+    assert results == cirq.experiments.T1DecayResult(
+        data=pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                          index=range(4),
+                          data=[
+                              [100.0, 10, 0],
+                              [400.0, 10, 0],
+                              [700.0, 10, 0],
+                              [1000.0, 10, 0],
+                          ]))
+
+
+def test_bad_args():
+    with pytest.raises(ValueError, match='repetitions <= 0'):
+        _ = cirq.experiments.t1_decay(sampler=cirq.Simulator(),
+                                      qubit=cirq.GridQubit(0, 0),
+                                      num_points=4,
+                                      repetitions=0,
+                                      max_delay=cirq.Duration(micros=1))
+
+    with pytest.raises(ValueError, match='max_delay < min_delay'):
+        _ = cirq.experiments.t1_decay(sampler=cirq.Simulator(),
+                                      qubit=cirq.GridQubit(0, 0),
+                                      num_points=4,
+                                      repetitions=10,
+                                      min_delay=cirq.Duration(micros=1),
+                                      max_delay=cirq.Duration(micros=0))
+
+    with pytest.raises(ValueError, match='min_delay < 0'):
+        _ = cirq.experiments.t1_decay(sampler=cirq.Simulator(),
+                                      qubit=cirq.GridQubit(0, 0),
+                                      num_points=4,
+                                      repetitions=10,
+                                      max_delay=cirq.Duration(micros=1),
+                                      min_delay=cirq.Duration(micros=-1))
+
+
+def test_str():
+
+    result = cirq.experiments.T1DecayResult(
+        data=pd.DataFrame(columns=['delay_ns', 'false_count', 'true_count'],
+                          index=range(2),
+                          data=[
+                              [100.0, 0, 10],
+                              [1000.0, 10, 0],
+                          ]))
+
+    assert str(result) == ('T1DecayResult with data:\n'
+                           '   delay_ns  false_count  true_count\n'
+                           '0     100.0            0          10\n'
+                           '1    1000.0           10           0')
+    cirq.testing.assert_equivalent_repr(result)
+
+    class FakePrinter:
+
+        def __init__(self):
+            self.text_pretty = ''
+
+        def text(self, to_print):
+            self.text_pretty += to_print
+
+    p = FakePrinter()
+    result._repr_pretty_(p, False)
+    assert p.text_pretty == str(result)
+
+    p = FakePrinter()
+    result._repr_pretty_(p, True)
+    assert p.text_pretty == 'T1DecayResult(...)'
diff --git a/cirq/testing/equivalent_repr_eval.py b/cirq/testing/equivalent_repr_eval.py
--- a/cirq/testing/equivalent_repr_eval.py
+++ b/cirq/testing/equivalent_repr_eval.py
@@ -15,12 +15,14 @@
 from typing import Any, Dict, Optional
 
 
-def assert_equivalent_repr(
-        value: Any,
-        *,
-        setup_code: str = 'import cirq\nimport numpy as np\nimport sympy',
-        global_vals: Optional[Dict[str, Any]] = None,
-        local_vals: Optional[Dict[str, Any]] = None) -> None:
+def assert_equivalent_repr(value: Any,
+                           *,
+                           setup_code: str = ('import cirq\n'
+                                              'import numpy as np\n'
+                                              'import sympy\n'
+                                              'import pandas as pd\n'),
+                           global_vals: Optional[Dict[str, Any]] = None,
+                           local_vals: Optional[Dict[str, Any]] = None) -> None:
     """Checks that eval(repr(v)) == v.
 
     Args: