Repository: quantumlib/Cirq

Issue:
Displays
@Strilanc I'd like to know your thoughts on this "display" concept that you mentioned at #942 . Do you imagine this as a separate class whose instances would be bundled with the Circuits sent to Quantum Engine?


Discussion:
A `Display` would be a special kind of gate that indicates "please tell me XX about the qubits I am targeting at this time". Displays are no-ops as far as the rest of the circuit is concerned (there would be dedicated runs of the circuit to compute each display's value, and for other runs the display is omitted).

Basically, it's exactly the concept of displays that I use in Quirk.
It might be useful to integrate this with the concept of an "acquaintance gate". One would commonly be interested in "displaying" the logical qubits (or modes in the fermionic case) mapped to the corresponding physical qubits at some point in time.
@Strilanc how does this affect the Quantum Engine API? Are you proposing to add a method separate from `run_sweep` that would be used to calculate displays?
I don't know. There might be separate methods, or the existing methods might be somehow generalized. It could be that we just have a `compute_displays` simulator.

Resolving patch:
diff --git a/cirq/__init__.py b/cirq/__init__.py
--- a/cirq/__init__.py
+++ b/cirq/__init__.py
@@ -79,33 +79,26 @@
 )
 
 from cirq.ops import (
-    amplitude_damp,
     AmplitudeDampingChannel,
-    asymmetric_depolarize,
+    ApproxPauliStringExpectation,
     AsymmetricDepolarizingChannel,
-    bit_flip,
     BitFlipChannel,
     CCX,
     CCXPowGate,
     CCZ,
     CCZPowGate,
-    SingleQubitCliffordGate,
     CNOT,
     CNotPowGate,
-    ControlledGate,
     CSWAP,
     CSwapGate,
     CZ,
     CZPowGate,
-    depolarize,
+    ControlledGate,
     DepolarizingChannel,
     EigenGate,
-    flatten_op_tree,
     FREDKIN,
-    freeze_op_tree,
     Gate,
     GateOperation,
-    generalized_amplitude_damp,
     GeneralizedAmplitudeDampingChannel,
     H,
     HPowGate,
@@ -122,32 +115,32 @@
     Pauli,
     PauliInteractionGate,
     PauliString,
+    PauliStringExpectation,
     PauliTransform,
-    phase_damp,
     PhaseDampingChannel,
-    phase_flip,
     PhaseFlipChannel,
     PhasedXPowGate,
     QubitId,
     QubitOrder,
     QubitOrderOrList,
     ReversibleCompositeGate,
-    rotation_error,
     RotationErrorChannel,
     Rx,
     Ry,
     Rz,
     S,
+    SWAP,
+    SamplesDisplay,
+    SingleQubitCliffordGate,
     SingleQubitGate,
     SingleQubitMatrixGate,
-    SWAP,
     SwapPowGate,
     T,
-    ThreeQubitGate,
     TOFFOLI,
-    transform_op_tree,
+    ThreeQubitGate,
     TwoQubitGate,
     TwoQubitMatrixGate,
+    WaveFunctionDisplay,
     X,
     XPowGate,
     XX,
@@ -160,6 +153,20 @@
     ZPowGate,
     ZZ,
     ZZPowGate,
+    amplitude_damp,
+    asymmetric_depolarize,
+    bit_flip,
+    depolarize,
+    flatten_op_tree,
+    freeze_op_tree,
+    generalized_amplitude_damp,
+    measure,
+    measure_each,
+    pauli_string_expectation,
+    phase_damp,
+    phase_flip,
+    rotation_error,
+    transform_op_tree,
 )
 
 from cirq.optimizers import (
@@ -187,11 +194,6 @@
 )
 
 from cirq.sim import (
-    bloch_vector_from_state_vector,
-    density_matrix_from_state_vector,
-    dirac_notation,
-    measure_state_vector,
-    sample_state_vector,
     SimulatesSamples,
     SimulationTrialResult,
     Simulator,
@@ -199,6 +201,11 @@
     StepResult,
     SimulatesFinalWaveFunction,
     SimulatesIntermediateWaveFunction,
+    bloch_vector_from_state_vector,
+    density_matrix_from_state_vector,
+    dirac_notation,
+    measure_state_vector,
+    sample_state_vector,
     to_valid_state_vector,
     validate_normalized_state,
 )
diff --git a/cirq/ops/__init__.py b/cirq/ops/__init__.py
--- a/cirq/ops/__init__.py
+++ b/cirq/ops/__init__.py
@@ -66,6 +66,13 @@
 from cirq.ops.controlled_gate import (
     ControlledGate,
 )
+from cirq.ops.display import (
+    ApproxPauliStringExpectation,
+    PauliStringExpectation,
+    SamplesDisplay,
+    WaveFunctionDisplay,
+    pauli_string_expectation,
+)
 from cirq.ops.eigen_gate import (
     EigenGate,
 )
diff --git a/cirq/ops/display.py b/cirq/ops/display.py
new file mode 100644
--- /dev/null
+++ b/cirq/ops/display.py
@@ -0,0 +1,206 @@
+# Copyright 2018 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.
+
+"""Classes to represent displays.
+
+A Display is an operation that signifies extracting some information about the
+qubits it is applied to without actually performing any effect on those qubits.
+Each Display in a circuit has an associated key, and the result of computing
+the values of Displays in a circuit is a dictionary from Display key to
+Display value.
+"""
+
+from typing import Any, Dict, Hashable, Optional, Tuple, Union, TYPE_CHECKING
+
+import abc
+
+import numpy as np
+
+from cirq import protocols, value
+from cirq.ops import op_tree, raw_types
+
+if TYPE_CHECKING:
+    # pylint: disable=unused-import
+    from cirq.ops import pauli_string
+
+
+class SamplesDisplay(raw_types.Operation):
+    """A display whose value is computed from measurement results.
+
+    The value of a SamplesDisplay on some qubits is computed in the following
+    steps:
+        1. Repeat the following some number of times:
+            a. Start with the state which exists just prior to the Moment
+               containing the SamplesDisplay
+            b. Perform some unitary operations on the qubits
+            c. Sample a bitstring from the resulting state
+        2. Apply a function to the sampled bitstrings
+    """
+
+    @property
+    @abc.abstractmethod
+    def key(self) -> Hashable:
+        pass
+
+    @abc.abstractmethod
+    def measurement_basis_change(self) -> op_tree.OP_TREE:
+        """Operations to perform prior to measurement."""
+        pass
+
+    @property
+    @abc.abstractmethod
+    def num_samples(self) -> int:
+        """The number of measurement samples to take."""
+        pass
+
+    @abc.abstractmethod
+    def value_derived_from_samples(self,
+                                   measurements: np.ndarray) -> Any:
+        """The value of the display, derived from measurement samples.
+
+        Args:
+            measurements: A 2-dimensional numpy array storing measurement
+                results. The first dimension corresponds to the sample and
+                the second to the actual boolean measurement results, ordered
+                by the qubits that were measured. Therefore, the array has
+                shape (self.num_samples, len(self.qubits))
+
+        Returns:
+            The value of the display.
+        """
+        pass
+
+
+class WaveFunctionDisplay(raw_types.Operation):
+    """A display whose value is computed from the full wavefunction."""
+
+    @property
+    @abc.abstractmethod
+    def key(self) -> Hashable:
+        pass
+
+    @abc.abstractmethod
+    def value_derived_from_wavefunction(self,
+                                        state: np.ndarray,
+                                        qubit_map: Dict[raw_types.QubitId, int]
+                                        ) -> Any:
+        """The value of the display, derived from the full wavefunction.
+
+        Args:
+            state: The wavefunction
+            qubit_map: A dictionary from qubit to qubit index in the
+                ordering used to define the wavefunction
+        """
+        pass
+
+
+@value.value_equality
+class ApproxPauliStringExpectation(SamplesDisplay):
+    """Approximate expectation value of a Pauli string."""
+
+    def __init__(self,
+                 pauli_string: 'pauli_string.PauliString',
+                 num_samples: int,
+                 key: Hashable=''):
+        self._pauli_string = pauli_string
+        self._num_samples = num_samples
+        self._key = key
+
+    @property
+    def qubits(self) -> Tuple[raw_types.QubitId, ...]:
+        return self._pauli_string.qubits
+
+    def with_qubits(self,
+                    *new_qubits: raw_types.QubitId
+                    ) -> 'ApproxPauliStringExpectation':
+        return ApproxPauliStringExpectation(
+                self._pauli_string.with_qubits(*new_qubits),
+                self._num_samples,
+                self._key
+        )
+
+    @property
+    def key(self) -> Hashable:
+        return self._key
+
+    def measurement_basis_change(self) -> op_tree.OP_TREE:
+        return self._pauli_string.to_z_basis_ops()
+
+    @property
+    def num_samples(self) -> int:
+        return self._num_samples
+
+    def value_derived_from_samples(self,
+                                   measurements: np.ndarray) -> float:
+        return np.mean([(-1)**sum(bitstring) for bitstring in measurements])
+
+    def _value_equality_values_(self):
+        return self._pauli_string, self._num_samples, self._key
+
+
+@value.value_equality
+class PauliStringExpectation(WaveFunctionDisplay):
+    """Expectation value of a Pauli string."""
+
+    def __init__(self,
+                 pauli_string: 'pauli_string.PauliString',
+                 key: Hashable=''):
+        self._pauli_string = pauli_string
+        self._key = key
+
+    @property
+    def qubits(self) -> Tuple[raw_types.QubitId, ...]:
+        return self._pauli_string.qubits
+
+    def with_qubits(self,
+                    *new_qubits: raw_types.QubitId
+                    ) -> 'PauliStringExpectation':
+        return PauliStringExpectation(
+                self._pauli_string.with_qubits(*new_qubits),
+                self._key
+        )
+
+    @property
+    def key(self) -> Hashable:
+        return self._key
+
+    def value_derived_from_wavefunction(self,
+                                        state: np.ndarray,
+                                        qubit_map: Dict[raw_types.QubitId, int]
+                                        ) -> float:
+        num_qubits = state.shape[0].bit_length() - 1
+        ket = np.reshape(np.copy(state), (2,) * num_qubits)
+        for qubit, pauli in self._pauli_string.items():
+            buffer = np.empty(ket.shape, dtype=state.dtype)
+            args = protocols.ApplyUnitaryArgs(
+                    target_tensor=ket,
+                    available_buffer=buffer,
+                    axes=(qubit_map[qubit],)
+                    )
+            ket = protocols.apply_unitary(pauli, args)
+        ket = np.reshape(ket, state.shape)
+        return np.dot(state.conj(), ket)
+
+    def _value_equality_values_(self):
+        return self._pauli_string, self._key
+
+
+def pauli_string_expectation(
+        pauli_string: 'pauli_string.PauliString',
+        num_samples: Optional[int] = None,
+        key: Hashable='') -> Union[ApproxPauliStringExpectation,
+                                   PauliStringExpectation]:
+    if num_samples is None:
+        return PauliStringExpectation(pauli_string, key=key)
+    return ApproxPauliStringExpectation(pauli_string, num_samples, key=key)


Test patch:
diff --git a/cirq/ops/display_test.py b/cirq/ops/display_test.py
new file mode 100644
--- /dev/null
+++ b/cirq/ops/display_test.py
@@ -0,0 +1,166 @@
+# Copyright 2018 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 numpy as np
+import pytest
+
+import cirq
+
+
+def test_pauli_string_expectation_value():
+    qubits = cirq.LineQubit.range(4)
+    qubit_index_map = {q: i for i, q in enumerate(qubits)}
+
+    circuit = cirq.Circuit.from_ops(
+            cirq.X(qubits[1]),
+            cirq.H(qubits[2]),
+            cirq.X(qubits[3]),
+            cirq.H(qubits[3]),
+    )
+    state = circuit.apply_unitary_effect_to_state(qubit_order=qubits)
+
+    z0z1 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[0]: cirq.Pauli.Z,
+                              qubits[1]: cirq.Pauli.Z})
+            )
+    z0z2 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[0]: cirq.Pauli.Z,
+                              qubits[2]: cirq.Pauli.Z})
+            )
+    z0z3 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[0]: cirq.Pauli.Z,
+                              qubits[3]: cirq.Pauli.Z})
+            )
+    z0x1 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[0]: cirq.Pauli.Z,
+                              qubits[1]: cirq.Pauli.X})
+            )
+    z1x2 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[1]: cirq.Pauli.Z,
+                              qubits[2]: cirq.Pauli.X})
+            )
+    x0z1 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[0]: cirq.Pauli.X,
+                              qubits[1]: cirq.Pauli.Z})
+            )
+    x3 = cirq.PauliStringExpectation(
+            cirq.PauliString({qubits[3]: cirq.Pauli.X})
+            )
+
+    np.testing.assert_allclose(
+        z0z1.value_derived_from_wavefunction(state, qubit_index_map), -1)
+    np.testing.assert_allclose(
+        z0z2.value_derived_from_wavefunction(state, qubit_index_map), 0)
+    np.testing.assert_allclose(
+        z0z3.value_derived_from_wavefunction(state, qubit_index_map), 0)
+    np.testing.assert_allclose(
+        z0x1.value_derived_from_wavefunction(state, qubit_index_map), 0)
+    np.testing.assert_allclose(
+        z1x2.value_derived_from_wavefunction(state, qubit_index_map), -1)
+    np.testing.assert_allclose(
+        x0z1.value_derived_from_wavefunction(state, qubit_index_map), 0)
+    np.testing.assert_allclose(
+        x3.value_derived_from_wavefunction(state, qubit_index_map), -1)
+
+
+@pytest.mark.parametrize('paulis', [
+    (cirq.Pauli.Z, cirq.Pauli.Z),
+    (cirq.Pauli.Z, cirq.Pauli.X),
+    (cirq.Pauli.X, cirq.Pauli.X),
+    (cirq.Pauli.X, cirq.Pauli.Y),
+])
+def test_approx_pauli_string_expectation_measurement_basis_change(paulis):
+    qubits = cirq.LineQubit.range(2)
+    display = cirq.ApproxPauliStringExpectation(
+        cirq.PauliString({qubits[0]: paulis[0],
+                          qubits[1]: paulis[1]}),
+        num_samples=1
+    )
+    matrix = np.kron(cirq.unitary(paulis[0]), cirq.unitary(paulis[1]))
+
+    circuit = cirq.Circuit.from_ops(display.measurement_basis_change())
+    unitary = circuit.to_unitary_matrix(qubit_order=qubits)
+
+    ZZ = np.diag([1, -1, -1, 1])
+    np.testing.assert_allclose(
+        np.dot(unitary, np.dot(matrix, unitary.T.conj())),
+        ZZ
+    )
+
+
+@pytest.mark.parametrize('measurements, value', [
+    (np.array([[0, 0, 0],
+               [0, 0, 0]]),
+     1),
+    (np.array([[0, 0, 0],
+               [0, 0, 1]]),
+     0),
+    (np.array([[0, 1, 0],
+               [1, 0, 0]]),
+     -1),
+    (np.array([[0, 1, 0],
+               [1, 1, 1]]),
+     -1),
+])
+def test_approx_pauli_string_expectation_value(measurements, value):
+    display = cirq.ApproxPauliStringExpectation(
+        cirq.PauliString({}),
+        num_samples=1
+    )
+    assert display.value_derived_from_samples(measurements) == value
+
+
+def test_properties():
+    qubits = cirq.LineQubit.range(9)
+    qubit_pauli_map = {q: cirq.Pauli.XYZ[q.x % 3] for q in qubits}
+    pauli_string = cirq.PauliString(qubit_pauli_map, negated=True)
+
+    pauli_string_expectation = cirq.PauliStringExpectation(
+        pauli_string, key='a')
+    assert pauli_string_expectation.qubits == tuple(qubits)
+    assert pauli_string_expectation.key == 'a'
+
+    approx_pauli_string_expectation = cirq.ApproxPauliStringExpectation(
+        pauli_string, num_samples=5, key='a')
+    assert approx_pauli_string_expectation.qubits == tuple(qubits)
+    assert approx_pauli_string_expectation.num_samples == 5
+    assert approx_pauli_string_expectation.key == 'a'
+
+
+def test_with_qubits():
+    old_qubits = cirq.LineQubit.range(9)
+    new_qubits = cirq.LineQubit.range(9, 18)
+    qubit_pauli_map = {q: cirq.Pauli.XYZ[q.x % 3] for q in old_qubits}
+    pauli_string = cirq.PauliString(qubit_pauli_map, negated=True)
+
+    assert (
+        cirq.PauliStringExpectation(pauli_string).with_qubits(*new_qubits)
+        == cirq.PauliStringExpectation(pauli_string.with_qubits(*new_qubits))
+    )
+    assert (
+        cirq.ApproxPauliStringExpectation(
+            pauli_string, num_samples=1).with_qubits(*new_qubits)
+        == cirq.ApproxPauliStringExpectation(
+            pauli_string.with_qubits(*new_qubits), num_samples=1))
+
+
+def test_pauli_string_expectation_helper():
+    qubits = cirq.LineQubit.range(9)
+    qubit_pauli_map = {q: cirq.Pauli.XYZ[q.x % 3] for q in qubits}
+    pauli_string = cirq.PauliString(qubit_pauli_map, negated=True)
+
+    assert (cirq.pauli_string_expectation(pauli_string, key='a')
+            == cirq.PauliStringExpectation(pauli_string, key='a'))
+    assert (cirq.pauli_string_expectation(pauli_string, 5, key='a')
+            == cirq.ApproxPauliStringExpectation(pauli_string, 5, key='a'))