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
, andZ
. 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 aPauliString
will call both methods:round()
andsimplify()
[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