# Expectation value of an arbitrary observable. My own definition and cirq limitations

In my previous question I was struggling with the definition of expectation value of an observable for a circuit. Here is what I have derived after some support (I simplify the definition to 2-qubit system (+3rd ancilla)):

My Definition. Given a 2-qubit circuit $$C$$ and a 2-qubit observable $$Ob$$, the expectation value of $$Ob$$ for $$C$$ is defined as $$EV(C, Ob):=\sqrt{\sum_{ij}|\alpha_{ij0}|^2} - \sqrt{\sum_{ij}|\alpha_{ij1}|^2},$$ where $$\alpha_{ijk}$$ are such that $$(Ob_X\circ(C\otimes I))|000⟩=\sum_{ijk}\alpha_{ijk}|ijk⟩,$$ where $$Ob_X$$ denotes X-Axis control, i.e. $$Ob_X=(I\otimes H)\circ Ob_C\circ(I\otimes H)$$, where $$Ob_C$$ denotes $$Ob$$ controlled by the ancilla qubit.

My question is whether this definition is correct. If so, why does cirq constrain simulate_expectation_values method only for observables being Pauli Strings (tensor product of Pauli Gates)?

After @Craig Gidney answer I thought it would be easy to write (just using cirq and numpy) a code that computes expectation value with regard to arbitrary observable and I derived the following snippet:

import cirq
from copy import deepcopy
from cirq import X, CZ, H, Circuit, Simulator, GridQubit, InsertStrategy
import numpy as np

def example_circuit(q0, q1):
sqrt_x = X ** 0.5
yield sqrt_x(q0), sqrt_x(q1)
yield CZ(q0, q1)
yield sqrt_x(q0), sqrt_x(q1)

def extend_circ(C, Ob):
circ = deepcopy(C)
q2 = GridQubit(2, 0)
ObC = Ob.controlled_by(q2)
circ.append([H(q2), ObC, H(q2)], strategy=InsertStrategy.NEW_THEN_INLINE)
return circ

def main():
q0 = GridQubit(0, 0)
q1 = GridQubit(1, 0)

C = Circuit(example_circuit(q0, q1))
Ob = cirq.X(q0) * cirq.X(q1)
# Ob = cirq.X(q0)*cirq.X(q1) + 0.1 * cirq.Z(q0) * cirq.Z(q1)

ext_circ = extend_circ(C, Ob)
print(ext_circ)
result = Simulator().simulate(ext_circ)
print(result)
p0 = np.sqrt(np.sum(np.abs(result.final_state_vector[::2])**2))
p1 = np.sqrt(np.sum(np.abs(result.final_state_vector[1::2])**2))
expectation_value = p0 - p1
print('Expectation Value: {}'.format(expectation_value))

if __name__ == '__main__':
main()


Which gives the following output obtained from prints:

(0, 0): ───X^0.5───@───X^0.5───────PauliString(+X)───────
│               │
(1, 0): ───X^0.5───@───X^0.5───────X─────────────────────
│
(2, 0): ───────────────────────H───@─────────────────H───
measurements: (no measurements)
output vector: 0.5|000⟩ + 0.5j|010⟩ + 0.5j|100⟩ + 0.5|110⟩
Expectation Value: 0.9999999403953552


However, if I uncomment # Ob = cirq.X(q0)*cirq.X(q1) + 0.1 * cirq.Z(q0) * cirq.Z(q1) I obtain the error AttributeError: 'PauliSum' object has no attribute 'controlled_by' which indicates that in order to apply my method an observable must be "controlable".

My naive question. Maybe it would be easier to make in cirq all observables controlable by definition (i.e. extend controlled_by method to all observables)?

A good contribution to Cirq would be to generalize this method to work on cirq.PauliSum observables like cirq.X(q1)*cirq.X(q2) + 0.1 * cirq.Z(q1) * cirq.Z(q2). Cirq doesn't really have a more general definition of observable than that at the moment IIRC.
• I just naively tried to implement this method on all circuits and observables (see above) and I find out that controlled_by method do not apply to observable like cirq.X(q0)*cirq.X(q1) + 0.1 * cirq.Z(q0) * cirq.Z(q1). Wouldn't be easier to make all observables controllable? – Fallen Apart Apr 9 at 15:31
• @FallenApart The meaning of a control can get a bit hairy when you're dealing with observables that are also unitary matrices such as Paulis. There's an ambiguity in whether or not the measurement result should be 0 or +1 when the control isn't satisfied, and this ambiguity interacts with other conventions in cirq. I'd consider adding controlled_by to PauliSum to be a complex software engineering challenge, because of the need to resolve these ambiguities in a satisfying way. Probably not as bad as trying to define what it means to control a Kraus operator, though. – Craig Gidney Apr 9 at 15:52
• @FallenApart There's also issues of closure. For example, you wouldn't just be defining (X(a) + Y(b)).controlled_by(c) you'd also want to define (X(a).controlled_by(d) + Y(b)).controlled_by(c). And you'd want to make sure that e.g. controlling before converting to a PauliSum was consistent with controlling afterward. I wouldn't be surprised if there's some contradiction in behavior hiding in this system when you try to generalize what it means to control these things. – Craig Gidney Apr 9 at 15:59