Measuring quantum states in QuAIRKit

This tutorial demonstrates how to perform quantum measurement using QuAIRKit.

Table of Contents

[1]:
import time
import traceback

import quairkit as qkit
from quairkit.database import *
from quairkit.loss import *
from quairkit.qinfo import *

qkit.set_dtype("complex128")

Quantum measurement

The idea of measurement comes from one of the four postulates in quantum mechanics.

Postulate 3 [1]

Quantum measurements are described by a collection \(\{M_m\}\) of measurement operators. These are operators acting on the state space of the system being measured. The index \(m\) refers to the measurement outcomes that may occur in the experiment. If the state of the quantum system is \(|\psi\rangle\) immediately before the measurement, then the probability that result \(m\) occurs is given by

\[p(m) = \langle\psi| M_m^\dagger M_m |\psi\rangle \tag{1}\]

and the state of the system after the measurement is

\[\frac{M_m |\psi\rangle}{\sqrt{\langle\psi| M_m^\dagger M_m |\psi\rangle}}\tag{2}\]

where the measurement operators satisfy the completeness equation,

\[\sum_m M_m^\dagger M_m = I.\tag{3}\]

The completeness equation expresses the fact that probabilities sum to one,

\[\sum_m p(m) = \sum_m \langle \psi \vert M_m^\dagger M_m \vert \psi \rangle= 1\tag{4}\]

Such operator set \(\{M_m\}\) is called a Positive Operator-Valued Measure (POVM). When all \(M_m\) are orthogonal projectors (i.e., \(M_m M_{m'} = \delta_{m,m'}M_m\) and \(M = M^\dagger\)), this set is called a Projection-valued Measure (PVM). The quantum measurement described by PVM is called a projective measurement.

Users can perform projective measurements based on the eigenbasis of an observable. For example, one can generate a PVM for the Pauli matrix \(X\) as an observable. According to the spectral decomposition theorem, the Pauli matrix \(X\) has the decomposition: \(X = \sum_m P_m\), where the set \(\{P_m\}\) forms a Projection-valued Measure.

[2]:
pvm = pauli_str_povm("x")
print(f"Projection-valued measure: \n{pvm}")
Projection-valued measure:
tensor([[[ 0.5000+0.j,  0.5000+0.j],
         [ 0.5000+0.j,  0.5000+0.j]],

        [[ 0.5000+0.j, -0.5000+0.j],
         [-0.5000+0.j,  0.5000+0.j]]])

Perform measurement

Projective measurements in QuAIRKit are mainly called by a torch Module Measure. There are several ways to initialize a Measure instance:

  1. Set computational measurement by default, i.e., \(M_m = \{|m\rangle\langle m|\}\)

  2. Set measurement by given Pauli string(s)

  3. Set measurement by given PVM(s) in torch.Tensor

For Measure instances initialized in the first two ways, if the measurement is across all qubits, then the output state(s) will always be recognized as pure state(s).

[3]:
op = Measure()  # computational measure
op = Measure("x")  # x measure on a qubit
op = Measure(pvm)  # measure with a pvm

Measure accepts the State instances and an (optional) measure position as input and returns the measurement result. Note that if the measure only happens on a part of the system, then the argument qubits_idx should be specified. The following example is to measure the first subsystem of state \(\rho\) with the PVM defined before.

[4]:
op = Measure(pvm)
rho = random_state(num_qubits=2, rank=2)
prob = op(rho, qubits_idx=[0])  # measure rho
print("The probability distribution of outcome", prob)
The probability distribution of outcome tensor([0.8449, 0.1551])

Users can get the collapsed state by setting keep_state = True.

[5]:
prob, collapsed_state = op(rho, qubits_idx=[0], keep_state=True)
print("The collapsed state for each outcome is", collapsed_state)
The collapsed state for each outcome is
---------------------------------------------------
 Backend: density_matrix
 System dimension: [2, 2]
 System sequence: [0, 1]
 Batch size: [2]

 # 0:
[[ 0.3+0.j   -0. -0.24j  0.3+0.j   -0. -0.24j]
 [-0. +0.24j  0.2+0.j   -0. +0.24j  0.2+0.j  ]
 [ 0.3+0.j   -0. -0.24j  0.3+0.j   -0. -0.24j]
 [-0. +0.24j  0.2+0.j   -0. +0.24j  0.2+0.j  ]]
 # 1:
[[ 0.15+0.j    0.19+0.09j -0.15+0.j   -0.19-0.09j]
 [ 0.19-0.09j  0.35+0.j   -0.19+0.09j -0.35+0.j  ]
 [-0.15+0.j   -0.19-0.09j  0.15+0.j    0.19+0.09j]
 [-0.19+0.09j -0.35+0.j    0.19-0.09j  0.35+0.j  ]]
---------------------------------------------------

The probability of particular measurement outcome is obtained via setting desired_result=x.

[6]:
x = "1"
prob, collapsed_state = op(
    rho, qubits_idx=[0], keep_state=True, desired_result=x
)  # return the second outcome
print(
    f"The probability for obtaining outcome {x} is {prob}, with outcome state",
    collapsed_state,
)
The probability for obtaining outcome 1 is tensor([0.1551]), with outcome state
---------------------------------------------------
 Backend: density_matrix
 System dimension: [2, 2]
 System sequence: [0, 1]
[[ 0.15+0.j    0.19+0.09j -0.15+0.j   -0.19-0.09j]
 [ 0.19-0.09j  0.35+0.j   -0.19+0.09j -0.35+0.j  ]
 [-0.15+0.j   -0.19-0.09j  0.15+0.j    0.19+0.09j]
 [-0.19+0.09j -0.35+0.j    0.19-0.09j  0.35+0.j  ]]
---------------------------------------------------

Users can also directly call the attribute measure of State instances for simple measurement.

[7]:
rho = random_state(num_qubits=1, rank=2)
prob = rho.measure(pvm)  # same as Measure(pvm)(rho)
print("The probability distribution of outcome", prob)
prob, collapsed_state = rho.measure(
    pvm, keep_state=True
)  # same as Measure(pvm)(rho, keep_state=True)
print("The collapsed state for each outcome is", collapsed_state)
The probability distribution of outcome tensor([0.5878, 0.4122])
The collapsed state for each outcome is
---------------------------------------------------
 Backend: density_matrix
 System dimension: [2]
 System sequence: [0]
 Batch size: [2]

 # 0:
[[0.5+0.j 0.5+0.j]
 [0.5+0.j 0.5+0.j]]
 # 1:
[[ 0.5+0.j -0.5+0.j]
 [-0.5+0.j  0.5+0.j]]
---------------------------------------------------

Positive operator-valued measurement

POVMs are the generalization of PVMs, describing the effect on a subsystem of a projective measurement performed on a larger system. In QuAIRKit, we can perform positive operator-valued measurement by State.measure, with is_povm set to True.

Computation for POVM is often more efficient than that for PVM, as it directly computes the probability. However, its potential lack of a unique post-measurement state makes it less useful in practice.

[8]:
start_time = time.time()
prob = rho.measure(pvm)
print(f"Time for measuring with pvm: {time.time() - start_time:.10f}s")


start_time = time.time()
prob = rho.measure(pvm, is_povm=True)
print(f"Time for measuring with povm: {time.time() - start_time:.10f}s")

try:
    rho.measure(pvm, is_povm=True, keep_state=True)
except ValueError:
    traceback.print_exc()
Time for measuring with pvm: 0.0069618225s
Time for measuring with povm: 0.0017402172s
Traceback (most recent call last):
  File "C:\Users\Cloud\AppData\Local\Temp\ipykernel_24616\2671688636.py", line 11, in <module>
    rho.measure(pvm, is_povm=True, keep_state=True)
  File "c:\Users\Cloud\anaconda3\envs\quair_test\lib\site-packages\quairkit\core\state\backend\__init__.py", line 633, in measure
    raise ValueError(
ValueError: `is_povm` and `keep_state` cannot be both True, since a general POVM does not distinguish states.

Batch measurement

QuAIRKit supports batched measurement under the broadcasting rule, which includes either one or multiple PVMs, one or multiple input states, or both. This means that users can perform different measurements at the same time. The broadcasting rule is summarized as follows:

PVM with size m

State

Probability

[None, …]

[None, …]

[m]

[None, …]

[n, …]

[n, m]

[n, …]

[None, …]

[n, m]

[n, …]

[n, …]

[n, m]

[n, …]

[p, …]

Error

Here the first dimension indicates the batch size of PVMs and input states.

  • The first row refers to 1 PVM set with m elements, and 1 state to be measured, so there is m outcomes to form a probability distribution.

  • The second row means there is 1 PVM set with m elements, and n state to be measured. Therefore, same measurement is implemented for different states.

  • The third row denotes n PVM sets with m elements, and 1 state to be measured. Different measurements will be executed for one state.

  • The fourth row means there are n PVM sets with m elements each and n states, to perform corresponding measurements for every state.

Notice that the number of PVM sets should be consistent with the number of states, as it is shown in the last row.

Users can also initialize a batch Measure instance via a Pauli string, or directly input the batched PVM in torch.Tensor.

[9]:
op = Measure(["x", "y", "z"])  # measure states by x, y, z basis, respectively

list_pvm = pauli_str_povm(["x", "y", "z"])

print(f"The tensor shape of the measurement: {list_pvm.shape}")

op = Measure(list_pvm)  # equivalent to Measure(['x', 'y', 'z'])
The tensor shape of the measurement: torch.Size([3, 2, 2, 2])

As shown by the above table, users are able to apply Measure to a single state,

[10]:
prob, collapsed_state = op(rho, keep_state=True)
print(
    "The measured states for the first batch is",
    collapsed_state[0],
    f"with prob distribution {prob[0]}",
)
The measured states for the first batch is
---------------------------------------------------
 Backend: density_matrix
 System dimension: [2]
 System sequence: [0]
 Batch size: [2]

 # 0:
[[0.5+0.j 0.5+0.j]
 [0.5+0.j 0.5+0.j]]
 # 1:
[[ 0.5+0.j -0.5+0.j]
 [-0.5+0.j  0.5+0.j]]
---------------------------------------------------
 with prob distribution tensor([0.5878, 0.4122])

or apply Measure to a batched state. Note that the batch dimension needs to be matched.

[11]:
batch_size = 3
list_rho = random_state(num_qubits=1, size=batch_size)

prob, collapsed_state = op(list_rho, keep_state=True)
print(
    "The measured states for the first batch is",
    collapsed_state[0],
    f"with prob distribution {prob[0]}",
)
The measured states for the first batch is
---------------------------------------------------
 Backend: density_matrix
 System dimension: [2]
 System sequence: [0]
 Batch size: [2]

 # 0:
[[0.5+0.j 0.5+0.j]
 [0.5+0.j 0.5+0.j]]
 # 1:
[[ 0.5+0.j -0.5+0.j]
 [-0.5+0.j  0.5+0.j]]
---------------------------------------------------
 with prob distribution tensor([0.6556, 0.3444])

Sampled measurements

Users can use quairkit.qinfo.prob_sample to determine shots of measurement based on given probability distributions. The function is used to simulate the outcomes of quantum measurements. When users perform a quantum measurement, the result is probabilistic, namely, outcomes are generated with different probabilities. The prob_sample function allows users to simulate this by generating samples (or “shots”) based on a provided probability distribution.

For example, if users simulate 1024 shots, the output might be

{'00': tensor([98, ...]), '01': tensor([230, ...]), '10': tensor([300, ...]), '11': tensor([396, ...])}

which means that: - 00 occurred 98 times, - 01 occurred 230 times, - 10 occurred 300 times, - 11 occurred 396 times.

Users can also adjust the argument binary and proportional to change the output format:

  • binary is False: the dictionary index is in the decimal system.

  • proportional is True: values are transformed into proportions.

[12]:
batch_size = 3
print(f"{batch_size} probability distributions are\n", prob)

print(f"\nThe outcomes of quantum measurements:\n{prob_sample(prob)}")
print(
    f"\nThe outcomes of quantum measurements with the decimal system of dictionary system:\n",
    prob_sample(prob, binary=False),
)
print(
    f"\nThe outcomes of quantum measurements in proportion:\n",
    prob_sample(prob, proportional=True),
)
3 probability distributions are
 tensor([[0.6556, 0.3444],
        [0.7414, 0.2586],
        [0.8497, 0.1503]])

The outcomes of quantum measurements:
{'0': tensor([657, 753, 860]), '1': tensor([367, 271, 164])}

The outcomes of quantum measurements with the decimal system of dictionary system:
 {'0': tensor([658, 753, 864]), '1': tensor([366, 271, 160])}

The outcomes of quantum measurements in proportion:
 {'0': tensor([0.6494, 0.7432, 0.8350]), '1': tensor([0.3506, 0.2568, 0.1650])}

References

[1] Nielsen, Michael A., and Isaac L. Chuang. Quantum computation and quantum information. Vol. 2. Cambridge: Cambridge university press, 2001.

Table: A reference of notation conventions in this tutorial.

Symbol

Variant

Description

\(p\)

\(p(x),p(m)\)

probability distribution

\(M_m\)

measurement operator

\(M_m^\dagger\)

conjugate transpose of \(M_m\)

\(\{\vert m \rangle \langle m \vert\}\)

computational basis

\(\delta_{m,m'}\)

Kronecker delta

[13]:
qkit.print_info()

---------VERSION---------
quairkit: 0.2.0
torch: 2.4.1+cpu
numpy: 1.26.0
scipy: 1.14.1
matplotlib: 3.9.2
---------SYSTEM---------
Python version: 3.10.15
OS: Windows
OS version: 10.0.26100
---------DEVICE---------
CPU: ARMv8 (64-bit) Family 8 Model 1 Revision 201, Qualcomm Technologies Inc