This page was generated from the notebook examples/notebooks/3_Expectation_value_of_observables.ipynb.

Computing expectation values of observables

In addition of sampling the circuit in a basis or extracting the state-vector in the ideal case, MPQP also allows you to compute expectation values of observables with respect to the state generated by the circuit.

To demonstrate this, let us create a circuit generating a 2-qubit state.

[1]:
from mpqp import QCircuit
from mpqp.gates import *
circuit = QCircuit([H(0), CNOT(0,1), Ry(2.6, 0), Ry(-0.87, 1)])
print(circuit)
     ┌───┐      ┌─────────┐
q_0: ┤ H ├──■───┤ Ry(2.6) ├─
     └───┘┌─┴─┐┌┴─────────┴┐
q_1: ─────┤ X ├┤ Ry(-0.87) ├
          └───┘└───────────┘

We then import the two needed objects: Observable and ExpectationMeasure. The first takes as parameter a hermitian matrix (as a numpy array), and the second is the measure to be added to the circuit.

[2]:
from mpqp.measures import Observable, ExpectationMeasure
[3]:
import numpy as np
from mpqp.tools.maths import is_hermitian

matrix = np.array([[4,  2,  3, 8],
                   [2, -3,  1, 0],
                   [3,  1, -1, 5],
                   [8,  0,  5, 2]])
is_hermitian(matrix)
[3]:
True
[4]:
obs = Observable(matrix)

The ExpectationMeasure takes as parameter the list of qubits corresponding to the observable. The indices of qubits can be given non-ordered, non-contigous and restricted to some qubits, and MPQP will automatically operate on the circuit and the observable to handle that. One has also to precise the number of shots, when sampling the circuit to compute the expectation. If the number of shots is zero, the exact value is returned.

[5]:
circuit.add(ExpectationMeasure([0,1], observable=obs, shots=0))
[6]:
from mpqp.execution import run, ATOSDevice, IBMDevice
result = run(circuit, ATOSDevice.MYQLM_PYLINALG)
print(result)
print(result.expectation_value)
Result: ATOSDevice, MYQLM_PYLINALG
 Expectation value: -3.5935083233096687
 Error/Variance: 0.0
-3.5935083233096687
[7]:
circuit = circuit.without_measurements()
circuit.add(ExpectationMeasure([0,1], observable=obs, shots=2000))
[8]:
results = run(circuit, [ATOSDevice.MYQLM_PYLINALG, IBMDevice.AER_SIMULATOR])
print(results)
BatchResult: 2 results
Result: ATOSDevice, MYQLM_PYLINALG
 Expectation value: -3.6279999999999997
 Error/Variance: 0.10472127540648739
Result: IBMDevice, AER_SIMULATOR
 Expectation value: -3.2729999999999992
 Error/Variance: 22.060085000000004

Pauli String representation

An observable can also be represented by a Pauli string. This section will demonstrate how to create and manipulate PauliString objects.

[9]:
from mpqp.measures import I, X, Y, Z

pauli_string import: pauli atoms are named I, X, Y, and Z. If you have conflicts with the gates of the same name, you can:

  • Rename Import:

from mpqp.measures import X as Pauli_X,  Y as Pauli_Y
ps = Pauli_X + Pauli_Y/2
  • Import the whole module:

from mpqp.measures import pauli_string
ps = pauli_string.X + pauli_string.Y/2

A PauliString is based on the following hierarchy: - an atom is the most elemental building brick of pauli string, it it either I, X, Y or Z; - a monomial is a tensor product of atoms multiplied by a real coefficient, for instance 0.3 * I⊗Z⊗Y. In MPQP, the tensor product is denoted as @, so the previous example would be expressed as 0.3 * I@Z@Y; - a string is a sum of monomials, for instance 0.3 * I@Z@Y + X@X@X.

In practice, you never need to handle these types independently, you can express everything in term of expression based on the fundamental atoms. Let’s see how.

Creating and Manipulating PauliString Objects

Let’s illustrate how to create and manipulate PauliString objects:

[10]:
ps_1 = I@Z - 3 * X@Y
print(f"{ps_1=}")
ps_1=1*I@Z + -3*X@Y

Simplifying and Rounding PauliStrings

When dealing with PauliStrings, simplification and rounding are common operations:

  • Simplify: We combine like terms and eliminate those with zero coefficients;

  • Round: We can round the coefficients to a specified number of decimals (5 by default).

str on a PauliString will call both methods: round() and simplify()

[11]:
ps_2 = I@Z + 2.555555555*Y@I + X@Z - X@Z
print("ps_2 =",repr(ps_2))
print("     =",repr(ps_2.simplify()))
print("    ~=",repr(ps_2.round(1)))
print("    ~=",ps_2)
ps_2 = 1*I@Z + 2.555555555*Y@I + 1*X@Z + -1*X@Z
     = 2.555555555*Y@I + 1*I@Z
    ~= 1*I@Z + 2.6*Y@I + 1*X@Z + -1*X@Z
    ~= 1*I@Z + 2.5556*Y@I

Arithmetic Operations

We can perform various arithmetic operations on PauliString objects, including addition, subtraction, scalar multiplication, scalar division, and matrix multiplication:

[12]:
ps_2 = ps_2.round(1).simplify()
print(f"""Addition:
({ps_1}) + ({ps_2}) = {ps_1 + ps_2}

Subtraction:
({ps_1}) - ({ps_2}) = {ps_1 - ps_2}

Scalar product:
2 * ({ps_1}) = {2 * ps_1}

Scalar division:
({ps_2}) / 3 ~= {ps_2 / 3}

Tensor product:
({ps_1}) @ Z = {ps_1@Z}
({ps_1}) @ ({ps_2}) = {ps_1@ps_2}""")
Addition:
(1*I@Z + -3*X@Y) + (1*I@Z + 2.6*Y@I) = 2*I@Z + -3*X@Y + 2.6*Y@I

Subtraction:
(1*I@Z + -3*X@Y) - (1*I@Z + 2.6*Y@I) = -3*X@Y + -2.6*Y@I

Scalar product:
2 * (1*I@Z + -3*X@Y) = 2*I@Z + -6*X@Y

Scalar division:
(1*I@Z + 2.6*Y@I) / 3 ~= 0.3333*I@Z + 0.8667*Y@I

Tensor product:
(1*I@Z + -3*X@Y) @ Z = 1*I@Z@Z + -3*X@Y@Z
(1*I@Z + -3*X@Y) @ (1*I@Z + 2.6*Y@I) = 1*I@Z@I@Z + -3*I@Z@X@Y + 2.6*Y@I@I@Z + -7.8*Y@I@X@Y

Create an Observable with a Pauli string

[13]:
obs1 = Observable(ps_1)

Since there is an equivalence between definition from a Pauli string or from a Hermitian matrix, both these observable wan can also be expressed in term of the mean through which it was not defined (Pauli for matrix and vice versa):

[14]:
print("`obs` created with matrix:")
print("matrix:")
print(obs.matrix)
print("Pauli string:")
print(obs.pauli_string)


print("\n\n`obs1` created with Pauli string:")
print("Pauli string:")
print(obs1.pauli_string)
print("matrix:")
print(obs1.matrix)
`obs` created with matrix:
matrix:
[[ 4.+0.j  2.+0.j  3.+0.j  8.+0.j]
 [ 2.+0.j -3.+0.j  1.+0.j  0.+0.j]
 [ 3.+0.j  1.+0.j -1.+0.j  5.+0.j]
 [ 8.+0.j  0.+0.j  5.+0.j  2.+0.j]]
Pauli string:
0.5*I@I + 3.5*I@X + 1*I@Z + 1.5*X@I + 4.5*X@X + 1.5*X@Z + -3.5*Y@Y + -1.5*Z@X + 2.5*Z@Z


`obs1` created with Pauli string:
Pauli string:
1*I@Z + -3*X@Y
matrix:
[[ 1.+0.j  0.+0.j  0.+0.j  0.-3.j]
 [ 0.+0.j -1.+0.j  0.+3.j  0.+0.j]
 [ 0.+0.j  0.-3.j  1.+0.j  0.+0.j]
 [ 0.+3.j  0.+0.j  0.+0.j -1.+0.j]]

ExpectationMeasure

Next, we measure the expectation value of the observable by adding it to the circuit.

[15]:
circuit = circuit.without_measurements()
circuit.add(ExpectationMeasure([0, 1], observable=obs1, shots=1000))
[16]:
results = run(
    circuit,
    [
        ATOSDevice.MYQLM_PYLINALG,
        IBMDevice.AER_SIMULATOR,
        ATOSDevice.MYQLM_CLINALG,
    ],
)
print(results)
BatchResult: 3 results
Result: ATOSDevice, MYQLM_CLINALG
 Expectation value: 0.08200000000000007
 Error/Variance: 0.10000294289963965
Result: ATOSDevice, MYQLM_PYLINALG
 Expectation value: 0.12600000000000006
 Error/Variance: 0.10000590573151757
Result: IBMDevice, AER_SIMULATOR
 Expectation value: 0.14800000000000002
 Error/Variance: 9.987479999999998