Quantum States
This tutorial shows how to handle quantum states in Perceval.
First, the necessary import:
[1]:
import perceval as pcvl
# or you can import each symbol, depending on your prefered coding style
from perceval import BasicState, StateVector, SVDistribution, BSDistribution, BSCount, BSSamples
I. BasicState
In linear optics (LO) Circuits, photons can have many discrete degrees of freedom, called modes. It can be the frequency, the polarisation, the position, or all of them.
We represent these degrees of freedom with Fock states. If we have \(n\) photons over \(m\) modes, the Fock state \(|s_1,s_2,...,s_m\rangle\) means we have \(s_i\) photons in the \(i^{th}\) mode. Note that \(\sum_{i=1}^m s_i =n\).
Modes are using 0-based numbering - so mode 0 is corresponding to the first one, and mode \((m-1)\) is corresponding to the \(m\)-th.
In Perceval, we will use the class pcvl.BasicState
.
[2]:
## Syntax of different BasicState (list, string, etc)
bs1 = BasicState([0, 2, 0, 1])
bs2 = BasicState('|0,2,0,1>') # Must start with | and end with >
print(bs1)
print(f"Number of modes: {bs1.m}")
print(f"Number of photons: {bs1.n}")
if bs1 == bs2:
print("bs1 and bs2 are the same states")
## You can iterate on modes
for i, photon_count in enumerate(bs1):
print(f"There is {photon_count} photon in mode {i} (or equivalently bs1[{i}]={bs1[i]}).")
|0,2,0,1>
Number of modes: 4
Number of photons: 3
bs1 and bs2 are the same states
There is 0 photon in mode 0 (or equivalently bs1[0]=0).
There is 2 photon in mode 1 (or equivalently bs1[1]=2).
There is 0 photon in mode 2 (or equivalently bs1[2]=0).
There is 1 photon in mode 3 (or equivalently bs1[3]=1).
A BasicState
is actually more than just a Fock state representation.
[3]:
# There are three kind of BasicStates
photon_position = [0, 2, 0, 1]
# FockState, all photons are indistinguishable
bs1 = BasicState(photon_position) # Or equivalently BasicState("|0,2,0,1>")
print(type(bs1), isinstance(bs1, BasicState))
# NoisyFockState, photons with the same tag are indistinguishable, photons with different tags are distinguishable (they will not interact)
noise_index = [0, 1, 0]
bs2 = BasicState(photon_position, noise_index) # Or equivalently BasicState("|0,{0}{1},0,{0}>")
print(type(bs2), isinstance(bs2, BasicState))
# AnnotatedFockState, with custom annotations (not simulable in the general case, needs a conversion to something simulable first)
bs3 = BasicState("|0,{lambda:925}{lambda:925.01},0,{lambda:925.02}>")
print(type(bs3), isinstance(bs3, BasicState))
<class 'exqalibur.FockState'> True
<class 'exqalibur.NoisyFockState'> True
<class 'exqalibur.AnnotatedFockState'> True
[4]:
# Basic methods are common between these types
print("Reminder, bs1 =", bs1)
print("bs1 * |1,2> =", bs1 * BasicState([1, 2])) # Tensor product
print("A slice of bs1 (extract modes #1 & 2) =", bs1[1:3]) # Slice
print("bs1 with threshold detection applied =", bs1.threshold_detection()) # Apply a threshold detection to the state, limiting detected photons to 1 per mode
Reminder, bs1 = |0,2,0,1>
bs1 * |1,2> = |0,2,0,1,1,2>
A slice of bs1 (extract modes #1 & 2) = |2,0>
bs1 with threshold detection applied = |0,1,0,1>
II. StateVector
A StateVector
is a complex superposition of BasicState
with the same number of modes. It can represent any pure state in the Fock space
[5]:
# StateVectors can be defined using arithmetic on BasicStates and other StateVectors
sv = (0.5 + 0.3j) * BasicState([1, 0, 1, 1]) + BasicState([0, 2, 0, 1])
# State vectors normalize themselves upon use
print("State vector is normalized upon use and display: ", sv)
for (basic_state, amplitude) in sv:
print(basic_state, "has the complex amplitude", amplitude)
# We can also access amplitudes as in a dictionary
print(sv[pcvl.BasicState([0, 2, 0, 1])])
State vector is normalized upon use and display: (0.432+0.259I)*|1,0,1,1>+0.864*|0,2,0,1>
|1,0,1,1> has the complex amplitude (0.4319342127906801+0.25916052767440806j)
|0,2,0,1> has the complex amplitude (0.8638684255813602+0j)
(0.8638684255813602+0j)
III. Distributions
Perceval contains a state vector distribution class that embodies a mixed state. All states defined in this distribution must have the same number of modes.
[6]:
# A SVDistribution is a collection of StateVectors
svd = SVDistribution({StateVector([1, 2]) : 0.4,
BasicState([3, 0]) + BasicState([2, 1]) : 0.6})
print("Five random samples according to the distribution:", svd.sample(5))
svd2 = SVDistribution({StateVector([0]) : 0.1,
BasicState([1]) + BasicState([2]) : 0.2})
svd2.normalize() # distributions have to be normalized to make sense
print(svd2, "sum of probabilities", sum(svd2.values()))
print("Tensor product")
print(svd * svd2) # Tensor product between distributions
Five random samples according to the distribution: [0.707*|3,0>+0.707*|2,1>, 0.707*|3,0>+0.707*|2,1>, 0.707*|3,0>+0.707*|2,1>, 0.707*|3,0>+0.707*|2,1>, |1,2>]
{
|0>: 0.3333333333333333
0.707*|1>+0.707*|2>: 0.6666666666666666
} sum of probabilities 1.0
Tensor product
{
0.707*|1,2,1>+0.707*|1,2,2>: 0.26666666666666666
|1,2,0>: 0.13333333333333333
0.707*|3,0,0>+0.707*|2,1,0>: 0.19999999999999998
0.5*|3,0,1>+0.5*|3,0,2>+0.5*|2,1,1>+0.5*|2,1,2>: 0.39999999999999997
}
IV. Results
When running a Perceval computation, you can expect the result type from the input and the command:
Performing a state evolution returns
A
StateVector
from a pure state inputA
SVDistribution
from a mixed state input
Most other Perceval computations return measurements. Three types exist to match the following commands:
probs
(for “probabilities”) returns aBSDistribution
sample_count
returns aBSCount
samples
returns aBSSamples
These data structures only hold simple FockState
instances without any noise index or annotation, as these data do not survive a measurement.
[7]:
# The BSDistribution is a collection of FockStates
bsd = BSDistribution()
bsd[BasicState([1, 2])] = 0.4
bsd[BasicState([2, 1])] = 0.2
print(bsd)
print("Number of modes:", bsd.m)
bsd.normalize() # Not automatic on distributions
# Distributions act much like python dictionaries
for (state, probability) in bsd.items():
print(state, "has the probability", probability, f"(or equivalently {bsd[state]})")
print("Tensor product")
print(bsd * BasicState([1])) # Tensor product, also works between distributions
bsc = BSCount()
bsc[BasicState([1, 2])] = 20
bsc[BasicState([2, 1])] = 118
print(bsc)
print("Total number of samples:", bsc.total())
{
|1,2>: 0.4
|2,1>: 0.2
}
Number of modes: 2
|1,2> has the probability 0.6666666666666666 (or equivalently 0.6666666666666666)
|2,1> has the probability 0.3333333333333333 (or equivalently 0.3333333333333333)
Tensor product
{
|1,2,1>: 0.6666666666666666
|2,1,1>: 0.3333333333333333
}
{
|1,2>: 20
|2,1>: 118
}
Total number of samples: 138
Functions exist to convert these measurement results from one type to another
[8]:
from perceval import probs_to_sample_count, sample_count_to_probs, sample_count_to_samples
print("bsd converted to a BSCount =", probs_to_sample_count(bsd, max_samples=20))
print("bsc converted to a BSDistribution =", sample_count_to_probs(bsc))
print("bsc converted to a BSSamples =", sample_count_to_samples(bsc)) # Here the sample order is generated by a Python random library
bsd converted to a BSCount = {
|1,2>: 10
|2,1>: 10
}
bsc converted to a BSDistribution = {
|1,2>: 0.14492753623188406
|2,1>: 0.855072463768116
}
bsc converted to a BSSamples = [ |2,1>, |2,1>, |2,1>, |2,1>, |2,1>, |2,1>, |2,1>, |1,2>, |2,1>, |1,2>, |2,1>, ... (size=138) ]
Now that you know how to define states in perceval, you are ready to go to the next part of the tutorial about LO circuits.