?? ??????-???????? ?????? & ??????? ?????????
?? ??????-???????? ?????? & ??????? ?????????
Features
• Includes over 104 codes in OOPs Python, all of which can be used either as a
standalone program or integrated with any other main program without any issues.
• Every parameter in the input, output and execution has been provided while keeping
both beginner and advanced users in mind.
• The output of every program is explained thoroughly with detailed examples.
• Detailed mathematical commenting is done alongside the code which enhances
clarity about the flow and working of the code.
Dr. Pranay Barkataki is currently a Research Analyst in Sony R&D, Bengaluru, where he
is working as a data analyst. He has completed his Ph.D. under the supervision of Dr. M.
S. Ramkarthik in the field of quantum entanglement and quantum many-body physics.
His research interests are in theoretical physics as well as machine learning and AI.
An Object-Oriented
Python Cookbook in Quantum
Information Theory and
Quantum Computing
M. S. Ramkarthik
Pranay Barkataki
First edition published 2023
by CRC Press
6000 Broken Sound Parkway NW, Suite 300, Boca Raton, FL 33487-2742
Reasonable efforts have been made to publish reliable data and information, but the author and publisher cannot
assume responsibility for the validity of all materials or the consequences of their use. The authors and publishers have
attempted to trace the copyright holders of all material reproduced in this publication and apologize to copyright holders
if permission to publish in this form has not been obtained. If any copyright material has not been acknowledged please
write and let us know so we may rectify in any future reprint.
Except as permitted under U.S. Copyright Law, no part of this book may be reprinted, reproduced, transmitted, or utilized
in any form by any electronic, mechanical, or other means, now known or hereafter invented, including photocopying,
microfilming, and recording, or in any information storage or retrieval system, without written permission from the
publishers.
For permission to photocopy or use material electronically from this work, access www.copyright.com or contact the
Copyright Clearance Center, Inc. (CCC), 222 Rosewood Drive, Danvers, MA 01923, 978-750-8400. For works that are
not available on CCC please contact [email protected]
Trademark notice: Product or corporate names may be trademarks or registered trademarks and are used only for
identification and explanation without intent to infringe.
DOI: 10.1201/9781003285489
Publisher’s note: This book has been prepared from camera-ready copy provided by the authors.
To my dear wife, for her motivation, faith, advice, care and
support.
M. S. Ramkarthik
Preface xiii
List of Figures xv
Acknowledgment xvii
vii
viii Contents
Bibliography 241
Writing the preface is a ceremonial event for any author. When I wrote the book, Numerical
Recipes in Quantum Information Theory and Quantum Computing: An Adventure in
FORTRAN 90, published by CRC Press in the year 2021, the committee at CRC Press
felt that a Python version of this also would be an interesting read and they encouraged
me to undertake the project. With this motivation from CRC Press, and also driven by my
own “computational” urges, I proceeded to author a book which incorporates a plethora of
operations and tasks in quantum information, quantum entanglement and spin chain sys-
tems in the latest version of the Python programming language. This book was christened
as An Object-Oriented Python Cookbook for Quantum Information Theory and Quantum
Computing; it is a cookbook because it has a lot of recipes in the aforesaid fields. When I
embarked to write this book and develop efficient Python codes, I thought, Why not in-
corporate the latest data structures in Python like OOPs (Object-Oriented Programming
System)? This paved a way to develop codes which not only look clean but also work
efficiently.
Python is one of the major tools used by everyone, from software professionals and
economists to physicists, chemists, and molecular biologists. The simplicity in syntax and the
variety of in-built functions and libraries overshadow its speed compared to other languages.
With such superior features and state-of-the-art data structures, OOPs Python was our
choice to develop this library. These packages which we have developed can be installed on
any computer using a PIP install command without much difficulty. Once it is installed,
all the libraries will be in their correct locations (paths) and the users can use the libraries
without any problems by following the instructions given in the book. Throughout the
book, the SciPy and NumPy libraries are used for invoking certain in-built functions in
Python. Another feature of the book is that, for each and every code, we have developed a
mathematical way of writing the comments using sophisticated packages so that the user
feels at home in every step of the code. This way of developing the comments along the
code also has an advantage of establishing a one-to-one mapping between the mathematical
nature of the operation and the algorithmic flow in the code as both are quite complementary
aspects in solving any problem. At the end of every chapter, a complete example involving
a mix of codes from that chapter is illustrated which enhances ones grasp on how to use
these codes.
This book contains more than 100 numerical routines in Python and it will be extremely
useful for a practicing physicist, both theoretical and experimental, working in the areas
of quantum information theory, quantum computing, quantum entanglement and also on
condensed matter quantum many-body spin half chains. We also remark here that, there
is a chapter devoted to numerical linear algebra procedures and a final chapter on random
states and matrices too. We can safely and humbly say that the computational libraries
cover a wide spectrum of topics.
In a bird’s eye view we see about the chapters. Chapter 1 is a self contained introduction
to the Python programming language and the nuances of OOPs programming. This chap-
ter is written in such a way that anyone even without prior knowledge of programming can
learn OOPs Python. Each and every syntax and structure of the language is explained with
xiii
xiv Preface
carefully chosen examples for better clarity. Chapter 2 contains the basic tools of quantum
mathematics, the mathematical operations which form the core of the theory. Chapter 3
is about numerical linear algebra and other linear algebra–based quantum mechanical op-
erations. Chapter 4 contains those codes with which you can build quantum gates, states
and a variety of important quantities involving the states such as fidelity, entropies, trace
distance and so on. Chapter 5 deals with quantification of entanglement or entanglement
measures. In this chapter, we have developed high-speed methods of computing partial trace
and partial transpose of any arbitrary set of qubits from a given superset of qubits. Later,
using these operations, we have developed codes for almost all frequently used entanglement
measures for both pure and mixed states. Chapter 5 culminates with some illustrations of
entanglement detection such as Peres PPT (Positive Partial Transpose) criteria and the
Horodecki’s reduction criteria for separability. Chapter 6 deals with the construction of
several one-dimensional spin half-chain Hamiltonians with and without random bond in-
teractions and with and without homogeneous magnetic fields, including Heisenberg and
Ising model and their variants, spin models with asymmetric interactions like the DM in-
teractions. Important to note here is that the codes developed in this chapter will work for
any rth neighbour exchange interaction; this enables us to construct any one-dimensional
Hamiltonian with very much ease. Last but not least, a final chapter on construction of
random states, density matrices and other pseudo random distributions is included for the
sake of completeness and elegance.
This book would not have been possible without the help of Ms. Carolina Antunes of
CRC Press Taylor & Francis, Ms. Elizabeth (Betsy) Byers of CRC Press and Ms. Sumati
Agarwal. All of them were very supportive right from the day the proposal was submitted
till the end. I would like to thank CRC Press, Taylor & Francis for giving us this opportunity
to write this book and make the codes available to all aspiring researchers in this area. Last
but not least, the codes given in this book were verified several times with different physical
systems and to the best of our knowledge gives accurate outputs. In spite of several rounds
of careful editing and testing, there may always be errors for which the authors will feel
grateful if the readers point out the same.
M. S. Ramkarthik
Pranay Barkataki
List of Figures
xv
Acknowledgment
xvii
1
Introduction to Python and Object-Oriented
Programming
DOI: 10.1201/9781003285489-1 1
2 Introduction to Python and Object-Oriented Programming
You can write your Python code just after >>>. Now we will see how to print some
character or a number on screen, i.e. on the terminal which we are working on. For example,
syntax for printing ‘Hello world’ on the terminal is given below,
>>> print("Hello world")
Hello world
If you want to find help at any time, use the help() function. Now to exit back to the
command line, you can use the exit() function. A Python code can also be written in a
Python script file, and this file has a .py file extension. The most common ways to run a
Python script file are as follows, from the terminal or integrated development environment
(IDE) such as Spyder. To run a Python script from the terminal, you have to open the
terminal in the same folder where the Python script which you want to run is located, and
thereafter, write the following syntax in the terminal as shown below:
$ python <filename>.py
For example, if you want to run the Python script myfile.py in the terminal then it can be
done as follows,
$ python myfile.py
Usually, most of the Python libraries, including the quantum information library developed
in this book, are a collection of functions written in .py files. It is now the right time to
introduce the structural aspects of any Python code.
1.1 Variables
Variables are reserved memory locations to store values [11]. The values assigned to these
variables can be from different data types. For example, it can be number, string, list,
tuple, dictionary, etc. Later in this chapter, we will be discussing a few of these data types.
A variable name may include the following, uppercase (A-Z) and lowercase (a-z) letters,
digits (0-9) and underscore (_). However, a variable name can never start with a number,
and we cannot include any of the special characters inside the variable name. Below we have
shown few examples of proper variable names,
>>> joules = 2 # integer data type
>>> energy_transform = "Convert joules to electron volt" # string data
type
>>> Energy_ev = joules*(6.242e+18) # float data type
>>> probabilities3 = [0.3, 0.6, 0.1] # List data type
Any statement written after # is considered as a comment by the Python interpreter, and
this statement is never executed by the interpreter. Comments are written to add more
clarity for the person who is trying to decode the program and for a better understanding
of the code. Now we are ready to delve into the world of the most commonly used data
types in Python.
Data types 3
The output of the preceding code will print the int class as shown below,
<class 'int'>
1.2.2 Real
The float class represents the real numbers. The real numbers are written in floating point
representation. Optionally, the character e or E followed by a positive or negative integer
may be appended to specify scientific notation.
>>> b=30.5
>>> print(type(b))
>>> h=6.626E-34 # Planck constant
>>> print("Multiplying Planck constant by 10 =",h*10)
The output of the preceding code will print the float class as shown below.
<class 'float'>
Multiplying Planck constant by 10 = 6.626e-33
1.2.3 Complex
Any complex number has two parts, real and imaginary, and it is represented by complex
class. For example, the complex number 3 + 2i in Python can be written as,
>>> c = complex(3, 2)
>>> print(type(c))
The output of the preceding code will print the complex class as shown below,
<class 'complex'>
1.2.4 String
The str class represents the string type. In the Python language, a string is an array of
bytes representing Unicode characters. Inside a enclosed single, double or triple quote, we
can write any number of characters. Few examples of valid string type variables are shown
below,
>>> str1 = 'Spooky action at a distance' # Valid string type
>>> str2 = "Entangled 2 qubit states." # Valid string type
4 Introduction to Python and Object-Oriented Programming
However, when a string contains a special character like a quote, then we need to escape
them, otherwise it will lead to syntax error. Escaping is done by placing backslash (\) before
the character as shown below,
>>> str3="She said, "Whats up" " # Not a valid string type
SyntaxError: invalid syntax
>>> str3 ="She said, \"Whats up\" "" # Valid string type
>>> print(str3)
She said, "Whats up"
>>> str4= 'Space like \t separated' # \t introduces a tab space
>>> print(str4)
Space like separated
>>> str5= "Hello \n world" # \n introduces a new line
>>> print(str5)
Hello
world
One may wonder why in Python the concept of writing a string inside a triple quote is
followed. The answer lies in the fact that a triple quote can enclose strings of characters
that contain both single and double quotes such that no escaping is needed, and it also
allows multi-line strings. Few examples of triple quote strings are shown below,
>>> print('''Einstein said, "Quantum mechanics is not correct."''')
Einstein said, "Quantum mechanics is not correct."
>>> print("""Hello
... World""")
Hello
World
1.2.5 Logical
There are only two logical values True and False corresponding to the standard Boolean
elements 1 and 0 respectively. The bool class represents the Boolean type. An example is
indicated as below,
>>> print(type(True))
<class 'bool'>
1.2.6 List
Lists are an ordered collection of various types of data, and it has no fixed size. Lists need
not be homogeneous always, which makes it the most powerful feature of Python. Unlike
strings, lists are mutable by a variety of list method calls, and in this section, we will discuss
a few of the important ones. A list is created by writing the comma-separated values (items)
inside a square bracket, for example,
>>> lst1 = [1, 'electron', 2, 'proton', 3, 'neutron']
>>> print(type(lst1))
<class 'list'>
Data types 5
In a list, the indexing of the first element starts from zero. If you want to access a particular
element or elements in a list, you can do the following:
>>> print(lst1[0]) # Indexing by position
1
>>> print(lst1[1:3]) # Slicing a list
['electron', 2]
>>> length = len(lst1) # Calculates the length of the list
>>> print(length)
6
To add, remove or insert elements at a particular position in a list can be done by the
following list method calls:
>>> lst1.append(6) # append element 6 into the list
>>> print(lst1)
[1, 'electron', 2, 'proton', 3, 'neutron', 6]
>>> lst1.remove(3) # remove the element 3
>>> print(lst1)
[1, 'electron', 2, 'proton', 'neutron', 6]
>>> lst1.insert(1,'atom') #.insert(i,x), insert at i'th index the element x
>>> print(lst1)
[1, 'atom', 'electron', 2, 'proton', 'neutron', 6]
>>> lst1.pop(5) # .pop(i), it removes the element in the i'th index
'neutron'
>>> print(lst1)
[1, 'atom', 'electron', 2, 'proton', 6]
1.2.7 Tuple
A tuple is a collection of objects which is ordered and immutable. Tuples are sequences, just
like lists. The difference between tuples and lists are as follows, tuples cannot be changed
like lists, and tuples use parentheses whereas lists use square brackets.
>>> tup1 = ('quantum', 'state', 0.75, 0.25) # valid tuple
>>> print(type(tup1))
<class 'tuple'>
>>> tup2 = (1, 2, 3 ) # valid tuple
>>> print(tup2[0])
1
>>> print(tup2[1])
2
or C++ for faster computation purposes. NumPy also understands array data format from
languages like C and FORTRAN, which is an added advantage. In order to create a NumPy
array, we first have to import the library and then call the array() method on the sequence
of numbers as written below,
>>> import numpy as np
>>> array1 = np.array([1, 2, 3]) # 1D array
>>> print(array1.shape) # .shape, it returns the shape of the array
(3,)
>>> array2 = np.array([[1,0],[0,-1]]) # 2D array
>>> print(array2.shape)
(2, 2)
>>> array3 = np.array([[[1,3,2.5],[4,8,-11]],[[3,22,-0.9],[2,3,4]]]) # 3D
array
>>> print(array3.shape)
(2, 2, 3)
There are other ways to create NumPy arrays, and we will discuss a few of them in this
section. Primarily we will be discussing about the functions empty(), zeros(), identity()
and ones(). The empty() function takes in an integer or tuple of integer defining the shape
of an array and then allocates the memory without assigning any values to the array. It
means that random noise close to zero is initialized in the array. The ones() and zeros()
functions take in an integer or tuple of integers defining the shape of an array and return an
n-dimensional array whose elements are one or zero, respectively. The identity() function
takes in an integer value defining the dimension of a square matrix, whose diagonal elements
are equal to one and off-diagonal elements are equal to zero. Few examples of the above
functions are given below,
>>> array3 = np.empty((2,3))
>>> print(type(array3))
<class 'numpy.ndarray'>
>>> array4 = np.zeros((2,3))
>>> print(array4)
[[0. 0. 0.]
[0. 0. 0.]]
>>> array5 = np.ones((2,3))
>>> print(array5)
[[1. 1. 1.]
[1. 1. 1.]]
>>> array6 = np.identity(3)
>>> print(array6)
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
The default data types of outputs of functions empty(), zeros() and ones() is float64.
The default data type output of identity() is float. If you want to change the data type
from float or float64 to int of the above outputs, you can do the following:
Data types 7
>>> array7 = np.zeros((2,3), dtype = 'int_') # dtype stands for data type
>>> print(array7)
[[0 0 0]
[0 0 0]]
>>> array8 = np.ones((2,3), dtype = 'int_')
>>> print(array8)
[[1 1 1]
[1 1 1]]
>>> array9 = np.identity(3, dtype = 'int_')
>>> print(array9)
[[1 0 0]
[0 1 0]
[0 0 1]]
There are many varieties of dtypes or data types available in NumPy. The dtypes have all
string character codes and can be used in creating more complicated types. Some dtypes are
flexible (f), and in these dtypes the arrays must have fixed size, but the dtypes length may
be different for different arrays. Usually string data types are flexible because one array
may have strings of length 20 and another array may have strings of length 40.
string_ f Bytes (or str in Python 2) data type. This is a flexible dtype
1.3 Operators
Operators are objects which perform certain mathematical operations to give another out-
put. Here A and B are called operands. There are three types of operators which can be
defined in Python, which are the arithmetic, relational and logical operators as we will see
further.
Operators 9
Operator Description
Operator Description
> It checks whether the left operand is greater than the right operand or
not
< It checks whether the left operand is less than the right operand or not
>= It checks whether the left operand is greater than or equal to the right
operand or not
<= It checks whether the left operand is less than or equal to the right
operand or not
Operator Description
and It is called the logical AND operator. If both the operands are non-zero,
then the condition becomes true.
or It is called the logical OR operator. If any one of the two operands are
non-zero, then the condition becomes true.
not It is called the logical NOT operator. Used to reverse the logical state
of its operand. If a condition is true then the logical NOT operator will
make false and vice versa.
10 Introduction to Python and Object-Oriented Programming
1.4 Decisions
Decisions are very important elements in any programming language. Conditional state-
ments helps us in making those decisions. Basic syntax for conditional statements are given
below.
If the condition <test_expression> holds true then the body of the if statement (<body
of if>) gets executed. An example of the above syntax is given below:
# Coefficients of quadratic equation, ax2 + bx + c = 0
a=5
b=6
c=1
# The discriminant is defined as, b2 − 4ac
d=(b**2)-(4*a*c)
if d > 0:
print("Roots are real and distinct")
If the condition <test_expression> holds true then the body of the if statement (<body of
if>) gets executed, otherwise the body of the else statement (<body of else>) is executed.
An example of the above syntax is given below,
# Coefficients of quadratic equation, ax2 + bx + c = 0
a=5
b=2
c=1
# The discriminant is defined as, b2 − 4ac
d=(b**2)-(4*a*c)
if d > 0:
Loops 11
The elif statements comes after if statement and before else statement. An example of the
above syntax is given below:
# Coefficients of quadratic equation, ax2 + bx + c = 0
a=5
b=2
c=1
# The discriminant is defined as, b2 − 4ac
d=(b**2)-(4*a*c)
if d > 0:
print("Roots are real and distinct")
elif d == 0:
print("Roots are real and equal")
else:
print("Roots are complex")
1.5 Loops
Loops are useful for repeated execution of similar operations. The importance of looping is
so much that, without looping elements, any programming language will be 99% incomplete.
There are two types of loops and the syntax of both of them are given below.
12 Introduction to Python and Object-Oriented Programming
The body of the while loop (<Body of while>) gets executed till the condition
<test_expression> holds. An example of the above syntax is as follows,
# Calculating the n! factorial
n = 3
fact=1
while n > 0:
fact=fact*n
n=n-1
print("Factorial =",fact)
As we have already discussed above, the while loop gets executed till the test expression
holds, however, the while loop becomes ineffective when you have to iterate over the items
of the list or have to run the body of the loop for n times. The for loop fits in this role
perfectly as discussed in the next section.
The variable <variable> is assigned the value of the new item present in the iterable
<iterable> for each loop. Few examples of the above syntax are shown below,
mylist=['quantum', 'mechanics', 1, complex(2,3), 3.8]
for i in mylist:
print(i)
Now if you want to run the variable <variable> from integral number n to integral number
m in steps of l (integer number), then you have to use the range() function. For the case
under consideration, the parametric inputs to the function is as follows, range(n, m +
1, l). Default value of the step in the range() function is 1. An example aforesaid case is
shown below,
Python file handling 13
We provide another example below where we show one way to use loops with arrays.
# Arithmetic progression, a1 , a1 + d, a1 + 2d, . . . , a1 + (n − 1)d
import numpy as np
a1=3
d=0.3
n=5
arr=np.zeros(n,dtype='float64')
for i in range(0,n,1):
arr[i]=a1+i*d
print("Printing the array holding the arithmetic progression")
print(arr)
1. Open a file.
2. Read or write (perform the operation).
3. Close the file.
Python has a built-in open() function to open a file. This function returns a file object,
also called a handle. The generic statement of the open() function is shown below,
file_object = open(r"file_name", "access_mode")
The file should exist in the same directory where the Python program file is saved. Other-
wise, before the filename, the full address or path has to be written. The different types of
available access modes available in Python are listed below,
Read Only "r" Open text file for reading. The handle is positioned
at the beginning of the file. If the file does not exist,
then it raises I/O error. This is also the default mode
in which file is opened
Read and Write "r+" Open the file for reading and writing. The handle is
positioned at the beginning of the file. Raises I/O
error if the file does not exist
Write Only "w" Open the file for writing. For existing file, the data is
truncated and over-written. The handle is positioned
at the beginning of the file. Creates the file, if the file
does not exists
Write and Read "w+" Open the file for reading and writing. For existing
file, data is truncated and over-written. The handle
is positioned at the beginning of the file
Append Only "a" Open the file for writing. The file is created if it does
not exist. The handle is positioned at the end of the
file. The data being written will be inserted at the
end, after the existing data meaning it will be ap-
pended
Append and Read "a+" Open the file for reading and writing. The file is cre-
ated if it does not exist. The handle is positioned at
the end of the file. The data being written will be
inserted at the end, after the existing data
# Closing file
file2.close()
The best way to write and load numpy arrays is shown below.
import numpy as np
arr=np.array([[1,2],[3,4],[5,6]])
This module does not support complex datatypes. The cmath module is the complex
counterpart. In the list below all the functions and attributes defined in math module and
its description are provided
Operator Description
Operator Description
Operator Description
Note that a function can be non-parameterized, which means that the function takes no
input argument. Few examples of functions are shown below,
Object-Oriented Programming (OOPs) 19
n = 4
# calling the function 'heading'
heading()
• Method
Each elemental block of OOP is explained in detail and compared to real-world examples.
The OOP creates an object, and it has two properties as follows, attributes and behaviour.
The preceding statement can be understood with the help of an example as follows, Ram,
20 Introduction to Python and Object-Oriented Programming
Matthew, Eric, etc. can be objects, and the attributes of these objects are weight, height,
etc., and the behaviour of these objects are as follows, eating, talking, walking, etc. A class
is a blueprint for an object, for example, Ram, Matthew and Eric are objects of the class
Human. The class defines the common attributes and behaviour of the objects. Methods
are the functions defining the behaviour of the objects in a class. Let’s try to understand
the above discussion by creating the class Human.
# object behavior
def walking(self, hours_walking):
print(self.name, "walks for", hours_walking, "km")
# object behavior
def fitness(self):
"""
This method calculates whether you are healthy or not.
Returns:
fit_level: string type telling whether you are
under-weight, fit or over-weight.
"""
self.BMI = self.weight/(self.height**2)
if self.BMI < 18.5:
fit_level = 'under-weight'
elif self.BMI >= 18.5 and self.BMI <= 24.9:
fit_level = 'fit'
else:
fit_level = 'over-weight'
return fit_level
The __init__ is a special method, which runs automatically whenever a new object
is created. The string written just under the method fitness is called a docstring, and it
describes the functionality of the method. Using the attribute __doc__ for any object,
we can access the docstring of the method fitness.
After creating the object, you can access the methods in the class as follows,
person1_obj.walking(30)
person2_obj.walking(20)
To access the docstring of the method fitness you write the following piece of code,
# Accessing the docstring
print(person1_obj.fitness.__doc__)
A good docstring not only describes the functionality of the method but also describes what
type of inputs are expected, and what output the method returns. All the methods written
in Chapters 2 to 7 have docstrings for better understanding.
print(person1_obj.fitness())
print(person2_obj.fitness())
import math
class Sphere:
def __init__(self, radius, center_x=0.0, center_y=0.0, center_z=0.0):
"""
Attributes:
radius: stores the radius of the sphere
center_x: x-coordinate of center of sphere, by default = 0.0
center_y: y-coordinate of center of sphere, by default = 0.0
center_z: z-coordinate of center of sphere, by default = 0.0
"""
self.radius=radius
self.center_x=center_x
self.center_y=center_y
self.center_z=center_z
def volume(self):
"""
This method calculates the volume of the sphere
"""
return (4/3)*math.pi*(self.radius**3)
def location(self,x,y,z):
"""
This method calculates whether a given point in 3D space
lies within the sphere or outside.
Attributes:
x: the x-coordinate of the point in a 3D space
y: the y-coordinate of the point in a 3D space
z: the z-coordinate of the point in a 3D space
Return: Tells the point lies inside or outside the sphere
"""
dist = sqrt(((x-self.center_x)**2)+((y-self.center_y)**2)\
+((z-self.center_z)**2))
if dist < self.radius:
return "lies inside the sphere"
elif dist == self.radius:
return "lies on the sphere"
else:
return "lies outside the sphere"
stat= sphere_obj.location(-0.2,-0.1,-0.3)
print("Point (-0.2,-0.1,-0.3)",stat)
There are primarily four concepts in OOPs, and we briefly introduce these concepts below.
1. Inheritance: One class (child class) inherits the attributes and method of another
class (parent class).
2. Encapsulation: It deals with security, which means that it hides the data access
to outsiders.
3. Polymorphism: Poly means many and morph means form. It refers to functions
or methods having the same name but different functionality.
4. Data abstraction: It hides the internal details of a method or function.
From the above discussion, it might seem that OOPs is a powerful style of coding, however,
it has its pros and cons. The advantage of using the OOPs style of coding is given below.
• Improved software-development productivity: Due to the modularity, extensibility
and reusability of OOPs, it is best suited for software-development productivity over
traditional procedure-based programming techniques.
• Faster development: Due to reusability and a large number of libraries of OOPs which
are already available for wider usage makes it easier to develop codes.
• Improved software maintainability: It is easier to maintain because of the modular
structure of coding. The part of the system which needs to be updated or modified can
be achieved without modifying the entire system.
There are a few disadvantages of using the OOPs style of coding, and there are listed below.
• Large program size: Compared to procedural programming, the OOPs have extra lines
of code.
• Slower programs: Compared to procedural programming codes, the OOPs codes require
more computation time for execution.
24 Introduction to Python and Object-Oriented Programming
The collection of many modules make a library or a package. A collection of modules and
sub-packages can also make a library. In this book, we construct the QuantumInforma-
tion library, and it is a collection of 6 modules, and each module is discussed separately
from Chapters 2 to 7.
1.11 SciPy
SciPy is an open-source scientific computing library. Many functions in the SciPy library are
used in different domains of science and engineering. It contains functions for linear algebra,
Ordinary Differential Equations (ODE), integration, Fourier Transform, etc. Primarily we
have used the SciPy library for linear algebra applications in our QuantumInformation
library, and to this end, we will delve more deeply into the module called LAPACK, which
primarily contains functions for linear algebra applications.
1.11.1 LAPACK
Calculations involving matrix operations are done by the module called LAPACK (Lin-
ear Algebra PACKage) [17, 18]. The library has many functions which are used for solv-
ing systems of simultaneous linear equations, least-squares solutions of linear systems of
equations, matrix eigenvalue problems, singular value problems, etc. The associated matrix
SciPy 25
factorizations like (LU, Cholesky, QR, SVD, Schur, generalized Schur) are also provided.
The LAPACK module can be imported directly from the SciPy library as shown below,
import scipy.linalg.lapack as la
Each function has a characteristic name associated with it. In the following table, we provide
description of most commonly used functions in LAPACK.
Function Description
Function Description
More details regarding the input and output parameters of these functions can be found in
SciPy library’s documentation. For the sake of continuity, few examples on how to use the
above functions are shown below,
# importing the LAPACK library
import scipy.linalg.lapack as la
The computational time depends upon number of system parameters, and therefore the
computational time varies from system to system.
• NumPy. To install NumPy in your local Linux based system, type the following command
in the terminal (command prompt in Windows).
pip install numpy
• SciPy. To install SciPy in your local Linux based system, type the following command in
the terminal
pip install scipy
28 Introduction to Python and Object-Oriented Programming
The built-in Python packages which we have used in the QuantumInformation library
are listed below,
• math module.
• cmath module.
• re (Regular Expression) module.
There are primarily two options to install QuantumInformation library on your local
computer. The first method is through pip install the library, type the following command
in terminal (command prompt in Windows) for installing the library on Linux (Windows).
pip install QuantumInformation
The second method is directly clone the library folder from the following GitHub repository
of the co-author, https://github.com/pranay1990/QuantumInformation.git. In this
method, the folder of the library has to be copied in the same folder as that of the code
which is to be compiled.
In order to compile and run a Python program simply open the terminal in the folder
where the code is located and type the following command in the terminal.
$ python3 <file_name>.py
The user can also compile the code using open source integrated development en-
vironment such as Spyder, Jupyter Notebook, etc. User can also install the com-
plete open source Python packages and libraries by installing Anaconda from
(https://www.anaconda.com/products/individual).
2
Basic Tools of Quantum Mechanics
In the last chapter we have gained an understanding of the Python programming language
and its features like OOPs, in this chapter we will proceed with the development of numerical
methods for some basic operations of quantum mechanics, which will be helpful in developing
other complicated methods (functions). All the methods which will be discussed in this
chapter are written inside the class QuantumMechanics, and this whole class is written
inside the Python module chap2_nutsboltsquantum.py. The methods inside the class
QuantumMechanics also uses some functions from the NumPy and Math libraries. We
have assumed that the reader will have a good grasp of basic quantum mechanics [19–23]
at the level of post graduation in order to understand the terminologies and the functions
in this chapter as well as the entire book without much difficulty. The level of difficulty of
functions will increase as we travel through the chapters. Here we have used the standard
quantum mechanics convention for all the operations, for example, any pure quantum state
is described by a ket vector | i and its corresponding dual by the bra vector which is h |. In
this chapter we have any vector |vi as a n × 1 column matrix, also called “ket v” which can
be written explicitly as,
v1
v2
.
.
.
|vi =
.
(2.1)
vi
.
.
.
vn
In Eq. [2.1] and Eq. [2.2], the entries of the matrix namely the vi ’s can either be real or
complex corresponding to a real vector and a complex vector respectively. Any one-qubit
state |ψi is written as a linear combination of two bits namely |0i and |1i (eigenstates
of the Pauli σz matrix) as |ψi = α|0i + β|1i such that α, β ∈ C and |α|2 + |β|2 = 1.
The corresponding dual of |ψi, which is the state hψ| is given by α∗ h0| + β ∗ h1|. It is well
known that one-qubit states are very fundamental in quantum information [24] as multi-
qubit states can be built using them. Any physically observable or measurable quantity in
DOI: 10.1201/9781003285489-2 29
30 Basic Tools of Quantum Mechanics
Regarding the discussion of the functionality of a method, the convention adopted here is
– [in] means that the attributes that need to be fed into the method by a user, and [out]
means the return of the method to the user. This convention is followed throughout this
book.
n
X
c = hv|wi = vi∗ wi , (2.3)
i=1
here, vi and wi are the ith entries or matrix elements of vectors |vi and |wi respectively.
The inner product is a very important operation in the space H as it is extensively used
to understand properties such as orthogonality and normalization of vectors as these are
ubiquitous in quantum mechanics.
inner_product(v1,v2)
Inner product between two vectors 31
Parameters
[out] inn inn is complex number storing the inner product between
vectors v1 and v2
Implementation
def inner_product(self,vec1,vec2):
"""
Here we compute the inner product
Attributes:
vec1: it is a column vector.
vec2: it is the second column vector.
Returns:
inn: inner product between vec1 and vec2.
"""
inn=complex(0.0,0.0)
# Checks the dimension of |vi and |wi whether they are equal or not
assert len(vec1) == len(vec2),\
"Dimension of two vectors are not equal to each other"
return inn
Example
In this example, we are finding the inner product between the vectors v1 = (1, 2, 3, 4)T and
v2 = (2, 3, 1, −1)T .
import numpy as np
v1=np.array([1.0,2.0,3.0,4.0])
v2=np.array([2.0,3.0,1.0,-1.0])
inn=quantum_obj.inner_product(v1,v2)
print(inn)
Prints, to standard output which is the inner product between the two real vectors.
(7+0j)
In this example, we are finding the inner product between the vectors v1 = (1, 1 − ι̇, 2 + 3ι̇)T
and v2 = (2, 3 − ι̇, 2 − 3ι̇)T .
import numpy as np
32 Basic Tools of Quantum Mechanics
Prints, to standard output which is the inner product between the two complex vectors.
(1-10j)
the norm is a very important number in the Hilbert space, it is used as a measure to define
the length of the abstract vector |vi.
norm_vec(vec)
Parameters
Implementation
def norm_vec(self,vec):
"""
Here we calculate norm of a vector
Attributes:
vec: column vector
Returns:
norm: it contains the norm of the column vector vec
"""
norm=0.0
# Next for loop calculates the ||v|| = hv|vi
p
for i in range(0,len(vec)):
Normalization of a vector 33
norm=norm+abs(np.conjugate(vec[i])*vec[i])
return np.sqrt(norm)
Example
In this example, we are finding the norm of the complex vector v = (1, 2, 3, 4)T
import numpy as np
v=np.array([1.0,2.0,3.0,4.0])
norm=quantum_obj.norm_vec(v)
print(norm)
In this example, we are finding the norm of the vector v = (1, 1 − ι̇, 2 + 3ι̇)T .
import numpy as np
v=np.array([1,complex(1,-1),complex(2,3)])
norm=quantum_obj.norm_vec(v)
print(norm)
def normalization_vec(self,vec)
Parameters
Implementation
def normalization_vec(self,vec):
"""
Here normalize a given vector
Attributes:
vec: unnormalized column vector
Returns:
vec: normalized column vector
"""
norm=0.0
# Next for loop calculates ||v||
for i in range(0,len(vec)):
norm=norm+abs(np.conjugate(vec[i])*vec[i])
# Next calculates |vi/||v||
vec=vec/np.sqrt(norm)
return vec
Example
In this example, we are normalizing the real vector v = (1, 2, 3, 4)T .
vec=np.array([1,2,3,4])
vec=quantum_obj.normalization_vec(vec)
print(vec)
In this example, we are normalizing the complex vector v = (1, 1 − ι̇, 2 + 3ι̇)T .
vec = np.array([1,complex(1,-1),complex(2,3)])
vec=quantum_obj.normalization_vec(vec)
print(vec)
The outer product is an important entity in quantum mechanics which has many uses,
specially used in the representation of projection operators, density matrices and so on.
When |vi = |wi, it represents the projection operator.
outer_product_rvec(vec1,vec2)
Parameters
Implementation
def outer_product_rvec(self,vec1,vec2):
"""
Here we calculate the outer product
Attributes:
vec1: it is a column real vector
vec2: it is another real vector
Returns:
matrix: outer product of vec1 and vec2
"""
matrix = np.zeros([len(vec1),len(vec2)],dtype='float64')
# In the next for loop we calculate |vihw|
for i in range(0,len(vec1)):
for j in range(0,len(vec2)):
matrix[i,j]=vec1[i]*vec2[j]
return matrix
Example
In this example, we are finding the outer product between real vectors v1 = (1, 2)T and
v2 = (2, 3, 1)T .
import numpy as np
vec1=np.array([1,2])
vec2=np.array([2,3,1])
matrix=quantum_obj.outer_product_rvec(vec1,vec2)
print(matrix)
Prints, to standard output which is the outer product of vectors vec1 and vec2.
36 Basic Tools of Quantum Mechanics
[[2. 3. 1.]
[4. 6. 2.]]
outer_product_cvec(vec1,vec2)
Parameters
Implementation
def outer_product_cvec(self,vec1,vec2):
"""
Here we calculate the outer product
Attributes:
vec1: it is a column complex vector
vec2: it is another complex vector
Returns:
matrix: outer product of vec1 and vec2
"""
matrix = np.zeros([len(vec1),len(vec2)],dtype=np.complex_)
# Next for loop we calculate |vihw|
for i in range(0,len(vec1)):
for j in range(0,len(vec2)):
matrix[i,j]=vec1[i]*np.conjugate(vec2[j])
return matrix
Example
In this example, we are finding the outer product between vectors v1 = (1, 1 − ι̇)T and
v2 = (2, 3 − ι̇)T .
import numpy as np
vec1=np.array([1,complex(1,-1)])
vec2=np.array([2,complex(3,-1)])
matrix=quantum_obj.outer_product_cvec(vec1,vec2)
print(matrix)
The above code prints the following standard output of outer product,
Tensor product between two matrices 37
[[2.+0.j 3.+1.j]
[2.-2.j 4.-2.j]]
Note that each of the elements of the matrix A is algebraically multiplied by the total
matrix B. Kronecker products are very useful when we deal with several identical or non
identical physical systems, and to find the quantum states representing those systems. For
example, any general one-qubit system can be represented by the state |φi = α|0i + β|1i,
now, if we have to find the two-qubit state |ξi corresponding to two one-qubit states, then
we can have
|ξi = (α1 |0i + β1 |1i)1 ⊗ (α1 |0i + β1 |1i)2 . (2.9)
The subscripts in the above refer to systems 1 and 2 respectively. Note that Eq.[2.9] can
be extended to N qubit states in a straightforward manner. Not only that, the concept
of tensor product can be extended to operators, which represent physical observables in
quantum mechanics also.
tensor_product_matrix(A,B)
Parameters
Implementation
def tensor_product_matrix(self,A,B):
"""
Here we calculate the tensor product of two matrix A and B
Attributes:
A: it is either a 1D or 2D array
B: it is either a 1D or 2D array
Returns: tensor product of A and B
"""
# Reshaping if input vector is of shape $(n,)$ to $(n,1)$.
if len(A.shape)==1:
A=A.reshape(A.shape[0],1)
if len(B.shape)==1:
B=B.reshape(B.shape[0],1)
return np.kron(A,B)
Example
In this example, we are finding the tensor product between two real matrices as given below:
1 1 2
A = , B = (2.10)
2 3 1
import numpy as np
A=np.array([1,2])
B=np.array([[1,2],[3,1]])
C=quantum_obj.tensor_product_matrix(A,B)
print(C)
The above code prints the output of the tensor product between the matrix A and B.
[[1 2]
[3 1]
[2 4]
[6 2]]
In this example, we are finding the tensor product between two complex matrices as given
below:
1 1 + ι̇
A= , B = (2.11)
1 − ι̇ 3 − 3ι̇
import numpy as np
A=np.array([1,complex(1,-1)])
B=np.array([complex(1,1),complex(3,-3)])
C=quantum_obj.tensor_product_matrix(A,B)
print(C)
The above code prints the output of the tensor product between the matrix A and B.
Commutator between two matrices 39
[[1.+1.j]
[3.-3.j]
[2.+0.j]
[0.-6.j]]
commutation(A,B)
Parameters
Implementation
def commutation(self,A,B):
"""
Here we calculate commutation between matrices A and B
Attributes:
A: it is a matrix
B: it is another matrix
Returns: AB-BA matrix
"""
40 Basic Tools of Quantum Mechanics
Example
In this example, we are finding the commutator between two real matrices as given below:
1 2 1 2
A= , B = (2.13)
2 −1 3 1
import numpy as np
A=np.array([[1,2],[2,-1]])
B=np.array([[1,2],[3,1]])
C=quantum_obj.commutation(A,B)
print(C)
The above code prints the output of the commutator between matrices A and B.
[[ 2 4]
[-6 -2]]
In this example, we are finding the commutator between two complex matrices as given
below,
1 1 − ι̇ 1 + ι̇ 1 + ι̇
A= , B = (2.14)
2 + 3ι̇ 4 3 − 3ι̇ 4 − ι̇
import numpy as np
A=np.array([[1,complex(1,-1)],[complex(2,3),4]])
B=np.array([[complex(1,1),complex(1,1)],[complex(3,-3),complex(4,-1)]])
C=quantum_obj.commutation(A,B)
print(C)
The above code prints the output of the commutator between matrices A and B.
[[ 1.-11.j -2. -8.j]
[-3.-14.j -1.+11.j]]
if {A, B} = 0 ⇒ AB = −BA, then matrix A and B are said to anti-commute with each
other. Anticommutators occur in the areas of second quantization involving Fermionic and
Bosonic operators [28].
Anticommutator between two matrices 41
anti_commutation(A,B)
Parameters
Implementation
def anti_commutation(self,A,B):
"""
Here we calculate anti commutation between matrices A and B
Attributes:
A: input a square matrix
B: input another square matrix
Returns: AB+BA matrix
"""
# The following return statement calculates, {A, B} = AB + BA
return np.matmul(A,B)+np.matmul(B,A)
Example
In this example, we are finding the anticommutator between two real matrices as given
below:
1 2 1 2
A= , B = (2.16)
2 −1 3 1
import numpy as np
A=np.array([[1,2],[2,-1]])
B=np.array([[1,2],[3,1]])
C=quantum_obj.anti_commutation(A,B)
print(C)
The above code prints the output of the anticommutator between matrices A and B.
[[12 4]
[ 4 8]]
In this example, we are finding the anticommutator between two complex matrices as given
below:
1 1 − ι̇ 1 + ι̇ 1 + ι̇
A= , B= (2.17)
2 + 3ι̇ 4 3 − 3ι̇ 4 − ι̇
42 Basic Tools of Quantum Mechanics
import numpy as np
A=np.array([[1,complex(1,-1)],[complex(2,3),4]])
B=np.array([[complex(1,1),complex(1,1)],[complex(3,-3),complex(4,-1)]])
C=quantum_obj.anti_commutation(A,B)
print(C)
The above code prints the output of the anticommutator between matrices A and B.
[[ 1.+1.j 10.+0.j]
[25.+0.j 31.-9.j]]
here the right to left convention is followed. Binary to decimal conversion is a very important
operation in quantum mechanics and quantum information theory because, the states of the
quantum systems (meaning pure states) are represented by a string of zeros and ones. For
example, the state of two electrons can be represented as |00i = | ↑↑i, |01i = | ↑↓i, |10i =
| ↓↑i and |11i = | ↓↓i. When we have to manipulate such states, then the bit representation
of such states and its corresponding decimal equivalent becomes handy. For example, the
decimal equivalent of the above states can be written as |00i = |0i, |01i = |1i, |10i = |2i and
|11i = |3i. Such representations can be easily extended to multi-qubit states, for example,
a state with four electrons can be in one of the states given by |1010i = | ↓↑↓↑i = |10i. The
main advantage of this decimal representation is that, instead of dealing with a string of
numbers which may often be tiring, we deal only with one number.
binary_decimal(vec)
Parameters
Implementation
def binary_decimal(self,vec):
"""
Binary to decimal conversion
Input:
vec: array containing binary matrix elements i.e. 0 or 1
Returns:
dec: decimal equivalent number of binary array vec
"""
dec=0
for i in range(0,vec.shape[0]):
dec=dec+int((2**(vec.shape[0]-1-i))*vec[i])
dec = int(dec)
return dec
Example
In this example, we are converting binary string of numbers 1010 into a decimal number.
A=np.array([1,0,1,0])
B=quantum_obj.binary_decimal(A)
print(B)
The above code prints the output of the decimal equivalent of 1010.
10
decimal_binary(i,N)
44 Basic Tools of Quantum Mechanics
Parameters
Implementation
def decimal_binary(self,i,N):
"""
Decimal to binary conversion
Inputs:
i: decimal number for which binary equivalent to be calculated
N: length of the binary string
Returns:
bnum: binary number equivalent to i, it is column matrix.
"""
bnum=np.zeros([N],dtype=int)
for j in range(0,N):
bnum[bnum.shape[0]-1-j]=i%2
i=int(i/2)
return bnum
Example
In this example we are converting the integer 10 into a binary string number.
dec=10
# 4 string
vec=quantum_obj.decimal_binary(dec,4)
print("The four binary string of 10")
print(vec)
# 6 string
vec2=quantum_obj.decimal_binary(dec,6)
print("The six binary string of 10")
print(vec2)
The above code prints the output of the binary equivalent of 10.
The four binary string of 10
[1 0 1 0]
The six binary string of 10
[0 0 1 0 1 0]
Binary digits shift 45
In Eq. [2.19] and Eq. [2.20], the ai ’s takes value either zero or one in the binary repre-
sentation. For shifting left or right k times we have to operate SL or SR , respectively,
totally k times in succession. This routine is useful for studying quantum states which are
shift symmetric and it can be put to other generic uses depending on the problem under
consideration.
binary_shift(vec,shift=1,shift_direction='right')
Parameters
Implementation
def binary_shift(self,vec,shift=1,shift_direction='right'):
"""
Shifting of string of binary number
Input:
vec: array containing binary matrix elements i.e. 0 or 1
shift: degree of the shift
shift_direction: its value is either left or right
Output:
46 Basic Tools of Quantum Mechanics
Example
In this example, we shift the binary string 0110 to the left by two degrees, and print the
output. Next, we shift the binary string 0110 to the right by one degree, and print the
output.
import numpy as np
A=np.array([0,1,1,0,0])
B=quantum_obj.binary_shift(A,2,'left')
C=quantum_obj.binary_shift(A,1,'right')
print("Left direction shift")
print(B)
print("Right direction shift")
print(C)
The above code prints the following output for both the above cases.
Left direction shift
[1 0 0 0 1]
Right direction shift
[0 0 1 1 0]
Quantum state shift 47
The left and right translation operators are defined as follows, T̂L and T̂R , respectively. The
result on operating T̂L on the quantum state written in Eq. [2.21] is shown below,
T̂L |ψi = |ψL i = c1 |010i + c2 |100i + c3 |001i. (2.23)
Similarly, on operating T̂R on the quantum state written in Eq. [2.21], we obtain the fol-
lowing,
T̂R |ψi = |ψR i = c1 |100i + c2 |001i + c3 |010i. (2.24)
The matrix representation of the states |ψL i and |ψR i are shown below,
0 0
c c
3 2
c1 c3
0 0
|ψL i = , |ψR i = . (2.25)
c2 c1
0 0
0 0
0 0
For translating the quantum state left or right k times, we have to operate T̂L or T̂R ,
respectively, totally k times in succession.
48 Basic Tools of Quantum Mechanics
rstate_shift(vec,shift=1,shift_direction='right')
Parameters
Implementation
def rstate_shift(self,vec,shift=1,shift_direction='right'):
"""
Shifting of a real quantum state
Inputs:
vec: real quantum state.
shift: degree of the state.
shift_direction: It shows the direction of the shift,
by default it is right
Return:
state2: shifted state of vec
"""
# Checks whether entered state is written in 2N vector space
assert vec.shape[0]%2==0,"Not a qubit quantum state"
# N stores the number of qubits
N=int(math.log(vec.shape[0],2))
# basis vector stores binary strings of any computational basis
basis=np.zeros([N],dtype=int)
# state2 will store the shifted quantum state
state2=np.zeros([2**N],dtype='float64')
# Next for loop calculates, T̂L |ψi or T̂R |ψi, as per choice
for i in range(0,vec.shape[0]):
if vec[i] != 0.0:
basis=self.decimal_binary(i,N)
basis=self.binary_shift(basis,shift=shift,\
shift_direction=shift_direction)
Quantum state shift 49
j=int(self.binary_decimal(basis))
state2[j]=vec[i]
return state2
Example
In this example, we left shift the following quantum state by 1 position to left
The above code prints the left shifted quantum state as shown below:
[0. 0.87287156 0.21821789 0. 0.43643578 0. 0. 0.]
cstate_shift(vec,shift=1,shift_direction='right')
Parameters
Implementation
def cstate_shift(self,vec,shift=1,shift_direction='right'):
"""
Shifting of a complex quantum state
Inputs:
50 Basic Tools of Quantum Mechanics
return state2
Example
In this example, we right shift the following quantum state by 1 position to the left
|ψi = (0.141776 + 0.212664ι̇)|001i + (0.283552 + 0.354441ι̇)|010i +
(0.567104 + 0.637993ι̇)|100i.
In order to normalize the state in Eq. [2.27], we will use the standard prescription,
|ψ1 i
|ψe1 i = p . (2.28)
hψ1 |ψ1 i
Thereafter, we form the outer product of the state |ψe1 i in order to create the pure state
density matrix ρ1 as shown below.
As another example, we create a single qubit normalized quantum state shown below.
T
1 1
|ψ2 i = √ √ . (2.30)
2 2
We now take the tensor product of the state |ψ2 ihψ2 | with the density matrix in Eq. [2.29]
to obtain another density matrix ρ as,
quantum_obj=QM()
Having done with the basic recipes involving vector and matrix elements in the previous
chapter, here in this chapter we will give the recipes for some major linear algebra oper-
ations [26, 29, 30]. These operations are very important from the point of view of many
quantum information tasks and also required in the solution of major problems in quan-
tum mechanics and elsewhere. To be more specific, this chapter broadly deals with matrix
decompositions, calculation of different matrix norms, functions of matrices and orthogonal-
ization procedures [25, 31]. However, in this chapter, we have purposely not included a few
numerical methods, which have been included in our earlier book. The reason behind such
a decision is because the NumPy library already included those methods, and therefore it is
redundant to include those methods in our QuantumInformation library. The methods
which are present in the NumPy library and not included in our library are listed below.
Method Description
Linear algebra is a branch of mathematics that deals with vectors, vector spaces, linear
maps, and a system of linear equations. However, we will be studying those topics which
are pertaining to quantum information theory. All the methods which will be discussed in
this chapter are written inside the class LinearAlgebra and this whole class is written
inside the Python module chap3_linearalgebra.py. The methods inside the the class
LinearAlgebra also uses some functions from the NumPy, Math, Cmath, and SciPy li-
braries. In the forthcoming sections, we will use the following matrices for the purpose of
illustration of how recipes work:
For a real symmetric matrix,
1 −2 3
A1 = (3.1)
−2 3 4 .
3 4 5
DOI: 10.1201/9781003285489-3 53
54 Numerical Linear Algebra Operations
To import the class LinearAlgebra from the QuantumInformation library, and there-
after we have to create an object of the class. The coding of the preceding statement can
be done as follows,
# importing the class LinearAlgebra from the QuantumInformation library
from QuantumInformation import LinearAlgebra as LA
The inverse of the matrix A can be obtained from the following mathematical expression,
adj(A)
A−1 = , (3.5)
det(A)
linear_obj.inverse_matrix(A)
Inverse of a matrix 55
Parameters
Implementation
def inverse_matrix(self,mat):
""" Calculates the inverse of a matrix
Attributes:
mat : Inverse of the array or matrix to be calculated.
Return: inverse of matrix mat
"""
# Here we check the determinant of the matrix det(A) 6= 0
assert np.linalg.det(mat) != 0, "Determinant of the matrix is zero"
return np.linalg.inv(mat)
Example
In this example, we will find the inverse of the real matrix given below:
1 2 3
A = 3 −2 −5 (3.6)
.
7 −2 −3
import numpy as np
A=np.array([[1, 2, 3], [3, -2, -5], [7, -2, -3]])
A_inv=linear_obj.inverse_matrix(A)
print(A_inv)
If the matrix is diagonalizable, the above equation reduces to a matrix form given as below,
f (M ) = P f (D)P −1 , (3.11)
where P is a matrix, whose columns are made of the normalized eigenvectors of M and D,
is a diagonal matrix which contains the eigenvalues of M as diagonal elements. If M is a
normal matrix, Eq. [3.10] is reduced to
f (M ) = U f (D)U † , (3.12)
where U is a unitary matrix. As we know almost all the problems of physics and specially
in quantum mechanics and quantum information theory involves only Hermitian matrices,
they are “compulsorily” diagonalizable, because every Hermitian matrix is a normal matrix
which can be diagonalized by a unitary transform. Thus Eq. [3.12] can be used to find
the function of matrices. We require the exponential of a matrix when we study the time
evolution of quantum systems. In finding quantum entropies, we require the logarithm of
a matrix and in many other diverse areas of other functions of matrices like power and
trigonometric functions are used. Note that for matrices which are not diagonalizable, these
techniques can not be used and in such cases the power series with proper truncation is
used based on the accuracy of the results we require.
function_smatrix(mat1,mode,log_base)
Function of a matrix 57
Parameters
[in] mode It defines the type of function of matrix. The allowed types
of functions are shown below;
if mode='exp', it will compute exp(A). It is the default
mode
if mode='sin', it will compute sin(A)
if mode='cos', it will compute cos(A)
if mode='tan', it will compute tan(A)
if mode='log', it will compute log(A)
[in] log_base It stores the base of the log function. The default value is
equal to 2
Implementation
eigenvalues,eigenvectors,info=la.dsyev(mat1)
if mode == 'exp':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing eD
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.exp(eigenvalues[i])
if mode == 'sin':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing sin(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.sin(eigenvalues[i])
if mode == 'cos':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing cos(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.cos(eigenvalues[i])
if mode == 'tan':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing tan(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.tan(eigenvalues[i])
if mode == 'log':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing log(D)
for i in range(0,diagonal.shape[0]):
# Checking the eigenvalues of M to be greater than 0.
assert eigenvalues[i] > 0.0,\
"eigenvalues of the matrix are negative or zero"
diagonal[i,i] = math.log(eigenvalues[i],log_base)
Example
In this example, we are finding the exponential of the matrix A1 .
A=np.array([[1, -2, 3], [-2, 3, 4], [3, 4, 5]])
B=linear_obj.function_smatrix(A,"exp")
print(B)
Now we show how robust is our function function_smatrix in detecting errors, we present
the following case. Consider the following real symmetric matrix
−1 0 0
Φ= 0 (3.13)
2 11 .
0 11 5
The eigenvalues of the matrix in Eq. [3.13] can be simply calculated to be −1, −7.6018 and
14.6018. The matrix Φ has two negative eigenvalues, therefore, we cannot calculate ln(Φ).
If we try to calculate ln(Φ) using function_smatrix then it would prompt error message
as shown below.
matrix = np.array([[-1, 0, 0],[0, 2, 11],[0, 11, 5]])
B=linear_obj.function_smatrix(matrix,"log")
function_hmatrix(mat1, mode,log_base)
Parameters
[in] mode It defines the type of function of matrix. The allowed types
of functions are shown below
if mode='exp', it will compute exp(A). It is the default
mode
if mode='sin', it will compute sin(A)
if mode='cos', it will compute cos(A)
if mode='tan', it will compute tan(A)
if mode='log', it will compute log(A)
[in] log_base It stores the base of the log function. The default value is
equal to 2
Implementation
eigenvalues,eigenvectors,info=la.zheev(mat1)
if mode == 'exp':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing eD
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.exp(eigenvalues[i])
if mode == 'sin':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing sin(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.sin(eigenvalues[i])
if mode == 'cos':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing cos(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.cos(eigenvalues[i])
Function of a matrix 61
if mode == 'tan':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing tan(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = math.tan(eigenvalues[i])
if mode == 'log':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=float)
# Constructing log(D)
for i in range(0,diagonal.shape[0]):
assert eigenvalues[i] > 0.0, "eigenvalues of the matrix are
negative"
diagonal[i,i] = math.log(eigenvalues[i],log_base)
Example
In this example, we are finding tan(A2 ).
A= np.array([[2,complex(1,-3)],[complex(1,3),4]])
B=linear_obj.function_hmatrix(A,"tan")
print(B)
function_gmatrix(mat1, mode="exp",log_base=2)
62 Numerical Linear Algebra Operations
Parameters
[in] mode It defines the type of function of matrix. The allowed types
of functions are shown below
if mode='exp', it will compute exp(A). It is the default
mode.
if mode='sin', it will compute sin(A)
if mode='cos', it will compute cos(A)
if mode='tan', it will compute tan(A)
if mode='log', it will compute log(A)
[in] log_base It stores the base of the log function. The default value is
equal to 2
Implementation
eigenvalues,eigenvectors=np.linalg.eig(mat1)
if mode == 'exp':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=complex)
# Constructing eD
Function of a matrix 63
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = cmath.exp(eigenvalues[i])
if mode == 'sin':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=complex)
# Constructing sin(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = cmath.sin(eigenvalues[i])
if mode == 'cos':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=complex)
# Constructing cos(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = cmath.cos(eigenvalues[i])
if mode == 'tan':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=complex)
# Constructing tan(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = cmath.tan(eigenvalues[i])
if mode == 'log':
diagonal=np.zeros((mat1.shape[0],mat1.shape[1]),dtype=complex)
# Constructing log(D)
for i in range(0,diagonal.shape[0]):
diagonal[i,i] = cmath.log(eigenvalues[i],log_base)
Example
In this example, we are finding the natural log of the matrix A3 i.e. ln(A3 ).
A=np.array([[1, -2, 3], [2, 3, 5],[-4, 4, 5]])
B=linear_obj.function_gmatrix(A,mode='log',log_base=math.exp(1))
print(B)
The above code prints the natural log of the matrix A3 as shown below,
[[ 1.64437167+2.72787460e-17j -0.96712394-2.35900820e-16j
1.01814825-2.38386296e-17j]
[ 1.21428832+6.75553539e-17j 1.25209154-1.82545025e-16j
0.68765921-3.30220382e-17j]
[-1.19275475-3.99432226e-17j 0.45126162+1.58309757e-16j
1.84846891-4.37110136e-17j]]
A=np.array([[2, complex(1,-3)],[complex(2,1),4]])
B=linear_obj.function_gmatrix(A,mode='cos')
print(B)
Ak = P Dk P −1 . (3.14)
power_smatrix(mat1,k,precision=10**(-10))
Parameters
Implementation
def power_smatrix(self,mat1,k,precision=10**(-10)):
"""
It calculates the power of a real symmetric matrix.
Attributes:
mat1 : The matrix or array of which power is to be calculated.
k : value of the power
precision: if the absolute eigenvalues below the precision
value will be considered as zero
Return: k'th Power of symmetric matrix mat1
"""
eigenvalues,eigenvectors,info=la.dsyev(mat1)
flag=0
for i in eigenvalues:
# Checking any eigenvalues is negative
if i < 0.0:
flag=1
# If all eigenvalues are +ve, then D is real type
if flag==0:
diag=np.zeros([eigenvectors.shape[0],eigenvectors.shape[1]],\
dtype='float64')
# If any eigenvalues is -ve, then D is complex type
else:
diag=np.zeros([eigenvectors.shape[0],eigenvectors.shape[1]],\
dtype='complex_')
# Constructing Dk
for i in range(0,eigenvectors.shape[0]):
if abs(eigenvalues[i]) <= precision:
diag[i,i]=0.0
eigenvalues[i]=0.0
if eigenvalues[i] < 0.0:
diag[i,i]=pow(abs(eigenvalues.item(i)),k)*pow(complex(0,1),2*k)
else:
diag[i,i]=pow(eigenvalues.item(i),k)
# Constructing ODk OT , where O is orthogonal matrix
diag=np.matmul(np.matmul(eigenvectors,diag),np.transpose(eigenvectors))
return diag
Example
2/3
In this example, we are finding the matrix A1 .
A=np.array([[1, -2, 3], [-2, 3, 4], [3, 4, 5]])
B=linear_obj.power_smatrix(A,2/3)
print(B)
2/3
The above code prints A1 .
[[ 0.99390456+0.77939749j -1.08157018+0.66289919j 1.42855357-0.61766176j]
[-1.08157018+0.66289919j 1.84533521+0.56381414j 1.75131174-0.52533846j]
[ 1.42855357-0.61766176j 1.75131174-0.52533846j 2.62409023+0.48948842j]]
66 Numerical Linear Algebra Operations
power_hmatrix(mat1,k,precision=10**(-10))
Parameters
Implementation
def power_hmatrix(self,mat1,k,precision=10**(-10)):
"""
It calculates the power of a Hermitian matrix.
Attributes:
mat1 : The matrix or array of which power is to be calculated.
k : value of the power
precision: if the absolute eigenvalues below the precision
value will be considered as zero
Return: k'th Power of Hermitian matrix mat1
"""
eigenvalues,eigenvectors,info=la.zheev(mat1)
flag=0
for i in eigenvalues:
# Checking any eigenvalues is negative
if i < 0.0:
flag=1
# If all eigenvalues are +ve then D is real type
if flag==0:
diag=np.zeros([eigenvectors.shape[0],eigenvectors.shape[1]],\
dtype='float64')
# If any eigenvalue is -ve then D is complex type
else:
diag=np.zeros([eigenvectors.shape[0],eigenvectors.shape[1]],\
dtype='complex_')
# Constructing Dk
for i in range(0,eigenvectors.shape[0]):
if abs(eigenvalues[i]) <= precision:
diag[i,i]=0.0
eigenvalues[i]=0.0
Power of a matrix 67
Example
2/3
In this example, we are finding the matrix A2 .
A=np.array([[2,complex(1,-3)],[complex(1,3),4]])
B=linear_obj.power_hmatrix(A,2/3)
print(B)
2/3
The above code prints A2 .
[[1.04224674+0.26180536j 0.36821119-1.71113837j]
[0.73211407+1.58983741j 2.142572+0.1405044j ]]
power_gmatrix(mat1, k, precision=10**(-10))
Parameters
Implementation
Example
In this example, we are finding the matrix A23 .
A=np.array([[1, -2, 3], [2, 3, 5],[-4, 4, 5]])
B=linear_obj.power_gmatrix(A,2)
print(B)
In the above equation, the square root is uniquely determined by spectral theorem as given
by Eq. [3.12]. Trace norm has applications in the calculation of logarithmic negativity of a
state, computation of fidelity between two density matrices and so on.
trace_norm_rmatrix(mat1, precision=10**(-13))
Parameters
Implementation
return trace_norm
70 Numerical Linear Algebra Operations
Example
In this example, we are finding the trace norm of the matrix A3 .
A=np.array([[1, -2, 3], [2, 3, 5],[-4, 4, 5]])
trace_norm=linear_obj.trace_norm_rmatrix(A)
print(trace_norm)
trace_norm_cmatrix(mat1, precision=10**(-13))
Parameters
Implementation
return trace_norm
Hilbert-Schmidt norm of a matrix 71
Example
In this example, we are finding the trace norm of the matrix A4 .
A=np.array([[2, complex(1,-3)],[complex(2,1),4]])
trace_norm=linear_obj.trace_norm_cmatrix(A)
print(trace_norm)
hilbert_schmidt_norm_rmatrix(mat1, precision=10**(-13))
Parameters
Implementation
# Calculating eigenvalues of AT A
eigenvalues,eigenvectors,info=la.dsyev(np.matmul(np.transpose(mat1),\
mat1))
htrace_norm=0.0
# Calculating T r(AT A)
for i in range(len(eigenvalues)):
if abs(eigenvalues[i]) < precision:
eigenvalues[i]=0.0
htrace_norm=htrace_norm+eigenvalues[i]
# Calculating
p
T r(AT A)
htrace_norm=np.sqrt(htrace_norm)
return htrace_norm
Example
In this example, we are finding the Hilbert-Schmidt norm of the matrix A3 .
A=np.array([[1, -2, 3], [2, 3, 5],[-4, 4, 5]])
htrace_norm=linear_obj.hilbert_schmidt_norm_rmatrix(A)
print(htrace_norm)
hilbert_schmidt_norm_cmatrix(mat1, precision=10**(-13))
Parameters
Implementation
return htrace_norm
Example
In this example, we are finding the Hilbert-Schmidt norm of the matrix A4 .
A=np.array([[2, complex(1,-3)],[complex(2,1),4]])
htrace_norm=linear_obj.hilbert_schmidt_norm_cmatrix(A)
print(htrace_norm)
absolute_value_rmatrix(mat1)
74 Numerical Linear Algebra Operations
Parameters
Implementation
def absolute_value_rmatrix(self,mat1):
"""
Calculates the absolute value of a real matrix
Attributes:
mat1 : The matrix of which absolute form has to calculated.
Return:
res_mat: Absoulte value of matrix mat1
""" √
# Calculating AT A
res_mat=self.power_smatrix(np.matmul(np.transpose(mat1),\
mat1),0.50)
return res_mat
Example
In this example, we are finding the absolute value of the matrix A3 .
A=np.array([[1, -2, 3], [2, 3, 5],[-4, 4, 5]])
B=linear_obj.absolute_value_rmatrix(A)
print(B)
The above code prints the entries of the absolute value of the matrix A3 .
[[ 4.40016286 -1.23216656 -0.34688963]
[-1.23216656 4.6660871 2.38943439]
[-0.34688963 2.38943439 7.29179476]]
absolute_value_cmatrix(mat1)
The Hilbert-Schmidt inner product between two matrices 75
Parameters
Implementation
def absolute_value_cmatrix(self,mat1):
"""
Calculates the absolute value of a complex matrix
Attributes:
mat1 : The matrix of which absolute form has to calculated.
Return:
res_mat: Absoulte value of matrix mat1
""" √
# Calculating A† A
res_mat=self.power_hmatrix(np.matmul(np.conjugate(np.transpose(mat1)),\
mat1),0.50)
return res_mat
Example
In this example, we are finding the absolute value of the matrix A4 .
A=np.array([[2, complex(1,-3)],[complex(2,1),4]])
B=linear_obj.absolute_value_cmatrix(A)
print(B)
The above code prints the entries of the absolute value of the matrix A4 .
[[2.17113985+0.j 1.46392482-1.46392482j]
[1.46392482+1.46392482j 4.65981204+0.j ]]
hilbert_schmidt_inner_product(A,B)
Parameters
Implementation
def hilbert_schmidt_inner_product(self,A,B):
"""
Calculates the Hilbert-Schmidt inner product between matrices.
Attributes:
A: It is a complex or real input matrix.
B: It is a complex or real input matrix.
Return: Hilbert-Schmidt inner product between A and B.
"""
# Calculating A† B
return np.trace(np.matmul(np.conjugate(np.transpose(A)),B))
Example
In this example, we are finding the Hilbert-Schmidt inner product between matrices A1 and
A3 .
A=np.array([[1, -2, 3], [-2, 3, 4], [3, 4, 5]])
B=np.array([[1, -2, 3], [2, 3, 5],[-4, 4, 5]])
hs_innp=linear_obj.hilbert_schmidt_inner_product(A,B)
print(hs_innp)
The above code prints the Hilbert-Schmidt inner product between matrices A1 and A3 .
68
Another example where we are finding the Hilbert-Schmidt inner product between matrices
A2 and A4 .
A=np.array([[2,complex(1,-3)],[complex(1,3),4]])
B=np.array([[2, complex(1,-3)],[complex(2,1),4]])
hs_innp=linear_obj.hilbert_schmidt_inner_product(A,B)
print(hs_innp)
The above prints the Hilbert-Schmidt inner product between matrices A2 and A4 .
(35-5j)
Gram-Schmidt orthogonalization 77
gram_schmidt_ortho_rmatrix(vectors)
Parameters
Implementation
def gram_schmidt_ortho_rmatrix(self,vectors):
"""
Orthornormal set of real vectors are calculated
Attributes:
vectors: A matrix whose columns are non-orthogonal set real vectors
Return:
orthonormal_vec: A matrix whose columns are orthonormal to each other
"""
78 Numerical Linear Algebra Operations
orthonormal_vec=np.zeros((vectors.shape[0],vectors.shape[1]),
dtype='float64')
for col in range(0,vectors.shape[1]):
if col != 0:
for col2 in range(0,col):
tr=0.0
# Here we calculate hvi |wk+1 i
for row2 in range(0,vectors.shape[0]):
tr=tr+(orthonormal_vec[row2,col2]*vectors[row2,col])
Pk
# Here we calculate i=1 hvi |wk+1 i|vi i
orthonormal_vec[:,col]=orthonormal_vec[:,col]+\
(tr*orthonormal_vec[:,col2])
Pk
# Here we calculate |wk+1 i − i=1 hvi |wk+1 i|vi i
orthonormal_vec[:,col]=vectors[:,col]-orthonormal_vec[:,col]
if col == 0:
# Here calculate |v1 i = |w1 i
orthonormal_vec[:,col]=vectors[:,col]
tr=0.0
Pk
|wk+1 i − i=1 hvi |wk+1 i|vi i
# Finally we calculate Pk
|||wk+1 i − i=1 hvi |wk+1 i|vi i||
for row in range(0,vectors.shape[0]):
tr=tr+(orthonormal_vec[row,col]*orthonormal_vec[row,col])
orthonormal_vec[:,col]=orthonormal_vec[:,col]/np.sqrt(tr)
return orthonormal_vec
Example
In this example, we are doing the Gram-Schmidt orthogonalization of the matrix A whose
columns are the linearly independent non-orthogonal vectors.
1 2 3 4
2 33 4 5
A= . (3.21)
3 4 53 6
4 5 6 73
The above code prints the orthogonal matrix whose columns are the orthonormal vectors.
[[ 0.18257419 -0.04712082 -0.10008963 -0.97694849]
[ 0.36514837 0.93063624 -0.00416827 0.02377968]
[ 0.54772256 -0.2120437 0.80879782 0.0297246 ]
[ 0.73029674 -0.29450514 -0.57949183 0.21005383]]
Gram-Schmidt orthogonalization 79
gram_schmidt_ortho_cmatrix(vectors)
Parameters
Implementation
def gram_schmidt_ortho_cmatrix(self,vectors):
"""
Orthornormal set of complex vectors are calculated
Attributes:
vectors: A matrix whose columns are non-orthogonal set
complex vectors
Return:
orthonormal_vec: A matrix whose columns are orthonormal to each other
"""
orthonormal_vec=np.zeros((vectors.shape[0],vectors.shape[1]),\
dtype=np.complex_)
for col in range(0,vectors.shape[1]):
if col != 0:
# Here initialize, |vk+1 i = |wk+1 i
orthonormal_vec[:,col]=vectors[:,col].copy()
Pk
# Next for loop we calculate |vk+1 i = |wk+1 i − i=1 hvi |wk+1 i|vi i
for col2 in range(0,col):
tr=complex(0.0,0.0)
# Next we calculate hvi |wk+1 i
for row2 in range(0,vectors.shape[0]):
tr=tr+(np.conjugate(orthonormal_vec[row2,col2])\
*vectors[row2,col])
orthonormal_vec[:,col]=orthonormal_vec[:,col]-\
(tr*\
orthonormal_vec[:,col2].copy())
if col == 0:
# Here initialize, |v1 i = |w1 i
orthonormal_vec[:,col]=vectors[:,col].copy()
tr=complex(0.0,0.0)
Pk
|wk+1 i − i=1 hvi |wk+1 i|vi i
# Finally we calculate Pk
|||wk+1 i − i=1 hvi |wk+1 i|vi i||
80 Numerical Linear Algebra Operations
return orthonormal_vec
Example
In this example, we are doing the Gram-Schmidt orthogonalization of the matrix A whose
columns are the linearly independent non-orthogonal vectors.
1 + ι̇ 2 + ι̇
A= . (3.22)
2 − ι̇ 4
A=np.array([[complex(1,1),complex(2,1)],[complex(2,-1),4]])
B=linear_obj.gram_schmidt_ortho_cmatrix(A)
print(B)
The above code prints a unitary matrix, whose columns constructed from the matrix in
Eq.[3.22] using Gram-Schmidt orthogonalization process.
[[0.37796447+0.37796447j 0.5500191 -0.64168895j]
[0.75592895-0.37796447j 0.27500955+0.45834925j]]
In this chapter, we have given the recipes to construct the most important quantum
gates [24, 38–41], which are used in quantum computing; however, a suitable combination
of these gates can be used to accomplish a specific quantum computing task in hand. Given
these gates, they have to act on something, those are the pure states. We have presented
how to construct many interesting states which are used in quantum computing. These are
the prototypical states in their respective Hilbert spaces. Towards the end, we have given
certain quantities like fidelity, trace distance, entropy, as these are important in studying
the closeness between two pure states and related to the entanglement content in them.
Last but not least, there are recipes dealing with single qubit quantum measurements in
the spin basis and expectation values of observables which help us to have a direct mapping
between the Hilbert space which resides in the mind of the mathematician to the laboratory
which resides in front of us in reality. The following pure states and density matrices will
be mostly used to demonstrate the recipes in this chapter.
DOI: 10.1201/9781003285489-4 83
84 Quantum Information and Quantum Computation Tools
All the methods which will be discussed in this chapter are written inside
the class GatesTools, and this whole class is written inside the Python module
chap4_quantumtools.py. To import the class GatesTools from the QuantumInfor-
mation library, and creating the object of the class in your Python code can be done as
follows,
# importing the class GatesTools from the QuantumInformation library
from QuantumInformation import GatesTools as GT
they operate on a quantum state to produce a desired output depending on what operation
they do. Gates are classified as one-qubit, two-qubit and multi-qubit depending on the state
on which they act. Just as how in Boolean algebra we have truth tables for the operations,
similarly we have them in the case of quantum gates too. We will be giving the numerical
recipes to construct such gates; however, one can build up complex quantum circuits using
these gates as the basic building blocks.
To flip a N -qubit computational basis, we have to operate N tensor products of the Pauli-X
matrix, and the mathematical operation is shown below,
X ⊗N |a1 a2 . . . aN i = |1 − a1 i ⊗ |1 − a2 i . . . |1 − aN i (4.10)
Circuit symbol
Or,
Truth table
Input Output
|0i |1i
|1i |0i
sx(N=1)
Parameters
[out] sigmax sigmax is a real array of dimension (0:2N −1,0:2N −1), which
is the N -qubit Pauli-X matrix
86 Quantum Information and Quantum Computation Tools
Implementation
def sx(self,N=1):
"""
Construct N-qubit Pauli spin matrix sigma_x
Inputs:
N: number of spins
Output:
sigmax: It stores the Pauli spin matrix sx
"""
sigmax=np.zeros([2**N,2**N])
j=(2**N)-1
for i in range(0,2**N):
sigmax[i,j]=1
j=j-1
return sigmax
Example
In the following example, we construct the X ⊗2 matrix,
B=gt_obj.sx(N=2)
print(B)
The N -qubit operation of the Pauli-Y matrix on a N -qubit computational basis is shown
below,
Circuit symbol
Y
Frequently used quantum gates 87
Truth table
Input Output
|0i ι̇|1i
|1i −ι̇|0i
sy(N=1)
Parameters
Implementation
def sy(self,N=1):
"""
Construct N-qubit Pauli spin matrix sigma_y
Inputs:
N: Number of spins
Outputs:
sigmay: It stores the Pauli spin matrix sy
"""
sigmay2=np.array([[0,complex(0,-1)],[complex(0,1),0]])
if N >1:
for i in range(2,N+1):
if i==2:
sigmay=np.kron(sigmay2, sigmay2)
elif i > 2:
sigmay=np.kron(sigmay, sigmay2)
else:
sigmay=sigmay2
return sigmay
Example
In the following example, we construct the Y ⊗2 matrix.
B=gt_obj.sy(N=2)
print(B)
The N -qubit operation of the Pauli-Z matrix on a N -qubit computational basis is shown
below,
Z ⊗N |a1 a2 . . . aN i = (1 − 2a1 )(1 − 2a2 ) . . . (1 − 2aN )|a1 a2 . . . aN i (4.12)
Circuit symbol
Truth table
Input Output
|0i |0i
|1i −|1i
sz(N=1)
Parameters
Implementation
def sz(self,N=1):
"""
Construct N-qubit Pauli spin matrix sigma_z
Inputs:
N: Number of spins
Outputs:
sigmaz: It stores the Pauli spin matrix sz
"""
sigmaz2=np.array([[1,0],[0,-1]])
if N >1:
for i in range(2,N+1):
if i==2:
sigmaz=np.kron(sigmaz2, sigmaz2)
elif i > 2:
sigmaz=np.kron(sigmaz, sigmaz2)
else:
sigmaz=sigmaz2
return sigmaz
Example
In the following example, we construct the Z ⊗2 matrix.
B=gt_obj.sz(N=2)
print(B)
The N -qubit operation of the Hadamard matrix on a N -qubit computational basis is shown
below,
Circuit symbol
Truth table
Input Output
|0i+|1i
|0i √
2
|0i−|1i
|1i √
2
hadamard_mat(N=1)
Parameters
Implementation
def hadamard_mat(self,N=1):
"""
Construct N-qubit Hadamard matrix
Inputs:
N: Number of spins
Outputs:
hadamard: It stores the Hadamard matrix
"""
hadamard2=np.array([[1/np.sqrt(2),1/np.sqrt(2)],\
[1/np.sqrt(2),-1/np.sqrt(2)]])
if N >1:
for i in range(2,N+1):
if i==2:
hadamard=np.kron(hadamard2, hadamard2)
elif i > 2:
Frequently used quantum gates 91
hadamard=np.kron(hadamard, hadamard2)
else:
hadamard=hadamard2
return hadamard
Example
In the following example, we construct the H ⊗2 matrix,
B=gt_obj.hadamard_mat(N=2)
print(B)
The N -qubit operation of the phase gate on a N -qubit computational basis is shown below,
Circuit symbol
Truth table
Input Output
|0i |0i
|1i ι̇|1i
phase_gate(N=1)
92 Quantum Information and Quantum Computation Tools
Parameters
Implementation
def phase_gate(self,N=1):
"""
Construct N-qubit phase gate matrix
Inputs:
N: Number of spins
Outputs:
phaseg: It stores the phase gate matrix
"""
phaseg2=np.array([[1,0],\
[0,complex(0,1)]])
if N >1:
for i in range(2,N+1):
if i==2:
phaseg=np.kron(phaseg2, phaseg2)
elif i > 2:
phaseg=np.kron(phaseg, phaseg2)
else:
phaseg=phaseg2
return phaseg
Example
In the following example, we construct the S ⊗2 matrix,
B=gt_obj.phase_gate(N=2)
print(B)
The N -qubit operation of the rotation matrix on a N -qubit computational basis is shown
below,
2π ι̇ 2π ι̇ 2π ι̇
Rk⊗N |a1 a2 . . . aN i = e 2k ×a1 e 2k ×a2 · · · e 2k ×aN |a1 a2 . . . aN i (4.15)
Circuit symbol
Rk
Truth table
Input Output
|0i |0i
2π ι̇
|1i e 2k |1i
Parameters
Implementation
def rotation_gate(self,k,N=1):
"""
Construct N-qubit rotation gate matrix
Input:
k: is a positive number
N: number of spins
Returns:
rotg: Rotation gate matrix
"""
assert k > 0, "k is not positive number"
94 Quantum Information and Quantum Computation Tools
z=complex(0,(2*math.pi)/(2**k))
rotg2=np.array([[1,0],[0,cmath.exp(z)]])
if N >1:
for i in range(2,N+1):
if i==2:
rotg=np.kron(rotg2, rotg2)
elif i > 2:
rotg=np.kron(rotg, rotg2)
else:
rotg=rotg2
return rotg
Example
In the following example, we construct the R3⊗2 matrix,
B=gt_obj.rotation_gate(k=3, N=2)
print(B)
Circuit symbol
Frequently used quantum gates 95
Or,
Truth table
Input Output
|00i |00i
|01i |01i
|10i |11i
|11i |10i
cx_gate()
Parameters
Implementation
def cx_gate(self):
"""
It returns controlled NOT gate
"""
return np.array([[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]])
Example
In this example, we are illustrating the fourth operation of the truth table which is CX|11i =
|10i.
A=gt_obj.cx_gate()
state=np.array([0,0,0,1])
res_state=np.matmul(A,state)
print(res_state)
The above code prints the output of the fourth operation of the truth table.
[0 0 1 0]
96 Quantum Information and Quantum Computation Tools
Circuit symbol
Truth table
Input Output
|00i |00i
|01i |01i
|10i |10i
|11i −|11i
cz_gate()
Parameters
Implementation
def cz_gate(self):
"""
It returns controlled Z gate
"""
return np.array([[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,-1]])
Frequently used quantum gates 97
Example
In this example, we are illustrating the fourth operation of the truth table which is CZ|11i =
−|11i.
A=gt_obj.cz_gate()
state=np.array([0,0,0,1])
res_state=np.matmul(A,state)
print(res_state)
The above code prints the output of the fourth operation of the truth table.
[ 0 0 0 -1]
Circuit symbol
Truth table
Input Output
|00i |00i
|01i |10i
|10i |01i
|11i |11i
swap_gate()
Parameters
Implementation
def swap_gate(self):
"""
It returns a swap gate
"""
return np.array([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])
Example
In this example, we are illustrating the third operation of the truth table which is
SWAP|10i = |01i.
A=gt_obj.swap_gate()
state=np.array([0,0,1,0])
res_state=np.matmul(A,state)
print(res_state)
The above code prints the output of the third operation of the truth table.
[0 1 0 0]
Circuit symbol
Frequently used quantum gates 99
Truth table
Input Output
|000i |000i
|001i |001i
|010i |010i
|011i |011i
|100i |100i
|101i |101i
|110i |111i
|111i |110i
toffoli_gate()
Parameters
Implementation
def toffoli_gate(self):
"""
It returns a Toffoli gate
"""
return np.array([[1,0,0,0,0,0,0,0],[0,1,0,0,0,0,0,0],\
[0,0,1,0,0,0,0,0],[0,0,0,1,0,0,0,0],\
[0,0,0,0,1,0,0,0],[0,0,0,0,0,1,0,0],\
[0,0,0,0,0,0,0,1],[0,0,0,0,0,0,1,0]])
Example
In this example, we are illustrating the seventh operation of the truth table which is
CCX|110i = |111i.
A=gt_obj.toffoli_gate()
state=np.array([0,0,0,0,0,0,1,0])
res_state=np.matmul(A,state)
print(res_state)
The above code prints the output of the seventh operation of the truth table.
[0 0 0 0 0 0 0 1]
100 Quantum Information and Quantum Computation Tools
Truth table
Input Output
|000i |000i
|001i |001i
|010i |010i
|011i |011i
|100i |100i
|101i |110i
|110i |101i
|111i |111i
fredkin_gate()
Parameters
Implementation
def fredkin_gate(self):
"""
It returns a Fredkin gate
"""
return np.array([[1,0,0,0,0,0,0,0],[0,1,0,0,0,0,0,0],\
[0,0,1,0,0,0,0,0],[0,0,0,1,0,0,0,0],\
[0,0,0,0,1,0,0,0],[0,0,0,0,0,0,1,0],\
[0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1]])
Example
In this example, we are illustrating the seventh operation of the truth table which is
F |110i = |101i.
A=gt_obj.fredkin_gate()
state=np.array([0,0,0,0,0,0,1,0])
res_state=np.matmul(A,state)
print(res_state)
The above code prints the output of the seventh operation of the truth table.
[0 0 0 0 0 1 0 0]
1
where each of the two-qubit elemental blocks are, [i i + 1] = √ (|01iii+1 − |10iii+1 ), and N
2
(even number) represents the system size. From Eq. [4.20] we observe that the |LN i state
can be constructed by translationally shifting the |RN i state to the right. Based on the
above discussion, in our following methods, we provide the options of constructing N tensor
product of any of the four Bell states and also its translationally shifted state.
Parameters
[in] tot_spins It stores the total number of spins (N ), and it must be even
number. The default value is 2
[out] state It is a real array which stores either the state |φ1 i or |φ2 i.
Implementation
def bell1(self,tot_spins=2,shift=0):
"""
Construct N tensor products of the |bell1> or T|bell1> Bell state
Input:
tot_spins: The total number of spins
shift: for value 0 we get |bell1> and for value 1 T|bell1>.
Output:
state: the result will be |bell1> or T|bell1> state.
The Bell states 103
"""
# Checking the number of qubits N to be even
assert tot_spins%2==0,"the total number of spins is not an even number"
# Checking the entered shift value is correct or not
assert shift==0 or shift==1, "Invalid entry of the shift value"
terms=int(tot_spins/2)
row=np.zeros([terms,1])
mylist=[]
icount=-1
RecurNum.RecurChainRL1(row,tot_spins,icount,mylist,shift)
mylist=np.array(mylist)
state=np.zeros([2**tot_spins])
factor=1/math.sqrt(2)
len_mylist=len(mylist)
# Constructing the state |φ1 i or |φ2 i
for x in range(0,len_mylist):
state[mylist.item(x)]=factor**terms
return(state)
Example
In this example, we are generating the state |φ2 i of Eq. [4.22], for N = 4 total spins.
1
|φ2 i = (|0101i2341 + |0110i2341 + |1001i2341 + |1010i2341 ) (4.23)
2
1
|φ2 i = (|1010i1234 + |0011i1234 + |1100i1234 + |0101i1234 ) (4.24)
2
The matrix representation of the state |φ2 i in Eq. [4.24] can written as,
1/T
|φ2 i = [ 0 0 0 0.5 0 0.5 0 0 0 0 0.5 0 0.5 0 0 0 ] (4.25)
state=gt_obj.bell1(4,shift=1)
print(state)
As stated earlier, we have used the recursive function RecurChainRL1 in our present
method. For understanding purpose of this recursive function, let us consider the example
given in Eq. [4.24]. The state |φ2 i of Eq. [4.24] has non-zero entry at the following places
(10)10 ≡ (1010)2 , (3)10 ≡ (0011)2 , (12)10 ≡ (1100)2 , and (5)10 ≡ (0101)2 . These places are
calculated by the recursive function RecurChainRL1, and output will be stored in a list
as, [10, 3, 12, 5].
and
1
|φ4 i = (|01i23 − |10i23 ) ⊗ (|01i45 − |10i45 ) ⊗ . . . ⊗ (|01iN 1 − |10iN 1 ), (4.27)
2N/2
The following method uses the function RecurChainRL2 from the module RecurNum,
and the functionality of RecurChainRL2 explained at the end of this section.
bell2(tot_spins=2,shift=0)
Parameters
[in] tot_spins It stores the total number of spins (N ), and it must be even
number. The default value is 2
[out] state It is a real array which stores either the state |φ3 i or |φ4 i
Implementation
def bell2(self,tot_spins=2,shift=0):
"""
Construct N tensor products of the |bell2> or T|bell2> Bell state
Input:
tot_spins: The total number of spins
shift: for value 0 we get |bell2> and for value 1 T|bell2>.
Output:
state: the result will be |bell2> or T|bell2> state.
"""
# Checking the number of qubits N to be even
assert tot_spins%2==0,"the total number of spins is not an even number"
# Checking the entered shift value is correct or not
assert shift==0 or shift==1, "Invalid entry of the shift value"
terms=int(tot_spins/2)
row=np.zeros([terms,1])
mylist=[]
icount=-1
RecurNum.RecurChainRL2(row,tot_spins,icount,mylist,shift)
mylist=np.array(mylist)
state=np.zeros([2**tot_spins])
factor=1/math.sqrt(2)
len_mylist=len(mylist)
# Constructing the state |φ3 i or |φ4 i
for x in range(0,len_mylist):
if mylist.item(x)<0:
The Bell states 105
state[-mylist.item(x)]=-factor**terms
elif mylist.item(x)>=0:
state[mylist.item(x)]=factor**terms
return(state)
Example
In this example, we are generating the state |φ4 i of Eq. [4.27], for N = 4 total spins.
1
|φ4 i = (|0101i2341 − |0110i2341 − |1001i2341 + |1010i2341 ) (4.28)
2
1
|φ4 i = (|1010i1234 − |0011i1234 − |1100i1234 + |0101i1234 ) (4.29)
2
The matrix representation of the state |φ4 i in Eq. [4.29] can written as,
1/T
|φ4 i = [ 0 0 0 −0.5 0 0.5 0 0 0 0 0.5 0 −0.5 0 0 0 ] (4.30)
state=gt_obj.bell2(4,shift=1)
print(state)
As stated earlier, we have used the recursive function RecurChainRL2 in our present
method. For understanding purpose of this recursive function, let us consider the example
given in Eq. [4.29]. The state |φ4 i of Eq. [4.29] has non-zero entry at the following places
(10)10 ≡ (1010)2 , (3)10 ≡ (0011)2 , (12)10 ≡ (1100)2 , and (5)10 ≡ (0101)2 . These places are
calculated by the recursive function RecurChainRL2, and these place values will be stored
in list. Additionally a factor of +1 or −1 will be multiplied to these place values depending
on whether the corresponding coefficient sign is positive or negative, respectively. For the
case under consideration, the list which will be generated is [10, −3, −12, 5].
1
|φ5 i = (|00i12 + |11i12 ) ⊗ (|00i34 + |11i34 ) ⊗ . . . ⊗ (|00iN −1N + |11iN −1N ) , (4.31)
2N/2
and
1
|φ6 i = (|00i23 + |11i23 ) ⊗ (|00i45 + |11i45 ) ⊗ . . . ⊗ (|00iN 1 + |11iN 1 ) , (4.32)
2N/2
The following method uses the function RecurChainRL3 from the module RecurNum,
and the functionality of RecurChainRL3 explained at the end of this section.
bell3(tot_spins=2,shift=0)
106 Quantum Information and Quantum Computation Tools
Parameters
[in] tot_spins It stores the total number of spins (N ), and it must be even
number. The default value is 2
[out] state It is a real array which stores either the state |φ5 i or |φ6 i
Implementation
def bell3(self,tot_spins=2,shift=0):
"""
Construct N tensor products of the |bell3> or T|bell3> Bell state
Input:
tot_spins: The total number of spins
shift: for value 0 we get |bell3> and for value 1 T|bell3>.
Output:
state: the result will be |bell3> or T|bell3> state.
"""
# Checking the number of qubits N to be even
assert tot_spins%2==0,"the total number of spins is not an even number"
# Checking the entered shift value is correct or not
assert shift==0 or shift==1, "Invalid entry of the shift value"
terms=int(tot_spins/2)
row=np.zeros([terms])
mylist=[]
icount=-1
RecurNum.RecurChainRL3(row,tot_spins,icount,mylist,shift)
mylist=np.array(mylist)
state=np.zeros([2**tot_spins])
factor=1/math.sqrt(2)
len_mylist=len(mylist)
# Constructing the state |φ5 i or |φ6 i
for x in range(0,len_mylist):
state[mylist.item(x)]=factor**terms
return(state)
Example
In this example, we are generating the state |φ5 i of Eq. [4.31], for N = 4 total spins.
1
|φ5 i = (|0000i1234 + |0011i1234 + |1100i1234 + |1111i1234 ) (4.33)
2
The Bell states 107
The matrix representation of the state |φ5 i in Eq. [4.33] can written as,
1/T
|φ5 i = [ 0.5 0 0 0.5 0 0 0 0 0 0 0 0 0.5 0 0 0.5 ] (4.34)
state=gt_obj.bell3(4,shift=0)
print(state)
As stated earlier, we have used the recursive function RecurChainRL3 in our present
method. For understanding purpose of this recursive function, let us consider the example
given in Eq. [4.33]. The state |φ5 i of Eq. [4.33] has non-zero entry at the following places
(0)10 ≡ (0000)2 , (3)10 ≡ (0011)2 , (12)10 ≡ (1100)2 , and (15)10 ≡ (1111)2 . These places are
calculated by the recursive function RecurChainRL3, and output will be stored in a list
as, [0, 3, 12, 15].
1
|φ7 i = (|00i12 − |11i12 ) ⊗ (|00i34 − |11i34 ) ⊗ . . . ⊗ (|00iN −1N − |11iN −1N ), (4.35)
2N/2
and
1
|φ8 i = (|00i23 − |11i23 ) ⊗ (|00i45 − |11i45 ) ⊗ . . . ⊗ (|00iN 1 − |11iN 1 ), (4.36)
2N/2
The following method uses the function RecurChainRL4 from the module RecurNum,
and the functionality of RecurChainRL4 explained at the end of this section.
bell3(tot_spins=2,shift=0)
Parameters
[in] tot_spins It stores the total number of spins (N ), and it must be even
number. The default value is 2
[out] state It is a real array which stores either the state |φ7 i or |φ8 i
108 Quantum Information and Quantum Computation Tools
Implementation
def bell4(self,tot_spins=2,shift=0):
"""
Construct N tensor products of the |bell4> or T|bell4> Bell state
Input:
tot_spins: The total number of spins
shift: for value 0 we get |bell4> and for value 1 T|bell4>.
Output:
state: the result will be |bell4> or T|bell4> state.
"""
# Checking the number of qubits N to be even
assert tot_spins%2==0,"the total number of spins is not an even number"
# Checking the entered shift value is correct or not
assert shift==0 or shift==1, "Invalid entry of the shift value"
terms=int(tot_spins/2)
row=np.zeros([terms,1])
mylist=[]
icount=-1
RecurNum.RecurChainRL4(row,tot_spins,icount,mylist,shift)
mylist=np.array(mylist)
state=np.zeros([2**tot_spins])
factor=1/math.sqrt(2)
len_mylist=len(mylist)
# Constructing the state |φ7 i and |φ8 i
for x in range(0,len_mylist):
if mylist.item(x)<0:
state[-mylist.item(x)]=-factor**terms
elif mylist.item(x)>=0:
state[mylist.item(x)]=factor**terms
return(state)
Example
In this example, we are generating the state |φ7 i of Eq. [4.35], for N = 4 total spins.
1
|φ7 i = (|0000i1234 − |0011i1234 − |1100i1234 + |1111i1234 ) (4.37)
2
The matrix representation of the state |φ7 i in Eq. [4.37] can written as,
1/T
|φ7 i = [ 0.5 0 0 −0.5 0 0 0 0 0 0 0 0 −0.5 0 0 0.5 ] (4.38)
state=gt_obj.bell4(4,shift=0)
print(state)
As stated earlier, we have used the recursive function RecurChainRL4 in our present
method. For understanding purpose of this recursive function, let us consider the example
The N -qubit Greenberger-Horne-Zeilinger (GHZ) state 109
given in Eq. [4.37]. The state |φ7 i of Eq. [4.37] has non-zero entry at the following places
(10)10 ≡ (1010)2 , (3)10 ≡ (0011)2 , (12)10 ≡ (1100)2 , and (5)10 ≡ (0101)2 . These places are
calculated by the recursive function RecurChainRL4, and these place values will be stored
in list. Additionally, a factor of +1 or −1 will be multiplied to this place values depending
on whether the corresponding coefficient sign is positive or negative, respectively. For the
case under consideration, the list which will be generated is [0, −3, −12, 15].
|000i + |1111i
|GHZi = √ . (4.39)
2
A generalization of it to N qubits (N ≥ 3) gives us the N -qubit GHZ state which can be
defined as
|0i⊗N + |1i⊗N |00000 · · · 0iN + |11111 · · · 1iN
|GHZi = √ = √ . (4.40)
2 2
These states are extensively used in quantum teleportation [50, 51], quantum cryptography
protocols [52–54] and quantum game theory [55]. It is in fact quite interesting to note
that the N -qubit GHZ state is the ground state of a ferromagnet [56]. Another interesting
property of the three-qubit GHZ state (N = 3 case) is that all the three qubits are entangled
but no two-qubits are entangled.
nGHZ(tot_spins=3)
Parameters
[in] tot_spins It is the total number of qubits. Its default value is equal
to 3
Implementation
def nGHZ(self,tot_spins=3):
"""
Construct N-qubit GHZ state
Input:
tot_spins: it is the total number of spins, it should be equal to
or greater than 3.
Output:
state: N-qubit GHZ state.
"""
110 Quantum Information and Quantum Computation Tools
Example
state=gt_obj.nGHZ(tot_spins=3)
print(state)
1
|W i = √ (|100i + |010i + |001i) . (4.41)
3
It is very different from the three qubit GHZ state in terms of entanglement properties, that
is, all the three qubits are entangled and even the two-qubit subsystems are entangled. It
is also interesting to note that the three states making up the W state are such that there
is a shift of the qubit |1i, either to the right or left with respect to the first state. A natural
generalization of the W state to the N qubit case is as follows,
1
|W i = √ (|100 · · · 0i + |010 · · · 0i + · · · + |000 · · · 1i) . (4.42)
N
nW(tot_spins=3)
Parameters
Implementation
def nW(self,tot_spins=3):
"""
Construct N-qubit W state
Input:
tot_spins: it is the total number of spins, it should be equal to
or greater than 3.
Output:
state: N-qubit W state.
"""
# Checking number of spins N ≥ 3
assert tot_spins >= 3, "Total number of spins are less than 3"
# Constructing the |W i state
state=np.zeros([2**tot_spins])
for i in range(0,tot_spins):
state[2**i]=1/np.sqrt(tot_spins)
return state
Example
state=gt_obj.nW(tot_spins=3)
print(state)
Parameters
Implementation
def nWerner(self,p,tot_spins=2):
"""
Construct N-qubit Werner state
Input:
tot_spins: it is the total number of spins, it should be equal to
or greater than 2.
p: it is the mixing probability
Output:
rho: N-qubit Werner state.
"""
# Checking total number of spins N ≥ 2
assert tot_spins >= 2, "Total number of spins are less than 2"
qobj=QM()
# If N = 2 then |GHZi = |b3 i
if tot_spins == 2:
state=self.bell3()
else:
state=self.nGHZ(tot_spins=tot_spins)
den=qobj.outer_product_rvec(state,state)
# Constructing the identity matrix IN
identity=np.identity(2**tot_spins, dtype = 'float64')
identity=identity*(1/(2**tot_spins))
# Finally constructing the N-qubit Werner state ρ(p)
rho=(p*den)+((1-p)*identity)
return rho
Example
state=gt_obj.nWerner(p=0.5,tot_spins=2)
print(state)
The above code prints the two qubit generalized Werner state as given in Eq. [4.43].
[[0.375 0. 0. 0.25 ]
[0. 0.125 0. 0. ]
[0. 0. 0.125 0. ]
[0.25 0. 0. 0.375]]
Shannon entropy 113
Shannon entropy is one of the most important information theoretic measures widely used.
Note that when N = 2, the corresponding entropy is called the binary entropy which is,
Shannon entropy is useful while computing quantities like mutual information, and condi-
tional entropies [61].
shannon_entropy(pvec)
Parameters
Implementation
def shannon_entropy(self,pvec):
"""
Calculates the Shannon entropy
Input:
pvec: column vector which contains probabilities
Output:
se: it returns the Shannon entropy value
"""
size=pvec.shape[0]
se=0.0 # It stores Shannon entropy value H(p)
for i in range(0,size):
# Checking probability p is bounded between 0 ≤ p ≤ 1
assert pvec[i]<=1 and pvec[i] >=0, "probability values are
incorrect"
se=se-(pvec[i]*math.log2(pvec[i]))
return se
114 Quantum Information and Quantum Computation Tools
Example
In this example, we are finding the Shannon entropy of the probability vector p =
(0.3, 0.2, 0.5).
pvec=np.array([0.3, 0.2, 0.5])
se=gt_obj.shannon_entropy(pvec)
print(se)
linear_entropy(rho)
Parameters
Implementation
Example
In this example, we are finding the linear entropy of a two qubit generalized Werner state
ρ(0).
state=gt_obj.nWerner(0.0)
le=gt_obj.linear_entropy(state)
print(le)
In the following example, we are finding the linear entropy of a complex density matrix
defined below.
0.4 −0.2 − ι̇0.1
ρ0 = (4.48)
−0.2 + ι̇0.1 0.6
state=np.array([[complex(0.4,0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6,0.0)]])
le=gt_obj.linear_entropy(state)
print(le)
There is a term called participation ratio which is also related to purity of state and it is
defined as,
1
PR = . (4.49)
T r(ρ2 )
P R is interpreted as the effective number of pure states entering to form the mixture. We
have given an example of finding the participation ratio for I/2.
import numpy as np
PR = 1/np.trace(np.matmul(identity_matrix,identity_matrix))
Note that there is no upper limit on the value of relative entropy and S(ρ k σ) ≥ 0,
the equality holds when ρ = σ. This definition of relative entropy is with respect to base
e, however it can be changed to any base by dividing it with the appropriate conversion
factor. Relative entropy is used in the calculation of quantum mutual information, which is
an important quantity in information theory.
relative_entropy(rho,sigma)
Parameters
[out] rtent rtent is the relative entropy between states rho and sigma
density matrices
Implementation
def relative_entropy(self,rho,sigma):
"""
Calculates relative entropy
Input:
rho: input density matrix
sigma: input density matrix
Output:
rtent: the value of relative entropy
"""
laobj=LA()
typerho=str(rho.dtype)
typesig=str(sigma.dtype)
Relative entropy 117
if re.findall('^float|^int',typerho):
# Calculating log(ρ), where ρ = ρT
logrho=laobj.function_smatrix(rho, mode="log",\
log_base=math.exp(1))
elif re.findall("^complex",typerho):
# Calculating log(ρ), where ρ = ρ†
logrho=laobj.function_hmatrix(rho, mode="log",\
log_base=math.exp(1))
if re.findall('^float|^int',typesig):
# Calculating log(σ), where σ = σ T
logsig=laobj.function_smatrix(sigma, mode="log",\
log_base=math.exp(1))
elif re.findall("^complex",typesig):
# Calculating log(σ), where σ = σ †
logsig=laobj.function_hmatrix(sigma, mode="log",\
log_base=math.exp(1))
rtent=np.trace(np.matmul(rho,logrho))-np.trace(np.matmul(rho,logsig))
rtent=abs(rtent)
return rtent
Example
In this example, we are finding the relative entropy between the two qubit generalized
Werner states ρ(0.3) and ρ(0.5).
state1=gt_obj.nWerner(0.3)
state2=gt_obj.nWerner(0.5)
rtent=gt_obj.relative_entropy(state1,state2)
print(rtent)
In this example, we are finding the relative entropy between two complex density matrices
defined below,
0.4 −0.2 − ι̇0.1 0.4 −0.1 − ι̇0.4
ρ1 = , ρ2 = (4.51)
−0.2 + ι̇0.1 0.6 −0.1 + ι̇0.4 0.6
state1=np.array([[complex(0.4,0.0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6)]])
state2=np.array([[complex(0.4,0.0),complex(-0.1,-0.4)],\
[complex(-0.1,0.4),complex(0.6,0.0)]])
rtent=gt_obj.relative_entropy(state1,state2)
print(rtent)
trace_distance(rho,sigma)
Parameters
[out] trd trd is the trace distance between states rho and sigma
Implementation
def trace_distance(self,rho,sigma):
"""
Calculates trace distance between two density matrices
Input:
rho: input density matrix
sigma: input density matrix
Output:
trd: it stores trace distance
"""
# Calculating ρ − σ
res=rho-sigma
laobj=LA()
typeres=str(res.dtype)
if re.findall('^float|^int',typeres):
trd=laobj.trace_norm_rmatrix(res)
trd=trd/2
elif re.findall("^complex",typeres):
trd=laobj.trace_norm_cmatrix(res)
trd=trd/2
return trd
Fidelity 119
Example
In this example, we are finding the trace distance between the two-qubit generalized Werner
state ρ(p).
state1=gt_obj.nWerner(0.3)
state2=gt_obj.nWerner(0.5)
trd=gt_obj.trace_distance(state1,state2)
print(trd)
In this example, we are finding the trace distance between two complex density matrices ρ1
and ρ2 as defined in Eq. [4.51].
state1=np.array([[complex(0.4,0.0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6)]])
state2=np.array([[complex(0.4,0.0),complex(-0.1,-0.4)],\
[complex(-0.1,0.4),complex(0.6,0.0)]])
trd=gt_obj.trace_distance(state1,state2)
print(trd)
4.10 Fidelity
Fidelity [64, 65] is a measure of how close two quantum states are. It has a variety of
applications such as in the field of quantum teleportation [66], quantum error correction [67],
quantum spin chain system [68], etc. Fidelity between two density matrices ρ and σ can be
found using the trace norm as
√ √
F =k ρ σ k2 . (4.53)
In the special case when ρ and σ are pure states, that is, ρ = |ψ ρ ihψρ | and σ = |ψσ ihψσ |.
Fidelity can be found as
F = |hψρ |ψσ i|2 . (4.54)
Fidelity between a pure state |ψi and a density matrix σ can also be found as
F = hψ|σ|ψi. (4.55)
Note that fidelity always lies between 0 and 1. In fact the fidelity for pure states is similar
to the overlap between the states.
fidelity_vec2(self,vecrho,vecsigma)
120 Quantum Information and Quantum Computation Tools
Parameters
Implementation
def fidelity_vec2(self,vecrho,vecsigma):
"""
Calculates fidelity between two quantum states
Input:
vecrho: input pure state vector.
vecsigma: input pure state vector.
Output:
fidelity: it stores the value of fidelity
"""
typerho=str(vecrho.dtype)
if re.findall('^complex',typerho):
# Calculating, hψρ |ψσ i
fidelity=np.matmul(np.matrix.\
conjugate(np.matrix.transpose(vecrho)),\
vecsigma)
else:
# Calculating, hψρ |ψσ i
fidelity=np.matmul(np.matrix.transpose(vecrho), vecsigma)
# Calculating, |hψρ |ψσ i|2
fidelity=abs(fidelity)**2
return fidelity
Example
In this example, we are finding the fidelity between two-qubit states |ψ1 i and |ψ2 i.
from QuantumInformation import GatesTools as GT
gt_obj=GT()
state1=np.zeros([4],dtype='float64')
for i in range(0,state1.shape[0]):
state1[i]=i+1
state1=qobj.normalization_vec(state1)
state2=np.zeros([4],dtype='float64')
state2[0]=1/np.sqrt(2)
state2[3]=1/np.sqrt(2)
F=gt_obj.fidelity_vec2(state1,state2)
print(F)
Fidelity 121
In this example, we are finding the fidelity between two-qubit states |ψ3 i and |ψ4 i.
from QuantumInformation import GatesTools as GT
gt_obj=GT()
state3=np.zeros([4],dtype=np.complex_)
for i in range(0,state3.shape[0]):
state3[i]=complex(i,i+1)
state3=qobj.normalization_vec(state3)
state4=np.zeros([4],dtype=np.complex_)
for i in range(0,state4.shape[0]):
state4[i]=complex(i,i+2)
state4=qobj.normalization_vec(state4)
F=gt_obj.fidelity_vec2(state3,state4)
print(F)
fidelity_den2(rho,sigma)
Parameters
[out] fidelity It is the fidelity between density matrices rho and sigma
Implementation
def fidelity_den2(self,rho,sigma):
"""
Calculates fidelity between two density matrices
Input:
rho: input density matrix
sigma: input density matrix
Output:
fidelity: it stores the value of fidelity
"""
122 Quantum Information and Quantum Computation Tools
laobj=LA()
typerho=str(rho.dtype)
typesig=str(sigma.dtype)
flag=0
if re.findall('^float|^int',typerho):
√
# Calculating ρ, if ρ = ρT
rhosq=laobj.power_smatrix(rho,0.5)
elif re.findall("^complex",typerho):
√
# Calculating ρ, if ρ = ρ†
rhosq=laobj.power_hmatrix(rho,0.5)
flag=1
if re.findall('^float|^int',typesig):
√
# Calculating σ, if σ = σ T
sigsq=laobj.power_smatrix(sigma,0.5)
elif re.findall("^complex",typesig):
√
# Calculating σ, if σ = σ †
sigsq=laobj.power_hmatrix(sigma,0.5)
flag=1
if flag==0:
√ √
# If both matrices are real, calculate || ρ σ||2
fidelity=laobj.trace_norm_rmatrix(np.matmul(rhosq,sigsq))
fidelity=fidelity**2
else:
√ √
# If any of the matrix is complex, calculate || ρ σ||2
fidelity=laobj.trace_norm_cmatrix(np.matmul(rhosq,sigsq))
fidelity=fidelity**2
return fidelity
Example
In this example, we are finding the fidelity between two-qubit generalized Werner states
ρ(0.3) and ρ(0.5).
state1=gt_obj.nWerner(0.3)
state2=gt_obj.nWerner(0.5)
F=gt_obj.fidelity_den2(state1,state2)
print(F)
In this example, we are finding the fidelity between the complex states ρ1 and ρ2 .
state1=np.array([[complex(0.4,0.0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6)]])
state2=np.array([[complex(0.4,0.0),complex(-0.1,-0.4)],\
[complex(-0.1,0.4),complex(0.6,0.0)]])
F=gt_obj.fidelity_den2(state1,state2)
print(F)
0.8706512518934156
fidelity_vecden(vec,sigma)
Parameters
[in] vec vec is a real or complex state of dimension (0:2n −1), where
n is number of spins or qubits
[out] fidelity It is the fidelity between state vec and density matrix sigma
Implementation
def fidelity_vecden(self,vec,sigma):
"""
Calculates fidelity between a quantum state and a density matrix
Input:
vec: input pure state vector.
sigma: input density matrix
Output:
fidelity: it stores the value of fidelity
"""
typevec=str(vec.dtype)
if re.findall('^complex',typevec):
# Complex case: hψ|σ|ψi
fidelity=np.matmul(np.matrix.\
conjugate(np.matrix.transpose(vec)),\
np.matmul(sigma,vec))
else:
# Real case: hψ|σ|ψi
fidelity=np.matmul(np.matrix.transpose(vec),\
np.matmul(sigma,vec))
fidelity=abs(fidelity)
return fidelity
Example
In this example, we are finding the fidelity between two-qubit state |ψ2 i and two-qubit
generalized Werner state ρ(0.3).
124 Quantum Information and Quantum Computation Tools
In this example, we are finding the fidelity between the one-qubit complex state |ψ3 i and
complex density matrix ρ1 .
from QuantumInformation import GatesTools as GT
gt_obj=GT()
state1=np.zeros([2],dtype=np.complex_)
for i in range(0,2):
state1[i]=complex(i,i+1)
state1=qobj.normalization_vec(state1)
state2=np.array([[complex(0.4,0.0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6,0.0)]])
F=gt_obj.fidelity_vecden(state1,state2)
print(F)
for two-qubit states the super fidelity is the same as fidelity. The super fidelity is bounded
between 0 and 1, and it is concave function as shown below,
super_fidelity(rho,sigma)
Super fidelity 125
Parameters
Implementation
def super_fidelity(self,rho,sigma):
"""
Calculates super fidelity between two density matrices
Input:
rho: input density matrix.
sigma: input density matrix.
output:
sf: the value of the super fidelity
"""
# Calculating T r(ρ2 )
tr_rho2=np.trace(np.matmul(rho,rho))
# Calculating T r(σ 2 )
tr_sigma2=np.trace(np.matmul(sigma,sigma))
# Calculating T r(ρσ)
tr_rhosigma=np.trace(np.matmul(rho,sigma))
# Below we calculate, G(ρ, σ)
sf=tr_rhosigma+np.sqrt((1-tr_rho2)*(1-tr_sigma2))
sf=abs(sf)
return sf
Example
In this example, we are finding the super fidelity between the two-qubit generalized Werner
states ρ(0.3) and ρ(0.5).
state1=gt_obj.nWerner(0.3)
state2=gt_obj.nWerner(0.5)
SF=gt_obj.super_fidelity(state1,state2)
print(SF)
In this example, we are finding the super fidelity between two complex density matrices ρ1
and ρ2 .
126 Quantum Information and Quantum Computation Tools
state1=np.array([[complex(0.4,0.0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6)]])
state2=np.array([[complex(0.4,0.0),complex(-0.1,-0.4)],\
[complex(-0.1,0.4),complex(0.6,0.0)]])
SF=gt_obj.super_fidelity(state1,state2)
print(SF)
where F is the fidelity between two quantum states ρ and σ. In its own right Bures distance
qualifies to be an entanglement measure.
bures_distance_vec(rho,sigma)
Parameters
[in] rho rho is a real or complex state of dimension (0:2n −1), where
n is number of spins or qubits
[out] bd It is the Bures distance between the states rho and sigma
Implementation
def bures_distance_vec(self,rho,sigma):
"""
Calculates Bures distance between two quantum state
Input:
rho: input state vector
sigma: input state vector
Bures distance 127
Output:
bd: the value of the Bures distance
"""
# Calculating F (ρ, σ)
fid=self.fidelity_vec2(rho,sigma)
# Finally we calculate Bures distance D
bd=np.sqrt(2*(1-np.sqrt(fid)))
return bd
Example
In this example, we are finding the Bures distance between the two-qubit states |ψ1 i and
|ψ2 i.
from QuantumInformation import GatesTools as GT
gt_obj=GT()
state1=np.zeros([4],dtype='float64')
state2=np.zeros([4],dtype='float64')
for i in range(0,4):
state1[i]=i+1
state1=qobj.normalization_vec(state1)
state2[0]=1/np.sqrt(2)
state2[3]=1/np.sqrt(2)
Bd=gt_obj.bures_distance_vec(state1,state2)
print(Bd)
In this example, we are finding the Bures distance between the two-qubit complex states
|ψ3 i and |ψ4 i.
state1=np.zeros([4],dtype=np.complex_)
state2=np.zeros([4],dtype=np.complex_)
for i in range(0,4):
state1[i]=complex(i,i+1)
state2[i]=complex(i,i+2)
state1=qobj.normalization_vec(state1)
state2=qobj.normalization_vec(state2)
Bd=gt_obj.bures_distance_vec(state1,state2)
print(Bd)
bures_distance_den(rho,sigma)
128 Quantum Information and Quantum Computation Tools
Parameters
Implementation
def bures_distance_den(self,rho,sigma):
"""
Calculates Bures distance between two density matrix
Input:
rho: input density matrix
sigma: input density matrix
Output:
bd: the value of the Bures distance
"""
# Calculating F (ρ, σ)
fid=self.fidelity_den2(rho,sigma)
# Finally we calculate Bures distance D
bd=np.sqrt(2*(1-np.sqrt(fid)))
return bd
Example
In this example, we are finding the Bures distance between the two qubit generalized Werner
states ρ(0.3) and ρ(0.5).
state1=gt_obj.nWerner(0.3)
state2=gt_obj.nWerner(0.5)
Bd=gt_obj.bures_distance_den(state1,state2)
print(Bd)
In this example, we are finding the Bures distance between complex states ρ1 and ρ2 .
state1=np.array([[complex(0.4,0.0),complex(-0.2,-0.1)],\
[complex(-0.2,0.1),complex(0.6)]])
state2=np.array([[complex(0.4,0.0),complex(-0.1,-0.4)],\
[complex(-0.1,0.4),complex(0.6,0.0)]])
Expectation value of an observable 129
Bd=gt_obj.bures_distance_den(state1,state2)
print(Bd)
When many measurements are done on N copies of the quantum system, the expectation
value is the average value obtained when the number of such systems goes to infinity, that
is, N → ∞. The expectation value of any observable is a real number.
expectation_vec(vec,obs)
Parameters
Implementation
def expectation_vec(self,vec,obs):
"""
Expectation values of observable for a quantum state
Input:
vec: input state vector
obs: observable operator
Output:
expc: the expectation value of the measurement operator
"""
130 Quantum Information and Quantum Computation Tools
typevec=str(vec.dtype)
if re.findall('^complex',typevec):
# Calculating hψ|A|ψi, if |ψi is complex
expc=np.matmul(np.matmul(np.matrix.\
conjugate(np.matrix.transpose(vec)),\
obs),vec)
else:
# Calculating hψ|A|ψi, if |ψi is real
expc=np.matmul(np.matmul(np.matrix.transpose(vec),obs),vec)
return expc
Example
In this example, we are finding the expectation value of σ z with respect to |1i.
sz=gt_obj.sz()
state=np.array([0,1])
expec=gt_obj.expectation_vec(state,sz)
print(expec)
expectation_den(rho,obs)
Parameters
Implementation
def expectation_den(self,rho,obs):
"""
Expectation values of observable for a density matrix
Input:
rho: input density matrix
obs: observable operator
Output:
expc: the expectation value of the observable operator
"""
return np.trace(np.matmul(rho,obs))
Example
In this example, we are finding the expectation value of σ z with respect to |0ih0|.
sz=gt_obj.sz()
rho=np.array([[1,0],[0,0]])
expc=gt_obj.expectation_den(rho,sz)
print(expc)
Another
example
where we are finding the expectation value of σ with respect to
y
|0i+ι̇|1i h0|−ι̇h1|
√
2
√
2
.
sy=gt_obj.sy()
state=np.array([1/np.sqrt(2),complex(0,-1/np.sqrt(2))])
state=qobj.outer_product_cvec(state,state)
expc=gt_obj.expectation_den(state,sy)
print(expc)
where I16×16 is a 16 × 16 identity matrix. Finally, we calculate the fidelity between the pure
state |φ1 i4 , and density matrix ρ04 as shown below.
From the probability distribution shown in Fig. (5.1), it is clear that the outcome of any
one of the coins does not influence the outcome of the other coin. If the outcome of the first
coin is head, then with a 50% probability that the outcome of the second coin will be head
or tail. Now consider a case where we have two special coins, which have discrete probability
distributions as shown in Fig. (5.2). The table in Fig. (5.2) shows extreme entanglement.
If the outcome of the first special coin is head, then with 100% surety, we know the
outcome of the second special coin will be head. With this example discussed we now
introduce a more formal definition of quantum entanglement.
Entanglement is a non-local correlation which exists between systems that are sepa-
rated in space. Any measurement in one of the subsystems will affect the state of the
other subsystem, essentially it means that the subsystems are no longer independent [58].
Entanglement plays an important role in not only understanding the basic nature of quan-
tum states but also indispensable in understanding condensed matter systems like spin
chains [80–82], and quantum phase transitions [83, 84]. It is also an integral part of quan-
tum computation [85–87], etc.
The nature of entanglement in pure and mixed states are very different from each other
and, as we go along, we shall see the ways to detect and quantify entanglement. As far as
pure states are concerned, if we have a composite system AB which is in a state |ψiAB
made of two subsystems A and B, if the state of the composite system can be written as a
tensor product of the individual subsystem states, then we say that |ψiAB unentangled, if
not they are entangled. For example, consider one of the well known two-qubit Bell states
which is,
1
|b1 i = √ (|00iAB + |11iAB )
2
6= |some state of system Ai ⊗ |some state of system Bi (5.1)
In the above equation, we see that |b1 i is an entangled state. However, in the same token,
it is easy to see that the state |φi = √12 (|00iAB + |01iAB ) = √12 (|0iA ⊗ (|0iB + |1iB ))
is an unentangled state. In the state defined as |b1 i, we see that it is defined in terms of
the eigenvectors of the Pauli Z matrix, however, if we rotate the basis such that the state
is written in terms of the eigen vectors of the Pauli X matrix, even then, the non-local
correlations or entanglement does not die as the new state preserves these correlations up
to an arbitrary global phase. In the language of measurement theory, we see that in the
state |b1 i, if we measure the spin in the first qubit to be in the state |0i we can immediately,
without even measuring the second qubit say with 100 percent probability that it will be
in the state |0i, however, if we see the state |φi, measuring the first qubit to be in state |0i,
will throw the state of the second qubit in a superposition namely √12 (|0iB + |1iB ), which
is a hallmark of entanglement. However, coming to the arena of density matrices, A density
matrix ρ is called separable [88] if it can be written as a convex sum of product states ρA
and ρB , as X
ρ= pi ρA B
i ⊗ ρi , (5.2)
i
This definition, coupled with the fact that there is no unique decomposition [89] for a given
density matrix in terms of pure states, leads to a huge market of entanglement measures and
associated entanglement detection techniques. Some of them like PPT and reduction are
commonly used, and day by day, the research on these increase the number of such measures
and detection methods which practically makes them weaker or stronger with respect to each
other [90]. For qubit-qubit and qubit-qutrit systems (2×2 and 2×3, respectively), the simple
Peres–Horodecki criterion or the famous positive partial transpose PPT criteria [88] provides
both a necessary and a sufficient criterion for separability. This cannot be generalized to
higher-dimensional quantum states, directly makes detection non-unique.
For studying the entanglement content [91] or detecting the presence of entanglement
between two subsystems of a quantum system, we perform the following mathematical
operations on the quantum state of the composite quantum system, partial trace and partial
transpose. More detailed discussions regarding partial trace and partial transpose are given
in Sections 5.1 and 5.2, respectively.
In this chapter, we will be using the same states and density matrices as given in Chap-
ter 4 to demonstrate our recipes and outputs. (In this chapter) Here, the array named
“site” stores the index corresponding to the site of the qubits, which has been done via
zero-based indexing, meaning 0 corresponds to the first qubit and so on. Primarily we will
Partial trace 135
be discussing two classes, PartialTr and Entanglement. Both of these classes are writ-
ten inside the Python module chap5_partial_quantumentanglement.py. The class
PartialTr contains all the methods pertaining to partial trace and partial transpose. The
Entanglement class also inherits all methods from the PartialTr class. The procedure for
importing the classes and creating their objects, you have to write the following code,
# Importing both of the classes
from QuantumInformation import PartialTr as PT
from QuantumInformation import Entanglement as ENT
ρA = T rB (ρAB ). (5.3)
ρB = T rA (ρAB ). (5.4)
It can be generalized to multi-qubit systems also in this way, if we have a N -qubit state
ρ123···N , and if we are interested to know the state of the even qubit subsystem, we can write
ρ246··· = T r135··· (ρ123···N ). This implies that partial tracing can be done over a random set of
qubits. The partial trace operation is the faithful reflection of the marginal of a multivariate
probability distribution in the quantum domain. Partial trace operation is practically used
in calculating most of the entanglement measures as we will see further down. In the recipes
which we have given, for a N -qubit state, we can calculate the partial trace over any random
set of qubits as ρi1 i2 ···iN .
The following two methods use the function recur_comb_add from the module Re-
curNum and two private methods __dectobin, __bintodec, which are present within
the PartialTr class. The private methods are those methods which cannot be accessed
outside of the class. The reason to develop private methods is explained as follows, the
private method __dectobin (__bintodec) converts a decimal (binary) number to an
equivalent binary (decimal) number. We have already developed methods that convert dec-
imal to binary and vice versa, but the format of the input and output of these private
methods are different. The algorithm for the partial tracing in the following two methods
has been adopted from the ref. [92], and it is the fastest algorithm to calculate partial trace
136 Quantification of Quantum Entanglement
reported till now to the best of our knowledge. The ref. [92] uses the concept of power set to
construct the reduced density matrix, and to this end, we have used the recursive function
recur_comb_add in order to generate the power set.
partial_trace_vec(state,sub_tr)
Parameters
Implementation
def partial_trace_vec(self,state,sub_tr):
"""
Partial trace operation on a quantum state
Input:
state: real state vector
sub_tr: details of the subsystems not to be traced out
Output:
red_den: reduced density matrix
"""
# Storing data type (real or complex) input vector
typestate=str(state.dtype)
# Storing the number of spins
N=int(math.log2(state.shape[0]))
# Number of subsystems to be traced out
length=len(sub_tr)
# Checking whether entered subsystems are valid or not
assert set(sub_tr).issubset(set(np.arange(1,N+1))),\
"Invalid subsystems to be traced out"
# If the input state is complex then following condition is true
if re.findall("^complex",typestate):
# Constructing the reduced density matrix ρ0
red_den=np.zeros([2**(length),2**(length)],dtype=np.complex_)
vec=np.zeros([(N-length),1])
Partial trace 137
im=0
# Binary place value of the traced out subsystems
for ii in range(1,N+1):
if ii not in sub_tr:
# Creating the set S
vec[im]=2**(N-ii)
im=im+1
mylist=[]
icount=0
sum2=0
# Creating the power set P (S)
RecurNum.recur_comb_add(mylist,vec,icount,sum2)
irow=np.zeros([N,1])
icol=np.zeros([N,1])
mylist=np.array(mylist)
len_mylist=len(mylist)
# Row index of ρ0
for i1 in range(0,2**length):
col1=self.__dectobin(i1,length)
# Column index of ρ0
for i2 in range(0,2**length):
col2=self.__dectobin(i2,length)
i3=0
for k in range(0,N):
if k+1 not in sub_tr:
irow[k]=0
else:
irow[k]=col1[i3]
i3=i3+1
ic=0
for k2 in range(0,N):
if k2+1 not in sub_tr:
icol[k2]=0
else:
icol[k2]=col2[ic]
ic=ic+1
icc=self.__bintodec(irow)
jcc=self.__bintodec(icol)
red_den[i1,i2]=red_den[i1,i2]+(state[icc]*\
np.conjugate(state[jcc]))
for jj in range(0,len_mylist):
icc2=icc+mylist[jj]
jcc2=jcc+mylist[jj]
red_den[i1,i2]=red_den[i1,i2]+(state[icc2]*\
np.conjugate(state[jcc2]))
# If the input state is real then following condition is true
else:
# Constructing the reduced density matrix ρ0
red_den=np.zeros([2**(length),2**(length)],dtype='float64')
vec=np.zeros([(N-length),1])
im=0
138 Quantification of Quantum Entanglement
Example
In this example, we are performing the partial trace on the real four-qubit state |ψ2 i and
we will get the reduced density matrix ρ24 as an output by tracing over qubits 1 and 3.
Partial trace 139
The preceding code prints the required reduced density matrix ρ24 as shown below.
0.141711235 0.157754004 0.205882356 0.221925139
0.157754004 0.176470593 0.232620314 0.251336902
0.205882356 0.232620314 0.312834233 0.339572191
0.221925139 0.251336902 0.339572191 0.368983954
Similarly, we perform partial trace on the complex four-qubit state |ψ3 i and we will obtain
the reduced density matrix ρ4 as an output by tracing over qubits 1, 2 and 3.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 i
state2=np.zeros([16],dtype=np.complex_)
for i in range(0,16):
state2[i]=complex(i,i+1)
# Finally constructing the state |ψ3 i
state2=qobj.normalization_vec(state2)
# The subsystems to be retained
sub_tr=[4]
# Finally obtaining ρ4
red_den=pobj.partial_trace_vec(state2,sub_tr)
print(red_den)
The preceding code prints the required reduced density matrix ρ4 as shown below.
[[0.45321637+0.j 0.49707602+0.00292398j]
[0.49707602-0.00292398j 0.54678363+0.j ]]
partial_trace_den(state,sub_tr)
140 Quantification of Quantum Entanglement
Parameters
Implementation
def partial_trace_den(self,state,sub_tr):
"""
Partial trace operation on a density matrix
Input:
state: input real density matrix
sub_tr: details of the subsystem not to be traced out
Output:
red_den: reduced density matrix
"""
# Data type (real or complex) of the input density matrix
typestate=str(state.dtype)
N=int(math.log2(state.shape[0]))
length=len(sub_tr)
# count=length, and count0= N-length
# Checking whether entered subsystems are valid or not
assert set(sub_tr).issubset(set(np.arange(1,N+1))),\
"Invalid subsystems to be traced out"
# If the density matrix is complex then following condition is true
if re.findall("^complex",typestate):
# Constructing the reduced density matrix ρ0
red_den=np.zeros([2**(length),2**(length)],dtype=np.complex_)
vec=np.zeros([(N-length),1])
im=0
for ii in range(1,N+1):
if ii not in sub_tr:
# Constructing the set S
vec[im]=2**(N-ii)
im=im+1
mylist=[]
icount=0
sum2=0
# Constructing the power set P (S)
RecurNum.recur_comb_add(mylist,vec,icount,sum2)
Partial trace 141
irow=np.zeros([N,1])
icol=np.zeros([N,1])
mylist=np.array(mylist)
len_mylist=len(mylist)
# Row index of ρ0
for i1 in range(0,2**length):
col1=self.__dectobin(i1,length)
# Column index of ρ0
for i2 in range(0,2**length):
col2=self.__dectobin(i2,length)
i3=0
for k in range(0,N):
if k+1 not in sub_tr:
irow[k]=0
else:
irow[k]=col1[i3]
i3=i3+1
ic=0
for k2 in range(0,N):
if k2+1 not in sub_tr:
icol[k2]=0
else:
icol[k2]=col2[ic]
ic=ic+1
icc=self.__bintodec(irow)
jcc=self.__bintodec(icol)
red_den[i1,i2]=red_den[i1,i2]+(state[icc,jcc])
for jj in range(0,len_mylist):
icc2=icc+mylist[jj]
jcc2=jcc+mylist[jj]
red_den[i1,i2]=red_den[i1,i2]+(state[icc2,jcc2])
# If the density matrix is real then following condition is true
else:
# Constructing the reduced density matrix ρ0
red_den=np.zeros([2**(length),2**(length)],dtype='float64')
vec=np.zeros([(N-length),1])
im=0
for ii in range(1,N+1):
if ii not in sub_tr:
# Constructing the set S
vec[im]=2**(N-ii)
im=im+1
mylist=[]
icount=0
sum2=0
# Constructing the power set P (S)
RecurNum.recur_comb_add(mylist,vec,icount,sum2)
irow=np.zeros([N,1])
icol=np.zeros([N,1])
mylist=np.array(mylist)
len_mylist=len(mylist)
142 Quantification of Quantum Entanglement
Example
In this example, we are performing the partial trace on the real four-qubit state |ψ2 ihψ2 |
and we will get the reduced density matrix ρ24 as an output by tracing out qubits 1 and 3.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 i
state2=np.zeros([2**4],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
# Finally constructing the state |ψ2 i
state2=qobj.normalization_vec(state2)
# Constructing the density matrix, |ψ2 ihψ2 |
den=qobj.outer_product_rvec(state2,state2)
sub_tr=[2,4]
# Finally calculated the reduced density matrix, ρ24
red_den=pobj.partial_trace_den(den,sub_tr)
print(red_den)
Partial transpose 143
The preceding code prints the required reduced density matrix ρ24 as shown below.
[[0.14171123 0.15775401 0.20588235 0.22192513]
[0.15775401 0.17647059 0.23262032 0.2513369 ]
[0.20588235 0.23262032 0.31283422 0.33957219]
[0.22192513 0.2513369 0.33957219 0.36898396]]
Similarly, we perform the partial trace on a four-qubit state |ψ3 ihψ3 | and we will get the
reduced density matrix ρ4 as an output by tracing over qubits 1, 2 and 3.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 i
state2=np.zeros([2**4],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
# Finally constructing the state |ψ3 i
state2=qobj.normalization_vec(state2)
# Constructing the density matrix, |ψ3 ihψ3 |
den=qobj.outer_product_cvec(state2,state2)
sub_tr=[4]
# Finally calculated the reduced density matrix, ρ4
red_den=pobj.partial_trace_den(den,sub_tr)
print(red_den)
The preceding code prints the required reduced density matrix ρ4 as shown below.
[[0.45321637+0.j 0.49707602+0.00292398j]
[0.49707602-0.00292398j 0.54678363+0.j ]]
In the above equation, indices i, k are basis states in the Hilbert space HA of subsystem A
and j, l are basis states in the Hilbert space HB of subsystem B. Here rik;jl are the matrix
144 Quantification of Quantum Entanglement
elements. The partial transpose of ρAB with respect to the system B is given by,
X
ρTAB
B
= rik;jl (|iihk|)A ⊗ (|lihj|)B . (5.6)
ijkl
Of course, ρTAB
A
also can be found easily using the above. For a more in-depth understanding
regarding the mathematical operation of partial transpose, we have defined a two-qubit
density matrix as
a11 a12 a13 a14
a21 a22 a23 a24
ρAB = . (5.7)
a31 a32 a33 a34
a41 a42 a43 a44
After partially transposing the subsystem B of the density matrix in Eq. [5.7], we finally
obtain the following,
a a21 a13 a23
11
a12 a22 a14 a24
ρTAB
B
= . (5.8)
a13 a41 a33 a43
a32 a42 a34 a44
The recipes we have given can do partial transpose for any random partition of a bipartite
multi-qubit system.
ptranspose_vec(state,sub_tr)
Parameters
Implementation
def ptranspose_vec(self,state,sub_tr):
"""
Partial transpose operation on a quantum state
Parameters
state : It is a real or complex state.
sub_tr : List of number designating the subsystems
to be partially transposed.
Returns
denc2: It is partially transposed density matrix
"""
# Total number of qubits N
N=int(math.log2(state.shape[0]))
# Checking whether entered values of subsystems are correct or not
assert set(sub_tr).issubset(set(np.arange(1,N+1))),\
"Invalid subsystems to be traced out"
# The data type of the state, real or complex
typestate=str(state.dtype)
# Checking whether state is complex or not
if re.findall("^complex",typestate):
# It will store transposed density matrix ρTAB
B
denc2=np.zeros([2**N,2**N],dtype=np.complex_)
for i in range(state.shape[0]):
vec_row=qobj.decimal_binary(i,N)
for j in range(state.shape[0]):
vec_col=qobj.decimal_binary(j,N)
vec_row2=vec_row.copy()
for k in sub_tr:
temp=vec_row2[k-1]
vec_row2[k-1]=vec_col[k-1]
vec_col[k-1]=temp
row=qobj.binary_decimal(vec_row2)
col=qobj.binary_decimal(vec_col)
denc2[row,col]=state[i]*np.conjugate(state[j])
else:
# It will store transposed density matrix ρTAB
B
denc2=np.zeros([2**N,2**N],dtype='float64')
for i in range(state.shape[0]):
vec_row=qobj.decimal_binary(i,N)
for j in range(state.shape[0]):
vec_col=qobj.decimal_binary(j,N)
vec_row2=vec_row.copy()
for k in sub_tr:
temp=vec_row2[k-1]
vec_row2[k-1]=vec_col[k-1]
vec_col[k-1]=temp
row=qobj.binary_decimal(vec_row2)
col=qobj.binary_decimal(vec_col)
denc2[row,col]=state[i]*state[j]
return(denc2)
146 Quantification of Quantum Entanglement
Example
In this example, we are performing the partial transpose over the second qubit of the two-
qubit pure real state |ψ2 i and we will get ρT2 as the output.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
state2=np.zeros([2**2],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
sub_tr=[2]
denc2=pobj.ptranspose_vec(state2,sub_tr)
print(denc2)
The preceding codes prints the required partially transposed matrix as shown below.
[[0.03333333 0.06666667 0.1 0.2 ]
[0.06666667 0.13333333 0.13333333 0.26666667]
[0.1 0.13333333 0.3 0.4 ]
[0.2 0.26666667 0.4 0.53333333]]
Similarly, we perform the partial transpose over the second qubit of the two-qubit pure
complex state |ψ3 i and we will get ρT2 as the output.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
state2=np.zeros([2**2],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
sub_tr=[2]
denc2=pobj.ptranspose_vec(state2,sub_tr)
file2 = open('denc2_mat.txt', 'w')
# writing matrix elements in the file
for i in range(0,denc2.shape[0]):
for j in range(0,denc2.shape[1]):
file2.write(str(denc2[i,j]))
# to break the line between row elements of matrix
file2.write('\n')
# Closing file
file2.close()
The file “denc2_mat.txt” contains the entries of required partially transposed matrix.
(0.022727272727272728+0j)
(0.045454545454545456-0.022727272727272728j)
(0.06818181818181819+0.045454545454545456j)
(0.18181818181818182+0.02272727272727272j)
(0.045454545454545456+0.022727272727272728j)
(0.11363636363636365+0j)
(0.09090909090909091+0.06818181818181819j)
(0.25+0.04545454545454547j)
(0.06818181818181819-0.045454545454545456j)
Partial transpose 147
(0.09090909090909091-0.06818181818181819j)
(0.29545454545454547+0j)
(0.40909090909090917-0.022727272727272735j)
(0.18181818181818182-0.02272727272727272j)
(0.25-0.04545454545454547j)
(0.40909090909090917+0.022727272727272735j)
(0.5681818181818182+0j)
ptranspose_den(denc,sub_tr)
Parameters
Implementation
def ptranspose_den(self,denc,sub_tr):
"""
Partial transpose operation on density matrix
Parameters
denc : It is a real or complex density matrix.
sub_tr : List of number designating the subsystems
to be partially transposed.
Returns
denc2: It is partially transposed density matrix
"""
# Total number of qubits N
N=int(math.log2(denc.shape[0]))
# Checking whether entered values of subsystems are correct or not
assert set(sub_tr).issubset(set(np.arange(1,N+1))),\
"Invalid subsystems to be traced out"
# The type density matrix, real or complex
typestate=str(denc.dtype)
# Checking density is complex
if re.findall("^complex",typestate):
148 Quantification of Quantum Entanglement
denc2=np.zeros([2**N,2**N],dtype=np.complex_)
for i in range(denc.shape[0]):
vec_row=qobj.decimal_binary(i,N)
for j in range(denc.shape[1]):
vec_col=qobj.decimal_binary(j,N)
vec_row2=vec_row.copy()
for k in sub_tr:
temp=vec_row2[k-1]
vec_row2[k-1]=vec_col[k-1]
vec_col[k-1]=temp
row=qobj.binary_decimal(vec_row2)
col=qobj.binary_decimal(vec_col)
denc2[row,col]=denc[i,j]
else:
# It will store transposed density matrix ρTAB
B
denc2=np.zeros([2**N,2**N],dtype='float64')
for i in range(denc.shape[0]):
vec_row=qobj.decimal_binary(i,N)
for j in range(denc.shape[1]):
vec_col=qobj.decimal_binary(j,N)
vec_row2=vec_row.copy()
for k in sub_tr:
temp=vec_row2[k-1]
vec_row2[k-1]=vec_col[k-1]
vec_col[k-1]=temp
row=qobj.binary_decimal(vec_row2)
col=qobj.binary_decimal(vec_col)
denc2[row,col]=denc[i,j]
return(denc2)
Example
In this example, we are performing the partial transpose over the second qubit of the two-
qubit real density matrix |ψ2 ihψ2 | and we will get ρT2 as the output.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
state2=np.zeros([2**2],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_rvec(state2,state2)
sub_tr=[2]
denc2=pobj.ptranspose_den(state2,sub_tr)
print(denc2)
The preceding code prints the required partially transposed matrix as shown below.
[[0.03333333 0.06666667 0.1 0.2 ]
[0.06666667 0.13333333 0.13333333 0.26666667]
[0.1 0.13333333 0.3 0.4 ]
[0.2 0.26666667 0.4 0.53333333]]
Concurrence 149
Similarly, we perform the partial transpose over the second qubit of the two-qubit complex
density matrix |ψ3 ihψ3 | and we will get ρT2 as the output.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
state2=np.zeros([2**2],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_cvec(state2,state2)
sub_tr=[2]
denc2=pobj.ptranspose_den(state2,sub_tr)
file2 = open('denc2_mat.txt', 'w')
# writing matrix elements in the file
for i in range(0,denc2.shape[0]):
for j in range(0,denc2.shape[1]):
file2.write(str(denc2[i,j]))
# to break the line between row elements of matrix
file2.write('\n')
# Closing file
file2.close()
The file “denc2_mat.txt” contains the row entries of the required partially transposed
matrix.
(0.022727272727272728+0j)
(0.045454545454545456-0.022727272727272728j)
(0.06818181818181819+0.045454545454545456j)
(0.18181818181818182+0.02272727272727272j)
(0.045454545454545456+0.022727272727272728j)
(0.11363636363636365+0j)
(0.09090909090909091+0.06818181818181819j)
(0.25+0.04545454545454547j)
(0.06818181818181819-0.045454545454545456j)
(0.09090909090909091-0.06818181818181819j)
(0.29545454545454547+0j)
(0.40909090909090917-0.022727272727272735j)
(0.18181818181818182-0.02272727272727272j)
(0.25-0.04545454545454547j)
(0.40909090909090917+0.022727272727272735j)
(0.5681818181818182+0j)
5.3 Concurrence
Concurrence [96, 97] is a measure of entanglement defined for both pure and mixed states of
two-qubit. Consider a N -qubit quantum system and the quantum state is ρN . The pictorial
presentation of such a system is shown in Fig. (5.3). To calculate the entanglement content
between the ith and j th qubits of the quantum state, we first calculate the two-qubit reduced
density matrix ρij = T rij (ρN ). Thereafter using the reduced density matrix ρij , we calculate
150 Quantification of Quantum Entanglement
ρN
1 2 i i+1 j j+1 N
FIGURE 5.3: N -qubit quantum system, and the quantum state is denoted by ρN .
concurrence_vec(state,i,j,eps=10**(-13))
Parameters
[in] i, j It stores the place values of the qubits. Both of them can
store any integer value between 1 and N
[in] eps If the magnitude of any of the eigenvalues of ρij ρ̃ij is less
than eps, then that particular value will be considered equal
to zero. The default value is equal to 10−13
Implementation
def concurrence_vec(self,state,i,j,eps=10**(-13)):
"""
Calculation of concurrence for a quantum state
Parameters
Concurrence 151
Returns
conc: concurrence value
"""
sigmay=np.zeros([4,4],dtype='float64')
# Storing the datatype of the input state
typestate=str(state.dtype)
# If data type of input state is complex then IF cond. is true
if re.findall("^complex",typestate):
# Constructing σ y ⊗ σ y
sigmay[0,3]=-1
sigmay[1,2]=1
sigmay[2,1]=1
sigmay[3,0]=-1
sub_tr=[i,j]
# Constructing the two-qubit reduced density matrix ρij
rdm= self.partial_trace_vec(state,sub_tr)
# Calculating ρij ρ̃ij
rhot3=rdm@[email protected](rdm)@sigmay
# Diagonalizing ρij ρ̃ij
w,vl,vr,info =la.zgeev(rhot3)
# wc will store eigenvalues of ρij ρ̃ij
wc=[]
for i in range(0,4):
if abs(w.item(i))<eps:
wc.append(0.000000000000000)
else:
wc.append(abs(w.item(i)))
wc.sort(reverse=True)
wc=np.array(wc,dtype='float64')
√ √ √ √
# Calculating, λ1 − λ2 − λ3 − λ4
conc=math.sqrt(wc.item(0))-math.sqrt(wc.item(1))-\
math.sqrt(wc.item(2))-math.sqrt(wc.item(3))
if conc<0:
conc=0
# If data type of input state is real then ELSE cond. is true
else:
# Constructing σ y ⊗ σ y
sigmay[0,3]=-1
sigmay[1,2]=1
sigmay[2,1]=1
sigmay[3,0]=-1
sub_tr=[i,j]
# Constructing two-qubit reduced density matrix ρij
rdm= self.partial_trace_vec(state,sub_tr)
# Calculating ρij ρ̃ij
152 Quantification of Quantum Entanglement
rhot3=rdm@sigmay@rdm@sigmay
# Diagonalizing ρij ρ̃ij
wr,wi,vl,vr,info =la.dgeev(rhot3)
# w will be storing λi 's
w=[]
for i in range(0,4):
if wr[i] < eps:
w.append(0.000000000000000)
else:
w.append(np.float64(wr.item(i)))
w.sort(reverse=True)
w=np.array(w,dtype='float64')
√ √ √ √
# Finally calculating λ1 − λ2 − λ3 − λ4
conc=math.sqrt(w.item(0))-math.sqrt(w.item(1))-\
math.sqrt(w.item(2))-math.sqrt(w.item(3))
if conc<0:
conc=0.0
return(np.float64(conc))
Example
In this example, we are finding the value of concurrence C35 of a six-qubit real state |ψ2 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will be storing |ψ2 i
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
# Calculating the concurrence C35
conc=entobj.concurrence_vec(state2,3,5)
print(conc)
In this example, we are finding the value of concurrence C36 of the six-qubit complex state
|ψ3 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will be storing |ψ3 i
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
# Calculating the concurrence C36
conc=entobj.concurrence_vec(state2,3,6)
print(conc)
0.002929329915577223
concurrence_den(state,i,j,eps=10**(-13))
Parameters
[in] i, j It stores the place values of the qubits. Both of them can
store any integer value between 1 and N
[in] eps If the magnitude of any of the eigenvalues of ρij ρ̃ij is less
than eps, then that particular value will be considered equal
to zero. The default value is equal to 10−13
Implementation
def concurrence_den(self,state,i,j,eps=10**(-13)):
"""
Calculation of concurrence for a density matrix
Parameters
state : Real or complex density matrix
i : It stores the place values of the qubits.
j : It stores the place values of the qubits.
eps : Below the eps value the eigenvalues will be considered zero.
The default is 10**(-13).
Returns
conc: concurrence value
"""
sigmay=np.zeros([4,4],dtype='float64')
# Storing the data type of the input density matrix ρ
typestate=str(state.dtype)
# If ρ is complex then IF cond. is true
if re.findall("^complex",typestate):
# Constructing the σ y ⊗ σ y
sigmay[0,3]=-1
sigmay[1,2]=1
sigmay[2,1]=1
sigmay[3,0]=-1
154 Quantification of Quantum Entanglement
sub_tr=[i,j]
# Constructing the two-qubit reduced density matrix ρij
rdm= self.partial_trace_den(state,sub_tr)
# Constructing ρij ρ̃ij
rhot3=rdm@[email protected](rdm)@sigmay
w,vl,vr,info =la.zgeev(rhot3)
# wc stores the eigenvalues λi 's
wc=[]
for i in range(0,4):
if abs(w.item(i))<eps:
wc.append(0.000000000000000)
else:
wc.append(abs(w.item(i)))
wc.sort(reverse=True)
wc=np.array(wc,dtype='float64')
√ √ √ √
# Finally, λ1 − λ2 − λ3 − λ4
conc=math.sqrt(wc.item(0))-math.sqrt(wc.item(1))-\
math.sqrt(wc.item(2))-math.sqrt(wc.item(3))
if conc<0:
conc=0
# If ρ is real then ELSE cond. is true
else:
# Constructing σ y ⊗ σ y
sigmay[0,3]=-1
sigmay[1,2]=1
sigmay[2,1]=1
sigmay[3,0]=-1
sub_tr=[i,j]
# Constructing two-qubit reduced density matrix ρij
rdm= self.partial_trace_den(state,sub_tr)
# Constructing matrix ρij ρ̃ij
rhot3=rdm@sigmay@rdm@sigmay
# Diagonalizing ρij ρ̃ij
wr,wi,vl,vr,info =la.dgeev(rhot3)
# w will store the λi 's
w=[]
for i in range(0,4):
if wr[i] < eps:
w.append(0.000000000000000)
else:
w.append(np.float64(wr.item(i)))
w.sort(reverse=True)
w=np.array(w,dtype='float64')
√ √ √ √
# Finally, constructing λ1 − λ2 − λ3 − λ4
conc=math.sqrt(w.item(0))-math.sqrt(w.item(1))-\
math.sqrt(w.item(2))-math.sqrt(w.item(3))
if conc<0:
conc=0.0
return(np.float64(conc))
Concurrence 155
Example
In this example we are finding the value of the concurrence C36 of a six-qubit density matrix
|ψ2 ihψ2 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
Similarly, we also find the value of the concurrence C36 of the six-qubit density matrix
|ψ3 ihψ3 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 ihψ3 |
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_cvec(state2,state2)
# Finally calculating concurrence C36
conc=entobj.concurrence_den(state2,3,6)
print(conc)
In this final example, we calculate the value of concurrence C12 for the following mixed
state,
1 1
ρ= |ψ2 ihψ2 | + |ψ3 ihψ3 | (5.11)
2 2
state2=qobj.outer_product_rvec(state2,state2)
print(conc)
where ρA is the reduced density matrix √ of partition A. The example of finding the
I-concurrence for state (|00i + |11i) / 2 is given below,
import numpy as np
from QuantumInformation import PartialTr as PT
from QuantumInformation import GatesTools as GT
pobj=PT()
gtobj=GT()
√
# Created the state, (|00i + |11i) / 2
state=gtobj.bell3(2)
I_conc=np.sqrt(2*(1-np.trace(np.matmul(red_A,\
red_A))))
There exists another type of concurrence called the N-concurrence [99] which is defined as,
s
1− N
X
CN = 2 2 2N − 2 − T r(ρ2i ). (5.13)
i
√
The example of finding the N-concurrence for state (|00i + |11i) / 2 is given below,
import numpy as np
from QuantumInformation import PartialTr as PT
from QuantumInformation import GatesTools as GT
pobj=PT()
gtobj=GT()
N=2 √
# Created the state, (|00i + |11i) / 2
state=gtobj.bell3(N)
sumtr=0.0
# Calculating the value, i T r(ρi )
2
P
for i in range(1,N+1):
red_i = pobj.partial_trace_vec(state,[i])
sumtr = sumtr+np.trace(np.matmul(red_i,red_i))
N p
# Finally, calculating the CN = 21− 2 2N − 2 − i T r(ρ2i ).
P
N_conc = 2**(1-(N/2))*np.sqrt(2**N-2-sumtr)
where ρA and ρB are the reduced density matrices of the subsystems A and B, respectively.
Eq. [5.14] holds true because of Schmidt decomposition [101], which states that non-zero
eigenvalues of the reduced density matrix ρA are equal to non-zero eigenvalues of reduced
density matrix ρB , if the composite state of the bipartite split is a pure state. Eq. [5.14] can
also be written as,
X2
S ρAB = − λi log2 (λi ) , (5.15)
i=1
158 Quantification of Quantum Entanglement
where λi ’s are the eigenvalues of the reduced density matrix ρB (or ρA ), and we define
0 log2 0 ≡ 0. The entanglement content of a pure state is expressed in terms of the reduced
density matrices of the subsystems. The loss of information or ignorance in the state of the
subsystem even though the whole system may be completely known is quantified in terms
of entropy.
For a multi-qubit pure state, the entanglement between two blocks of spins is calculated
by block entropy [102]. In Fig. (5.4), we have N spins of a multi-qubit pure state and we
calculate the entanglement between L (L < N ) and N − L block of spins. The block entropy
gives the entanglement between these two blocks of spins as follows,
dL
X
S(ρL ) = − λi log2 λi . (5.16)
i=1
where λi are the eigenvalues of the reduced density matrix ρL = trL+1,...,N (ρ), and dL is
the Hilbert space dimension of the reduced density matrix ρL . Note that Eq. [5.16] is a
concave function. Now we will find the upper bound of the block entropy, and to this end,
we rewrite the Eq. [5.16] as follows by absorbing the negative sign inside,
dL
X 1
S(ρL ) = λi log2 . (5.17)
i=1
λi
By using the definition of concave function, the above equation transform into the following
ρL ρN −L
inequality,
dL dL
!
X 1 X 1
S(ρL ) = λi log2 ≤ log2 λi , (5.18)
i=1
λ i i=1
λi
S(ρL ) ≤ log2 (dL ). (5.19)
The equality in Eq. [5.19] holds true only in the case when all λi ’s are equal to 1/dL , and
it is only possible when the state is a maximally mixed state, I/dL . The upper and lower
bound of block entropy can be simply written as,
S(ρL ) is equal to zero when ρL is a pure state (where all the λi ’s are zero except any one
of the λi value which is equal to 1).
block_entropy_vec(state,sub_tr,eps=10**(-13))
Block entropy 159
Parameters
Implementation
def block_entropy_vec(self,state,sub_tr,eps=10**(-13)):
"""
Calculation of block entropy for a quantum state
Parameters
state : Real or complex state
sub_tr: List of numbers designating the particular subsystems
not to be traced out.
eps : Below the eps value the eigenvalues will be considered zero.
The default is 10**(-13).
Returns
Bent: Block entropy value
"""
# Storing the data type of the state |ψi
typestate=str(state.dtype)
# Calculating the reduced density matrix ρL
rdm= self.partial_trace_vec(state,sub_tr)
# If |ψi is complex then IF cond. is true
if re.findall("^complex",typestate):
# Diagonalizing ρL
w,v,info=la.zheev(rdm)
# If |ψi is real then ELSE cond. is true
else:
# Diagonalizing ρL
w,v,info=la.dsyev(rdm)
wlen=len(w)
Bent=0.0
PdL
# Finally calculating S(ρL ) = − i=1 λi log2 λi
for x in range(0,wlen):
if abs(w.item(x))<eps:
w[x]=0.000000000000000
else:
160 Quantification of Quantum Entanglement
Example
In this example, we are finding the value of the block entropy between the blocks 135 and
246 of the six-qubit real state |ψ2 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will be storing |ψ2 i
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
sub_tr=[1,3,5]
# Calculating block entropy S(ρ135 )
bent=entobj.block_entropy_vec(state2,sub_tr)
print(bent)
Similarly, we find the value of block entropy between the blocks 135 and 246 of the six-qubit
complex state |ψ3 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will be storing |ψ3 i
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
sub_tr=[1,3,5]
# Calculating block entropy S(ρ135 )
bent=entobj.block_entropy_vec(state2,sub_tr)
print(bent)
block_entropy_den(state,sub_tr,eps=10**(-13))
Block entropy 161
Parameters
Implementation
def block_entropy_den(self,state,sub_tr,eps=10**(-13)):
"""
Calculation of block entropy for a density matrix
Parameters
state : Real or complex density matrix
sub_tr: List of numbers designating the particular subsystems
not to be traced out.
eps : Below the eps value the eigenvalues will be considered zero.
The default is 10**(-13).
Returns
Bent: Block entropy value
"""
# Storing the data type of input density matrix ρ
typestate=str(state.dtype)
# Calculating reduced density matrix ρL
rdm= self.partial_trace_den(state,sub_tr)
# If ρ is complex then IF cond. is true
if re.findall("^complex",typestate):
# Diagonalizing ρL
w,v,info=la.zheev(rdm)
# If ρ is real then ELSE cond. is true
else:
# Diagonalizing ρL
w,v,info=la.dsyev(rdm)
wlen=len(w)
Bent=0.0
PdL
# Finally calculating S(ρL ) = − i=1 λi log2 λi
for x in range(0,wlen):
if abs(w.item(x))<eps:
162 Quantification of Quantum Entanglement
w[x]=0.000000000000000
else:
assert w.item(x) > 0.0,\
"The density matrix entered is not correct as the eigenvalues
are negative"
Bent=Bent-(w.item(x)*math.log(w.item(x),2))
return(Bent)
Example
In this example, we are finding the value of block entropy between the blocks 135 and 246
of the six-qubit real density matrix |ψ2 ihψ2 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will be storing |ψ2 ihψ2 |
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_rvec(state2,state2)
sub_tr=[2,4,6]
# Calculating block entropy S(ρ246 )
bent=entobj.block_entropy_den(state2,sub_tr)
print(bent)
Similarly, we also find the value of block entropy between the blocks 135 and 246 of the
six-qubit density matrix |ψ3 ihψ3 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will be storing |ψ3 ihψ3 |
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_cvec(state2,state2)
sub_tr=[2,4,6]
# Calculating block entropy S(ρ246 )
bent=entobj.block_entropy_den(state2,sub_tr)
print(bent)
renyi_entropy(rho,alpha)
Parameters
Implementation
def renyi_entropy(self,rho,alpha):
"""
Calculation of Renyi entropy
Parameters
rho : Real or complex density matrix
alpha : It is the value of Renyi index
Returns
renyi : Renyi Entropy value
"""
# Checking whether α =
6 1
assert alpha != 1.0, "alpha should not be equal to 1"
# Storing the data type of input density matrix ρ
typerho=str(rho.dtype)
# Creating the object of the LinearAlgebra class
laobj=LA()
164 Quantification of Quantum Entanglement
Example
In this example, we are finding the value of Renyi entropy of a two qubit generalized Werner
state ρ(0.5).
from QuantumInformation import GatesTools as GT
gtobj=GT()
# Constructing two-qubit Werner state with mixing probability p = 0.5
state=gtobj.nWerner(0.5)
# Finally calculating Renyi entropy
renyi=entobj.renyi_entropy(state,6.0)
print(renyi)
Similarly, we calculate the value of Renyi entropy of the density matrix ρ1 ⊗ ρ2 , where
density matrices ρ1 and ρ2 are already defined in Eq. [4.8] and Eq. [4.9], respectively.
# Constructing the density matrix ρ1 ⊗ ρ2
rho=np.zeros([4,4],dtype=np.complex_)
rho[0,0]=complex(0.16,0.0)
rho[0,1]=complex(-0.04,-0.16)
rho[0,2]=complex(-0.08,-0.04)
rho[0,3]=complex(-0.02,0.09)
rho[1,0]=complex(-0.04,0.16)
rho[1,1]=complex(0.24,0.0)
rho[1,2]=complex(0.06,-0.07)
rho[1,3]=complex(-0.12,-0.06)
rho[2,0]=complex(-0.08,0.04)
rho[2,1]=complex(0.06,0.07)
rho[2,2]=complex(0.24,0.0)
rho[2,3]=complex(-0.06,-0.24)
rho[3,0]=complex(-0.02,-0.09)
rho[3,1]=complex(-0.12,0.06)
rho[3,2]=complex(-0.06,0.24)
rho[3,3]=complex(0.36,0.0)
# Finally calculating Renyi entropy
renyi=entobj.renyi_entropy(rho,100.0)
print(renyi)
Negativity and logarithmic negativity 165
The mathematical construction of Eq. [5.22] is such that for separable states, the negativity
value goes to zero, and in the case of maximally entangled states, its value is equal to
entanglement entropy. It measures the degree to which ρTA fails to be positive, and therefore
it can be regarded as a quantitative version of Peres’ criterion for separability (PPT criteria).
Associated with negativity, we also define what is called the logarithmic negativity [108]
which is EN (ρ) = log2 (k ρTA k), as expected of a log function, logarithmic negativity has
additive properties. Though logarithmic negativity is not a convex function but its value
never increases on average under any general positive partial transpose preserving operation.
negativity_log_vec(state,sub_tr,eps=10**(-13))
Parameters
[in] eps If the magnitude of any of the eigenvalues of ρTA is less than
eps, then that particular value will be considered equal to
zero. The default value is equal to 10−13
[out] negv, lognegv Returns the value of negativity and logarithmic negativity
166 Quantification of Quantum Entanglement
Implementation
def negativity_log_vec(self,state,sub_tr,eps=10**(-13)):
"""
Calculation of negativity and logarithmic negativity for a quantum
state
Parameters
state : Real or complex state
sub_tr: List of numbers designating the particular subsystems
to be transposed.
eps : Below the eps value the eigenvalues will be considered zero.
The default is 10**(-13).
Returns
negv,lognegv : negativity and log negativity values, respectively
"""
# Constructing the object of the LinearAlgebra class
laobj=LA()
# Storing the data type of the input state, |ψi
typestate=str(state.dtype)
# Calculated partially transposed matrix, ρTA
rhoa=self.ptranspose_vec(state,sub_tr)
# If |ψi is complex then IF cond. is true
if re.findall("^complex",typestate):
k ρTA k −1
# Calculating, N (ρ) =
2
negv=laobj.trace_norm_cmatrix(rhoa,precision=eps)
# If |ψi is real then ELSE cond. is true
else:
k ρTA k −1
# Calculating, N (ρ) =
2
negv=laobj.trace_norm_rmatrix(rhoa,precision=eps)
# The value of N (ρ) should be greater than 0.
assert negv > 0.0,\
"The density matrix entered is not correct as the negativity is
negative"
# Finally calculating, EN (ρ) = log2 (k ρTA k)
lognegv=math.log2(negv)
negv=(negv-1)/2
return(negv,lognegv)
Example
In this example, we are finding the value of negativity and logarithmic negativity between
the blocks 135 and 246 of the six-qubit real state |ψ2 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 i
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
Negativity and logarithmic negativity 167
state2[i]=i+1
state2=qobj.normalization_vec(state2)
# Designating the systems that are to be transposed
sub_tr=[1,3,5]
# Calculating the negativity and logarithmic negativity
negv,logegv=entobj.negativity_log_vec(state2,sub_tr)
print(negv,logegv)
The preceding code prints the required values of negativity and logarithmic negativity.
0.0976744186046512 0.2574316996044179
Similarly, we calculate the value of negativity and logarithmic negativity between the blocks
135 and 246 of the six-qubit complex state |ψ3 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 i
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
# Designating the systems that are to be transposed
sub_tr=[1,3,5]
# Calculating the negativity and logarithmic negativity
negv,lognegv=entobj.negativity_log_vec(state2,sub_tr)
print(negv,lognegv)
The preceding code prints the required values of negativity and logarithmic negativity.
0.09996338337605282 0.26294635877747374
negativity_log_den(den,sub_tr,eps=10**(-13))
Parameters
[in] eps If the magnitude of any of the eigenvalues of ρTA is less than
eps, then that particular value will be considered equal to
zero. The default value is equal to 10−13
[out] negv, lognegv Returns the value of negativity and logarithmic negativity
168 Quantification of Quantum Entanglement
Implementation
def negativity_log_den(self,den,sub_tr,eps=10**(-13)):
"""
Calculation of negativity and logarithmic negativity for a density
matrix
Parameters
state : Real or complex density matrix
sub_tr: List of numbers designating the particular subsystems
to be transposed.
eps : Below the eps value the eigenvalues will be considered zero.
The default is 10**(-13).
Returns
negv,lognegv : negativity and log negativity values, respectively
"""
# Constructing the object of the LinearAlgebra class
laobj=LA()
# Storing the data type of the input state, ρ
typestate=str(den.dtype)
# Calculated partially transposed matrix, ρTA
rhoa=self.ptranspose_den(den,sub_tr)
# If ρ is complex then IF cond. is true
if re.findall("^complex",typestate):
k ρTA k −1
# Calculating, N (ρ) =
2
negv=laobj.trace_norm_cmatrix(rhoa,precision=eps)
# If ρ is real then ELSE cond. is true
else:
k ρTA k −1
# Calculating, N (ρ) =
2
negv=laobj.trace_norm_rmatrix(rhoa,precision=eps)
# The value of N (ρ) should be greater than 0.
assert negv > 0.0,\
"The density matrix entered is not correct as the negativity is
negative"
# Finally calculating, EN (ρ) = log2 (k ρTA k)
lognegv=math.log2(negv)
negv=(negv-1)/2
return(negv,lognegv)
Example
In this example, we are finding the value of negativity and logarithmic negativity between
the block 135 and 246 of the six-qubit state |ψ2 ihψ2 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 i
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
Negativity and logarithmic negativity 169
state2[i]=i+1
state2=qobj.normalization_vec(state2)
# Constructing the density matrix |ψ2 ihψ2 |
state2=qobj.outer_product_rvec(state2,state2)\
# Designating the subsystems that are to be transposed
sub_tr=[2,4,6]
# Calculating negativity and logarithmic negativity
negv, lognegv=entobj.negativity_log_den(state2,sub_tr)
print(negv, lognegv)
The preceding code prints the required values of negativity and logarithmic negativity.
0.0976744186046512 0.2574316996044179
Similarly, we calculate the value of negativity and logarithmic negativity between the blocks
135 and 246 of the six-qubit state |ψ3 ihψ3 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 i
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
# Constructing the density matrix |ψ3 ihψ3 |
state2=qobj.outer_product_cvec(state2,state2)
# Designating the subsystems that are to be transposed
sub_tr=[2,4,6]
# Calculating negativity and logarithmic negativity
negv, lognegv=entobj.negativity_log_den(state2,sub_tr)
print(negv, lognegv)
The preceding code prints the required values of negativity and logarithmic negativity.
0.09996338337605282 0.26294635877747374
In the final example, we construct the following mixed state density matrix,
1 1
ρ= |ψ2 ihψ2 | + |ψ3 ihψ3 |, (5.23)
2 2
and we calculate the negativity and log-negativity values between the block 123 and 456.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
for i in range(0,state3.shape[0]):
state3[i]=complex(i,i+1)
state3=qobj.normalization_vec(state3)
state3=qobj.outer_product_cvec(state3,state3)
print(negv, log_negv)
The preceding code prints the required values of negativity and logarithmic negativity.
0.030392688074903806 0.08513279189272352
where ρi is a single qubit density matrix got by tracing all the other qubits except the ith
qubit, and N is the total number of qubits. From the above expression it is clear that the
Q measure is the average subsystem linear entropy of the constituent qubits. The bound on
Q is such that, 0 ≤ Q ≤ 1.
QMeasure_vec(state)
Parameters
Implementation
def QMeasure_vec(self,state):
"""
Calculation of Q measure for a quantum state
Parameters
state : Real or complex state
Returns
Qmeas: Q measure value
"""
# NN will store the total number of qubits
NN=math.log2(state.shape[0])/math.log2(2)
NN=int(NN)
sub_tr=np.zeros([NN,1])
sum3=0.0
PN
# FOR loop will store the value, i=1 T r(ρi )
2
for x in range(0,NN):
sub_tr=[]
sub_tr.append(x+1)
rho=self.partial_trace_vec(state,sub_tr)
rho=np.matmul(rho,rho)
tr2=np.trace(rho)
sum3=sum3+tr2
PN
# Finally calculating, 2 1 − N1 i=1 T r(ρ2i )
Qmeas=2*(1-(sum3/NN))
return abs(Qmeas)
Example
In this example, we are finding the value of Q measure of the six-qubit real state |ψ2 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 i
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
# Calculating Q measure
Qmeas=entobj.QMeasure_vec(state2)
print(Qmeas)
Similarly, we find the value of Q measure of the six-qubit complex state |ψ3 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
172 Quantification of Quantum Entanglement
QMeasure_den(den)
Parameters
[in] den It is a real or complex array of dimension (0:2N −1, 0:2N −1)
which is the input density matrix
Implementation
def QMeasure_den(self,den):
"""
Calculation of Q measure for a density matrix
Parameters
den : Real or complex density matrix
Returns
Qmeas: Q measure value
"""
# NN stores total number of qubits
NN=math.log2(den.shape[0])/math.log2(2)
NN=int(NN)
sub_tr=np.zeros([NN,1])
sum3=0.0
PN
# FOR loop will store the value, i=1 T r(ρi )
2
for x in range(0,NN):
sub_tr=[]
sub_tr.append(x+1)
rho=self.partial_trace_den(den,sub_tr)
rho=np.matmul(rho,rho)
tr2=np.trace(rho)
Entanglement spectrum 173
sum3=sum3+tr2
PN
# Finally calculating, 2 1 − 1
N i=1 T r(ρ 2
i )
Qmeas=2*(1-(sum3/NN))
return abs(Qmeas)
Example
In this example, we are finding the value of Q measure of the six-qubit real density matrix
|ψ2 ihψ2 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 ihψ2 |
state2=np.zeros([2**6],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_rvec(state2,state2)
# Calculating the Q measure
Qmeas=entobj.QMeasure_den(state2)
print(Qmeas)
Similarly, we calculate the value of Q measure of the six-qubit complex density matrix
|ψ3 ihψ3 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 ihψ3 |
state2=np.zeros([2**6],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_cvec(state2,state2)
# Calculating the Q measure
Qmeas=entobj.QMeasure_den(state2)
print(Qmeas)
for quantifying entanglement, Haldane in his seminal work pointed out that the spectrum
of the reduced density matrix contains more information regarding the entanglement. This
quantity is more important when we study condensed matter systems like topological mate-
rials, quantum Hall effect, etc. If we have a reduced density matrix of a composite sys-
tem AB as ρA , then the entanglement spectrum is defined as −ln(λi ), where λi with
i = 1, · · · , dim(ρA ) are the eigenvalues of ρA .
entanglement_spectrum(rho)
Parameters
Implementation
def entanglement_spectrum(self,rho):
"""
Calculation of entanglement spectrum of a density matrix
Parameters
rho : Real or complex density matrix
Returns
eigenvalues : List containing the eigenvalues of rho
logeigenvalues : List containing the negative logarithmic
eigenvalues of rho
"""
# Storing the data type of input state |ψi
typerho=str(rho.dtype)
# If |ψi is complex then IF cond. is true
if re.findall('^complex',typerho):
# Calculating eigenvalues λi of |ψi
eigenvalues,eigenvectors,info=la.zheev(rho)
# If |ψi is real then ELSE cond. is true
else:
eigenvalues,eigenvectors,info=la.dsyev(rho)
# Calculating ln(λi )
logeigenvalues=np.zeros([eigenvalues.shape[0]],dtype='float64')
for i in range(0,eigenvalues.shape[0]):
assert eigenvalues[i]>0.0,\
Entanglement spectrum 175
Example
In this example, we are finding entanglement spectrum of a two qubit generalized Werner
state ρ(0.5).
from QuantumInformation import GatesTools as GT
gtobj=GT()
# Constructing the Werner state with mixing probability p = 0.5
state=gtobj.nWerner(0.5)
eigenvalues,logeigenvalues=entobj.entanglement_spectrum(state)
print(eigenvalues,logeigenvalues)
The preceding code prints the required eigenvalues and the entanglement spectrum as shown
below.
[0.125 0.125 0.125 0.625] [2.07944154 2.07944154 2.07944154 0.47000363]
The preceding code prints the required eigenvalues and the entanglement spectrum as shown
below.
[0.01931653 0.0564194 0.2357345 0.68852957] [3.94679423 2.87494213
1.44504911 0.37319701]
176 Quantification of Quantum Entanglement
τ3 = 4 det(ρ) − C12
2 2
− C13 . (5.26)
From the structure of the GHZ and W state, we see the contrasting behaviour of bipartite
entanglement in them, this three tangle will be useful in differentiating these entanglement
properties for such three qubit pure states.
residual_entanglement_vec(state)
Parameters
Implementation
def residual_entanglement_vec(self,state):
"""
Calculation of residual entanglement for a three-qubit quantum state
Parameters
state : Real or complex 3-qubit state
Returns
res_tang : Residual entanglement value
"""
# Checking whether entered state is a 3 qubit system
assert state.shape[0]==8,"It is not a three qubit quantum system"
# Calculating det(ρ)
Residual entanglement for three qubits 177
det=np.linalg.det(self.partial_trace_vec(state,[1]))
# Calculating 4 det(ρ)
det=4*det
# Finally calculating, τ3 = 4 det(ρ) − C12
2 2
− C13
res_tang=det-(self.concurrence_vec(state,1,2)**2)-\
(self.concurrence_vec(state,1,3)**2)
res_tang=abs(res_tang)
return res_tang
Example
In this example, we are finding the value of the residual entanglement of the three-qubit
real state |ψ2 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 i
state2=np.zeros([2**3],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
# Calculating τ3
res_tang=entobj.residual_entanglement_vec(state2)
print(res_tang)
The preceding code prints the required value of the residual entanglement as,
1.8041124150158794e-16
Similarly, we calculate the value of the residual entanglement of the three-qubit complex
state |ψ3 i.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 i
state2=np.zeros([2**3],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
# Calculating τ3
res_tang=entobj.residual_entanglement_vec(state2)
print(res_tang)
The preceding code prints the required value of the residual entanglement as,
2.970954353025952e-17
residual_entanglement_den(den)
178 Quantification of Quantum Entanglement
Parameters
Implementation
def residual_entanglement_den(self,den):
"""
Calculation of residual entanglement for a three-qubit density matrix
Parameters
den : Real or complex 3-qubit density matrix
Returns
res_tang : Residual entanglement value
"""
# Checking whether entered state is a 3-qubit system
assert den.shape[0]==8,"It is not a three qubit quantum system"
# Calculating det(ρ)
det=np.linalg.det(self.partial_trace_den(den,[1]))
# Calculating 4 det(ρ)
det=4*det
# Finally calculating, τ3 = 4 det(ρ) − C12
2 2
− C13
res_tang=det-(self.concurrence_den(den,1,2)**2)-\
(self.concurrence_den(den,1,3)**2)
res_tang=abs(res_tang)
return res_tang
Example
In this example, we are finding the value of the residual entanglement of the three-qubit
real density matrix |ψ2 ihψ2 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ2 ihψ2 |
state2=np.zeros([2**3],dtype='float64')
for i in range(0,state2.shape[0]):
state2[i]=i+1
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_rvec(state2,state2)
# Calculating τ3
res_tang=entobj.residual_entanglement_den(state2)
print(res_tang)
The preceding code prints the required value of the residual entanglement as,
PPT criteria or the Peres positive partial transpose criteria 179
1.8041124150158794e-16
Similarly, we calculate the value of the residual entanglement of the three-qubit complex
density matrix |ψ3 ihψ3 |.
from QuantumInformation import QuantumMechanics as QM
qobj=QM()
# state2 will store |ψ3 ihψ3 |
state2=np.zeros([2**3],dtype=np.complex_)
for i in range(0,state2.shape[0]):
state2[i]=complex(i,i+1)
state2=qobj.normalization_vec(state2)
state2=qobj.outer_product_cvec(state2,state2)
# Calculating τ3
res_tang=entobj.residual_entanglement_den(state2)
print(res_tang)
The preceding code prints the required value of the residual entanglement as,
2.970954353025952e-17
pobj=PT()
gtobj=GT()
ptrans_state=pobj.ptranspose_den(state,sub_tr)
This criteria [114] is a set of two inequalities as shown above where ρA and ρB are the
reduced density matrices of the subsystems A and B, respectively. In the recipes below,
we give the separability criteria for only 2 × 2 systems. In this example, we will detect
entanglement in two-qubit generalized Werner state ρ(0.5).
from QuantumInformation import PartialTr as PT
from QuantumInformation import QuantumMechanics as QM
from QuantumInformation import GatesTools as GT
import scipy.linalg.lapack as la
import numpy as np
pobj=PT()
qobj=QM()
gtobj=GT()
identity[1,1]=1.0
# Constructing ρA ⊗ I
mat=qobj.tensor_product_matrix(rdm,identity)
# Constructing ρA ⊗ I − ρ(0.5)
final_mat=mat-werner_state
# Calculating eigenvalues of ρA ⊗ I − ρ(0.5)
eigenvalues, eigenvectors, info = la.dsyev(final_mat)
for i in range(0,len(eigenvalues)):
if eigenvalues[i] < 0:
status = 'Not separable'
break
print(f"The state is {status}")
Next, we calculate the entanglement content between qubits 1 and 6, using the concurrence
value C16 . Finally, we perform partial transpose operation on the subsystem B, which con-
tains the qubits 2, 3 and 5 of the state ρ123456 , and determine whether the subsystem B is
separable from subsystem A (A subsystem contains the qubits 1, 4 and 6) or not.
pobj=PT()
gtobj=GT()
entobj=ENT()
qobj=QM()
182 Quantification of Quantum Entanglement
sub_tr=[2,3,6]
ptran_rho=pobj.ptranspose_den(mix_state,[2,3,5])
eigenvalues, eigenvectors,info=la.zheev(ptran_rho)
In this chapter we will give the numerical recipes for constructing the spin half Fermionic
Hamiltonians in one dimension [115–118], basically a lattice of contiguous electrons along a
line. Each electron at a given point on the one-dimensional lattice represents one qubit (here
qubits are synonymous with spins) of information. Spin Hamiltonians or what is popularly
called the spin chain models are prototypical in understanding the origin of magnetism and
magnetic phases [119–121]. Not only that these spin chains at a temperature of zero Kelvin
can be used to study and understand quantum phase transitions [80, 83], which unlike ther-
mal phase transitions do not involve any thermal parameter. These Hamiltonians also prove
as a fertile ground for testing various quantum information tasks not only theoretically but
also experimentally [122–124]. We have given the numerical recipes for all possible inter-
action Hamiltonian’s, and as we progress through the chapter, we will give some examples
on how to construct some famous spin chains using our numerical recipes for the sake of
illustration.
To understand further, we consider the simple but the well known Heisenberg spin
chain model, a boon for people working in the area of magnetism. Using the Heisenberg
model, the magnetic properties of many insulating crystals can be explained [125, 126]. The
Hamiltonian of the Heisenberg model can be written as,
N
X
Ĥ = −J ~σi .~σi+1 , (6.1)
i=1
where J is the coupling constant and the periodic boundary condition is in effect, that
is ~σN +1 ≡ ~σ1 . Every spin is represented by a quantum operator acting upon the tensor
product (C2 )⊗N as each site is associated with a complex two-dimensional Hilbert space.
To make things more clear, let us take a 3 spin Heisenberg model, we can have two differ-
ent types of boundary conditions, one is the periodic boundary condition (PBC) in which
there is a wrapping up of the spins end-to-end and the other which is the open boundary
condition (OBC) where there is no wrapping up and end spins are left free. Let us write
the Hamiltonian for the model given in Eq. [6.1] as below, considering periodic boundary
conditions with end to end wrap up of the form σN +1 ≡ σ1
3
X
H = −J ~σi .~σi+1 (6.2)
i=1
= −J( ~σ1 .~σ2 + ~σ2 .~σ3 + ~σ3 .~σ1 ), (6.3)
| {z }
wrap up
= −J(σ1x σ2x + σ1y σ2y + σ1z σ2z + σ2x σ3x + σ2y σ3y + σ2z σ3z
σ3x σ1x + σ3y σ1y + σ3z σ1z ) (6.4)
Now for a model with open boundary conditions where there is no such condition of end to
end wrap up like ~σN +1 ≡ ~σ1 , we can write
H = −J( ~σ1 .~σ2 + ~σ2 .~σ3 ) (6.5)
= −J(σ1x σ2x + σ1y σ2y + σ1z σ2z + σ2x σ3x + σ2y σ3y + σ2z σ3z ) (6.6)
However, these are the PBCs and OBCs for nearest-neighbour interaction, if we have r
nearest neighbours interacting, there will be r such end to end wrap up terms of the form
σN +r ≡ σr for r = 1, 2, . . .. It is also important to note that σiz = I⊗i−1 ⊗ σ z ⊗ I⊗N −i ,
where I is the 2 × 2 identity matrix. This is so because, the total Hamiltonian for N spins
will be a 2N × 2N matrix in the (C2 )⊗N – dimensional Hilbert space. All this implies that
the Pauli operator z with a subscript i which is σiz will only act on the qubit at site i. At
each site i, there is a quantum mechanical spin half particle either pointing along “up (| ↑i)”
or “down (| ↓i)” which are respectively denoted by |0i and |1i. Having the above discussed
facts in mind, we can proceed further. All the recipes given below are for both PBC’s and
OBC’s which can be chosen as a user defined variable while execution. The user also can
choose till how many neighbours he or she needs to build the spin chain. Also note that in
this chapter we are using σ matrices instead of the naive spin matrix S, bearing in mind
the relationship between them as Si = 12 σi (with ~ = 1), with i = x, y, z.
Note that for all Hamiltonians from Section (6.1) to Section (6.3), the output of an
example contains only the integer part of the matrix elements; this is done to accommodate
the output in the given space. However, for the Section (6.4) which is the DM interaction,
only integer part of the imaginary elements are displayed for the same reason. In this
chapter, all examples are given for three spins. In the codes given in this chapter we have
represented the interaction parameters like magnetic field, interaction strength (direct and
anti symmetric) by an array of dimension equal to number of spins in the system.
In the codes given for the interaction of the spins with magnetic field, we have considered
B to be an array of dimension equal to the number of spins in the system. This also gives
us the freedom to choose an inhomogenenous magnetic field at different sites, meaning, the
ith entry of this array will be the magnitude of the applied magnetic field at the ith site.
If the magnetic field is homogeneous, all entries of the array B will be same. Similarly, for
spin-spin interactions, we have chosen the interaction parameter to be an array of dimension
equal to the number of spins in the system. The ith entry of this array will be the interaction
strength between the ith and (i + r)th spin. If the interaction is homogeneous, all the entries
of the corresponding interaction parameter will be same. However, if random interactions
are present between the spins, the array will be filled with different numbers (drawn from
any probability distribution) corresponding to each pair of spins. You can understand more
about the usage as you see the examples given as we evolve through the chapter.
To import the class QuantumMechanics from the QuantumInformation library,
and creating the object of the class in your Python code can be done as follows,
# Importing the class Hamiltonian from the QuantumInformation library
from QuantumInformation import Hamiltonian as ham
where N is the total number of qubits present in the system and σ j are the Pauli spin
matrices, depending on which direction, the magnetic field is oriented, j can be accordingly
labeled as x, y, z as follows. Note that Bi is inside summation to accommodate a general
inhomogeneous magnetic field.
field_xham(N,mode='homogeneous',B=1)
Parameters
Implementation
def field_xham(self,N,mode='homogeneous',B=1):
"""
Constructs Hamiltonian of external magnetic field in X direction
Input
N: number of spins
mode: 'homogeneous' or 'inhomogeneous' magnetic field
B: it list of value if mode='inhomogeneous', and constant if
mode='homogeneous'
Output
186 One-Dimensional Quantum Spin-1/2 Chain Models
xham: Hamiltonian
"""
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
# Entering the homogeneous mode
if mode == 'homogeneous':
PN
# It will store i=1 Bσi
x
xham=np.zeros([2**N,2**N],dtype='float64')
PN
# Row index of i=1 Bσi
x
for i in range(0,2**N,1):
PN
# Column index of i=1 Bσi
x
for j in range(0,2**N,1):
# Decimal to binary decision
bvec=qobj.decimal_binary(j,N)
sum1=0.0
for k in range(0,N):
bb=np.copy(bvec)
# Matrix operation, σ z |ai = |1 − ai, where a = {0, 1}
bb[k]=1-bb[k]
row=qobj.binary_decimal(bb)
if row == i:
sum1=sum1+B
xham[i,j]=sum1
else:
# Checking whether there are N values of Bi
assert len(B)==N,\
"The entered values of magnetic strengths are not equal to number
of spins"
PN
# It will store i=1 Bi σi
x
xham=np.zeros([2**N,2**N],dtype='float64')
for i in range(0,2**N,1):
PN
# Row index of x
i=1 Bi σi
for j in range(0,2**N,1):
PN
# Column index of x
i=1 Bi σi
bvec=qobj.decimal_binary(j,N)
sum1=0.0
for k in range(0,N):
bb=np.copy(bvec)
# Matrix operation, σ z |ai = |1 − ai, where a = {0, 1}
bb[k]=1-bb[k]
row=qobj.binary_decimal(bb)
if row == i:
sum1=sum1+B[k]
xham[i,j]=sum1
return xham
Hamiltonian of spins interacting with an external magnetic field 187
Example
Here, we are finding the Hamiltonian of spins interacting with homogeneous magnetic field
in X direction.
hamx=hamobj.field_xham(3)
print(hamx)
The preceding code prints the matrix elements of the required Hamiltonian.
[[0. 1. 1. 0. 1. 0. 0. 0.]
[1. 0. 0. 1. 0. 1. 0. 0.]
[1. 0. 0. 1. 0. 0. 1. 0.]
[0. 1. 1. 0. 0. 0. 0. 1.]
[1. 0. 0. 0. 0. 1. 1. 0.]
[0. 1. 0. 0. 1. 0. 0. 1.]
[0. 0. 1. 0. 1. 0. 0. 1.]
[0. 0. 0. 1. 0. 1. 1. 0.]]
field_yham(N,mode='homogeneous',B=1)
Parameters
Implementation
def field_yham(self,N,mode='homogeneous',B=1):
"""
Constructs Hamiltonian of external magnetic field in Y direction
Input
N: number of spins
mode: 'homogeneous' or 'inhomogeneous' magnetic field
B: it list of value if mode='inhomogeneous', and constant if
mode='homogeneous'
Output
yham: Hamiltonian
"""
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
if mode == 'homogeneous':
PN y
# It will store, i=1 Bσi
yham=np.zeros([2**N,2**N],dtype=np.complex_)
PN y
# Row index of i=1 Bσi
for i in range(0,2**N,1):
PN y
# Column index of i=1 Bσi
for j in range(0,2**N,1):
# Decimal to binary conversions
bvec=qobj.decimal_binary(j,N)
sum1=0.0
for k in range(0,N):
bb=np.copy(bvec)
# Matrix operation, σ y |ai = (−ι̇)a |1 − ai, where a = {0, 1}
bb[k]=1-bb[k]
row=qobj.binary_decimal(bb)
if row == i:
sum1=sum1+(B*complex(0,1)*((-1)**bvec[k]))
yham[i,j]=sum1
else:
# Checking whether there are N values of Bi
assert len(B)==N,\
"The entered values of magnetic strengths are not equal to number
of spins"
PN y
# It will store, i=1 Bi σi
yham=np.zeros([2**N,2**N],dtype=np.complex_)
for i in range(0,2**N,1):
PN y
# Row index, i=1 Bi σi
for j in range(0,2**N,1):
PN y
# Column index, i=1 Bi σi
bvec=qobj.decimal_binary(j,N)
sum1=0.0
for k in range(0,N):
Hamiltonian of spins interacting with an external magnetic field 189
bb=np.copy(bvec)
# Matrix operation, σ y |ai = (−ι̇)a |1 − ai, where a = {0, 1}
bb[k]=1-bb[k]
row=qobj.binary_decimal(bb)
if row == i:
sum1=sum1+(B[k]*complex(0,1)*((-1)**bvec[k]))
yham[i,j]=sum1
return yham
Example
Here, we are finding the Hamiltonian of spins interacting with homogeneous magnetic field
in Y direction.
hamy=hamobj.field_yham(3)
print(hamy)
The preceding code prints the matrix elements of the required Hamiltonian.
[[0.+0.j 0.-1.j 0.-1.j 0.+0.j 0.-1.j 0.+0.j 0.+0.j 0.+0.j]
[0.+1.j 0.+0.j 0.+0.j 0.-1.j 0.+0.j 0.-1.j 0.+0.j 0.+0.j]
[0.+1.j 0.+0.j 0.+0.j 0.-1.j 0.+0.j 0.+0.j 0.-1.j 0.+0.j]
[0.+0.j 0.+1.j 0.+1.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.-1.j]
[0.+1.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.-1.j 0.-1.j 0.+0.j]
[0.+0.j 0.+1.j 0.+0.j 0.+0.j 0.+1.j 0.+0.j 0.+0.j 0.-1.j]
[0.+0.j 0.+0.j 0.+1.j 0.+0.j 0.+1.j 0.+0.j 0.+0.j 0.-1.j]
[0.+0.j 0.+0.j 0.+0.j 0.+1.j 0.+0.j 0.+1.j 0.+1.j 0.+0.j]]
N
X
H= Bi σiz . (6.10)
i=1
field_zham(N,mode='homogeneous',B=1)
190 One-Dimensional Quantum Spin-1/2 Chain Models
Parameters
Implementation
def field_zham(self,N,mode='homogeneous',B=1):
"""
Constructs Hamiltonian of external magentic field in Z direction
Input
N: number of spins
mode: 'homogeneous' or 'inhomogeneous' magnetic field
B: it list of value if mode='inhomogeneous', and constant if
mode='homogeneous'
Output
zham: Hamiltonian
"""
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
zham=np.zeros([2**N,2**N],dtype='float64')
if mode == 'homogeneous':
PN
# Constructing Hamiltonian, z
i=1 Bσi
for i in range(0,2**N):
sum1=0.0
bvec=qobj.decimal_binary(i,N)
for k in range(0,N):
sum1=sum1+(((-1)**bvec[k])*B)
zham[i,i]=sum1
else:
Hamiltonian for the direct exchange spin–spin interaction 191
Example
Here, we are finding the Hamiltonian of spins interacting with inhomogeneous magnetic
field in Z direction.
hamz=hamobj.field_zham(3)
print(hamz)
The preceding code prints the matrix elements of the required Hamiltonian.
[[ 3. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 1. 0. 0. 0. 0. 0.]
[ 0. 0. 0. -1. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 1. 0. 0. 0.]
[ 0. 0. 0. 0. 0. -1. 0. 0.]
[ 0. 0. 0. 0. 0. 0. -1. 0.]
[ 0. 0. 0. 0. 0. 0. 0. -3.]]
Here Jri is the interaction strength (which can be both positive and negative) which is also
called the coupling constant between the ith and the (i + r)th spins with r representing the
rth neighbouring interaction. Here N is the total number of spins (qubits) present in the
j
system and σ j are the Pauli spin matrices, where j = x, y, z. The notation σi+r denotes
the Pauli spin matrices corresponding to the r neighbouring spin interacting with the spin
th
represented by σij at the ith location. Note that Jri may be a constant or it can be chosen
from any probability distribution to induce bond randomness. In what follows below, we
discuss three cases.
192 One-Dimensional Quantum Spin-1/2 Chain Models
ham_xx(N,nn=1,mode='homogeneous',jx=1,condition='periodic')
Parameters
Implementation
def ham_xx(self,N,nn=1,mode='homogeneous',jx=1,condition='periodic'):
"""
Constructs Hamiltonian of spin-spin interaction in X direction
Input
N: number of spins
nn: specifying rth interaction
mode: 'homogeneous' or 'inhomogeneous' magnetic field
jx: it list of values if mode='inhomogeneous', and constant if
Hamiltonian for the direct exchange spin–spin interaction 193
mode='homogeneous'
condition: defining boundary conditions of the Hamiltonian
Output
ham: Hamiltonian
"""
# Checking entered boundary condition, PBC and OBC
assert condition == 'periodic' or condition == 'open'
# Number of interaction depending on the boundary condition
if condition == 'periodic':
kn=N
else:
kn=N-nn
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Checking number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
# Checking valid rth interaction
assert nn<=N-1 and nn>=1,"Not valid interaction"
if mode == 'homogeneous':
ham=np.zeros([2**N,2**N],dtype='float64')
col2=np.zeros([N,1])
PN
# Row index of matrix, x x
i=1 Jr σi σi+r
for i in range(0,2**N):
PN
# Column index of matrix, x x
i=1 Jr σi σi+r
for j in range(0,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k]=1-col2[k]
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
if dec==i:
inn=1
else:
inn=0
ham[i,j]=ham[i,j]+(inn*jx)
else:
# Checking number of Jri interactions are valid according to BC
if condition=='periodic':
assert len(jx)==N,\
"The entered values of magnetic strengths are not equal to
number of spins"
else:
assert len(jx)==N-nn,\
194 One-Dimensional Quantum Spin-1/2 Chain Models
Example
In this example, we are finding the Hamiltonian of spin–spin homogeneous interaction
(Jr =(1,1,1)) with periodic boundary conditions in X direction.
ham=hamobj.ham_xx(3)
print(ham)
The preceding code prints the matrix elements of the required Hamiltonian.
[[0. 0. 0. 1. 0. 1. 1. 0.]
[0. 0. 1. 0. 1. 0. 0. 1.]
[0. 1. 0. 0. 1. 0. 0. 1.]
[1. 0. 0. 0. 0. 1. 1. 0.]
[0. 1. 1. 0. 0. 0. 0. 1.]
[1. 0. 0. 1. 0. 0. 1. 0.]
[1. 0. 0. 1. 0. 1. 0. 0.]
[0. 1. 1. 0. 1. 0. 0. 0.]]
Hamiltonian for the direct exchange spin–spin interaction 195
In the following example, we are finding the Hamiltonian of spin–spin inhomogeneous in-
teraction (Jr1 = 0.1, and Jr2 = 0.3) with open boundary conditions in X direction.
ham=hamobj.ham_xx(3,mode='inhomogeneous',jx=[0.1,0.3],condition='open')
print(ham)
ham_yy(N,nn=1,mode='homogeneous',jy=1,condition='periodic')
Parameters
Implementation
def ham_yy(self,N,nn=1,mode='homogeneous',jy=1,condition='periodic'):
"""
Constructs Hamiltonian of spin-spin interaction in Y direction
Input
N: number of spins
nn: specifying rth interaction
mode: 'homogeneous' or 'inhomogeneous' magnetic field
jy: it list of values if mode='inhomogeneous', and constant if
mode='homogeneous'
condition: defining boundary conditions of the Hamiltonian
Output
ham: Hamiltonian
"""
# Checking entered boundary condition, PBC and OBC
assert condition == 'periodic' or condition == 'open'
# Number of interaction depending on the boundary condition
if condition == 'periodic':
kn=N
else:
kn=N-nn
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
Hamiltonian for the direct exchange spin–spin interaction 197
col2=col.copy()
col2[k]=1-col2[k]
col2[k1]=1-col2[k1]
if col2[k]==col2[k1]:
ind=-1
else:
ind=1
dec=qobj.binary_decimal(col2)
if dec==i:
inn=1
else:
inn=0
ham[i,j]=ham[i,j]+(inn*jy[k]*ind)
return ham
Example
In this example, we are finding the Hamiltonian of three qubit spin–spin interaction in the
Y direction with open boundary conditions and random interactions. The Hamiltonian has
nearest-neighbour interaction, and the strength of the random interactions are J11 = 0.1
and J12 = −0.3.
ham_yy=hamobj.ham_yy(3,nn=1,mode='inhomogeneous',\
jy=[0.1,-0.3],condition='open')
print(ham_yy)
ham_zz(N,nn=1,mode='homogeneous',jz=1,condition='periodic')
Hamiltonian for the direct exchange spin–spin interaction 199
Parameters
Implementation
def ham_zz(self,N,nn=1,mode='homogeneous',jz=1,condition='periodic'):
"""
Constructs Hamiltonian of spin-spin interaction in Z direction
Input
N: number of spins
nn: specifying rth interaction
mode: 'homogeneous' or 'inhomogeneous' magnetic field
jz: it list of values if mode='inhomogeneous', and constant if
mode='homogeneous'
condition: defining boundary conditions of the Hamiltonian
Output
ham: Hamiltonian
"""
# Checking entered boundary condition, PBC and OBC
assert condition == 'periodic' or condition == 'open'
# Number of interaction depending on the boundary condition
if condition == 'periodic':
kn=N
200 One-Dimensional Quantum Spin-1/2 Chain Models
else:
kn=N-nn
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Checking number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
# Checking valid rth interaction
assert nn<=N-1 and nn>=1,"Not valid interaction"
ham=np.zeros([2**N,2**N],dtype='float64')
if mode == 'homogeneous':
PN
# Row index of matrix, z z
i=1 Jr σi σi+r
for i in range(0,2**N):
PN
# Column index of matrix, z z
i=1 Jr σi σi+r
for j in range(0,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
egv1=1-2*col[k]
egv1=int(egv1)
egv2=1-2*col[k1]
egv2=int(egv2)
if i==j:
inn=1
else:
inn=0
ham[i,j]=ham[i,j]+(inn*jz*egv1*egv2)
else:
# Checking number of Jri interactions are valid according to BC
if condition=='periodic':
assert len(jz)==N,\
"The entered values of magnetic strengths are not equal to
number of spins"
else:
assert len(jz)==N-nn,\
"The entered values of magnetic strengths are not equal to
number of spins"
PN
# Row index of matrix, i z z
i=1 Jr σi σi+r
for i in range(0,2**N):
PN
# Column index of matrix, i z z
i=1 Jr σi σi+r
for j in range(0,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
Hamiltonian for the direct exchange spin–spin interaction 201
egv1=1-2*col[k]
egv1=int(egv1)
egv2=1-2*col[k1]
egv2=int(egv2)
if i==j:
inn=1
else:
inn=0
ham[i,j]=ham[i,j]+(inn*jz[k]*egv1*egv2)
return ham
Example
In this example, we are finding the Hamiltonian of the three qubit spin-spin homogeneous
interaction (Jri = 2, for all i = {1, 2, 3}) with periodic boundary conditions in Z direction.
The Hamiltonian has nearest-neighbour interactions (r = 1).
ham_zz=hamobj.ham_zz(3,jz=2)
print(ham_zz)
Using our recipes, we can construct the above Hamiltonian as follows. We have taken the
example of N = 3, J = 1, B = 1 and r = 1 under PBC.
ham_zz=hamobj.ham_zz(3,jz=1)
ham_z=hamobj.field_zham(3,mode='homogeneous',B=1)
ham=-ham_zz-ham_z
In the above program, the variable “ham” contains the required Hamiltonian matrix as
shown in Eq. [6.15]
202 One-Dimensional Quantum Spin-1/2 Chain Models
Using our recipes, we can construct the above Hamiltonian as follows. We have taken the
example of N = 3, J = 1, B = 1, and r = 1 under PBC.
ham_zz=hamobj.ham_zz(3,jz=1)
ham_x=hamobj.field_xham(3,mode='homogeneous',B=1)
ham=-ham_zz-ham_x
In the above program, the variable ham contains the required Hamiltonian matrix as shown
in Eq. [6.16]. The tilted field Ising model as well as other Ising type models with any
neighbour interaction can simply be constructed suitably using similar prescriptions as
given in Sections (6.2.4.1, 6.2.4.2).
Note the dot product in the above Hamiltonian. If J < 0, the ground state of the above
Hamiltonian is ferromagnetic and If J > 0, the ground state is antiferromagnetic. It can
be solved analytically for its ground states as well as the excited states by Bethe Ansatz
[128] for the case when J < 0 and by Hulthen’s method [117] for the case when J > 0.
We generalize the Heisenberg Hamiltonian to the rth neighbour with random interaction
strengths using Eq. [6.17] as,
N
X N
X
H= Jri ~σi .~σi+r = Jri (σix σi+r
x
+ σiy σi+r
y
+ σiz σi+r
z
), (6.18)
i=1 i=1
where Jri is the interaction strength which is also called the coupling constant between the
the ith and (i + r)th spin. ~σ = (σ x , σ y , σ z ) where σ j are the Pauli spin matrices, with
j = x, y, z. Also note σi+r denotes the Pauli spin matrices corresponding to the rth
neighbouring spin from σi . It is very easy to verify that
Parameters
Implementation
def heisenberg_hamiltonian(self,N,nn=1,mode='homogeneous',j=1.0,\
condition='periodic'):
"""
Construct Heisenberg interaction type Hamiltonian
Input
N: number of spins
nn: specifying rth interaction
mode: 'homogeneous' or 'inhomogeneous' magnetic field
j: it list of values if mode='inhomogeneous', and constant if
mode='homogeneous'
condition: defining boundary conditions of the Hamiltonian
204 One-Dimensional Quantum Spin-1/2 Chain Models
Output
ham: Hamiltonian
"""
# Checking entered boundary condition, PBC and OBC
assert condition == 'periodic' or condition == 'open'
# Number of interaction depending on the boundary condition
if condition == 'periodic':
kn=N
else:
kn=N-nn
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Checking number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
# Checking valid rth interaction
assert nn<=N-1 and nn>=1,"Not valid interaction"
ham=np.zeros([2**N,2**N],dtype='float64')
if mode == 'homogeneous':
PN
# Row index of matrix, i=1 Jr ~
σi .~σi+r
for i in range(0,2**N):
PN
# Column index of matrix, i=1 Jr ~
σi .~σi+r
for jj in range(0,2**N):
col=qobj.decimal_binary(jj,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
temp=col2[k]
col2[k]=col2[k1]
col2[k1]=temp
dec=qobj.binary_decimal(col2)
if dec==i:
inn=1
else:
inn=0
ham[i,jj]=ham[i,jj]+(inn*j*2.0)
if i==jj:
ham[i,jj]=ham[i,jj]-j
else:
# Checking number of Jri interactions are valid according to BC
if condition=='periodic':
assert len(j)==N,\
"Entered list of values j are not equal to number of
interaction in PBC"
else:
assert len(j)==N-nn,\
The Heisenberg interaction 205
Example
In this example, we are finding the Hamiltonian of the Heisenberg random interaction
(Jr=(1,2,3)) with periodic boundary conditions.
ham_hie=hamobj.heisenberg_hamiltonian(3,mode='inhomogeneous',j=[1,2,3],
condition='periodic')
print(ham_hie)
the form,
N
X
H= (Jrx σix σi+r
x
+ Jry σiy σi+r
y
+ Jrz σiz σi+r
z
), (6.20)
i=1
here if Jrx = Jry = Jrz , we call it the XXX model, if Jrx 6= Jry 6= Jrz , it is called the
XY Z model and if J = Jrx = Jry 6= Jrz , it is called the XXZ model. These can be readily
generalized to the rth neighbour models. Note that here Jrj does not contain superscript i
as the original Heisenberg model does not have bond randomness.
In the above program, the variable “ham” contains the required Hamiltonian matrix of the
XXX model.
In the above program, the variable “ham” contains the required Hamiltonian matrix of the
XY Z model.
In the above program, the variable “ham” contains the required Hamiltonian matrix of the
Majumdar-Ghosh model.
The Dzyaloshinskii-Moriya interaction 207
where D ~ = (Dx , Dy , Dz ) is the DM vector, whose components specify the coupling param-
eter of the DM interaction in the X, Y, Z directions, respectively, N is the total number of
spins or qubits. Note that for the DM interaction between spins at sites i and i + 1, the
vector D is orthogonal to both the spins. That is, if the two interacting spins are lying in
the plane of this book, then the DM vector is along the direction either into or out of the
plane containing book. This cross product term as given in Eq. [6.22] is also related to what
is known as the Heisenberg spin current [137]. Since the interaction is antisymmetric, the
above Hamiltonian involves a cross product. We can recast Eq. [6.22] as,
Dx Dy Dz
N
X
H= σix σiy σiz . (6.23)
i=1
x y z
σi+1 σi+1 σi+1
We give the recipes for constructing these three terms in the Hamiltonian separately. The
above formalism of antisymmetric exchange can be faithfully extended to r nearest neigh-
bours also and in fact the recipes we give involve DM interaction between the spin at the
site i and the spin at site the i + r as
N
X
H= Drx (σiy σi+r y
) + Dry (σiz σi+r )
i z
− σiz σi+r i x
− σix σi+r
z
i=1
y
+Drz
i
(σix σi+r − σiy σi+r
x
) . (6.25)
Also note that to accommodate the random couplings between ith and (i + r)th spins, we
make the components of D~ random by introducing the superscript i. It is noteworthy to
208 One-Dimensional Quantum Spin-1/2 Chain Models
mention that, in general, in Eq. [6.24], only one component of the vector D is non-zero,
depending on the direction of the orientation of vector D.
dm_xham(N,nn=1,mode='homogeneous',dx=1.0, condition='periodic')
Parameters
Implementation
N: number of spins
nn: specifying rth interaction
mode: 'homogeneous' or 'inhomogeneous' magnetic field
dx: it list of values if mode='inhomogeneous', and constant if
mode='homogeneous'
condition: defining boundary conditions of the Hamiltonian
Output
ham: Hamiltonian
"""
# Checking entered boundary condition, PBC and OBC
assert condition == 'periodic' or condition == 'open'
# Number of interaction depending on the boundary condition
if condition == 'periodic':
kn=N
else:
kn=N-nn
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Checking number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
# Checking valid rth interaction
assert nn<=N-1 and nn>=1,"Not valid interaction"
ham=np.zeros([2**N,2**N],dtype=np.complex_)
if mode == 'homogeneous':
PN y z z y
# Row index of matrix, i=1 Drx (σi σi+r − σi σi+r )
for i in range(0,2**N):
PN y z z y
# Column index of matrix, i=1 Drx (σi σi+r − σi σi+r )
for j in range(i,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k]=1-col2[k]
dec=qobj.binary_decimal(col2)
inn1=complex(0.0,0.0)
if dec==i:
phase=complex(0,1)*(-1)**col[k]
szpart=(-1)**col[k1]
inn1=phase*szpart
col2=col.copy()
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
inn2=complex(0.0,0.0)
if dec==i:
phase=complex(0,1)*(-1)**col[k1]
210 One-Dimensional Quantum Spin-1/2 Chain Models
szpart=(-1)**col[k]
inn2=phase*szpart
ham[i,j]=ham[i,j]+((inn1-inn2)*dx)
ham[j,i]=np.conjugate(ham[i,j])
else:
# Checking number of Drxi
interactions are valid according to BC
if condition=='periodic':
assert len(dx)==N,\
"The entered values of magnetic strengths are not equal to
number of spins"
else:
assert len(dx)==N-nn,\
"The entered values of magnetic strengths are not equal to
number of spins"
PN y z z y
# Row index of matrix, i=1 Drx (σi σi+r − σi σi+r )
i
for i in range(0,2**N):
PN y z z y
# Column index of matrix, i=1 Drx (σi σi+r − σi σi+r )
i
for j in range(i,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k]=1-col2[k]
dec=qobj.binary_decimal(col2)
inn1=0.0
if dec==i:
phase=complex(0,1)*(-1)**col[k]
szpart=(-1)**col[k1]
inn1=phase*szpart
col2=col.copy()
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
inn2=0.0
if dec==i:
phase=complex(0,1)*(-1)**col[k1]
szpart=(-1)**col[k]
inn2=phase*szpart
ham[i,j]=ham[i,j]+((inn1-inn2)*dx[k])
ham[j,i]=np.conjugate(ham[i,j])
return ham
Example
In this example, we are finding the DM Hamiltonian with random interaction couplings
(Dr=(1,2,3)) in the X direction with periodic boundary condition.
ham=hamobj.dm_xham(3,mode='inhomogeneous',dx=[1,2,3],condition='periodic')
print(ham)
The Dzyaloshinskii-Moriya interaction 211
The preceding code prints the matrix elements of the required Hamiltonian as shown below.
[[0.-0.j 0.-1.j 0.-1.j 0.+0.j 0.+2.j 0.+0.j 0.+0.j 0.+0.j]
[0.+1.j 0.-0.j 0.+0.j 0.+3.j 0.+0.j 0.-4.j 0.+0.j 0.+0.j]
[0.+1.j 0.-0.j 0.-0.j 0.-5.j 0.+0.j 0.+0.j 0.+4.j 0.+0.j]
[0.-0.j 0.-3.j 0.+5.j 0.-0.j 0.+0.j 0.+0.j 0.+0.j 0.-2.j]
[0.-2.j 0.-0.j 0.-0.j 0.-0.j 0.-0.j 0.+5.j 0.-3.j 0.+0.j]
[0.-0.j 0.+4.j 0.-0.j 0.-0.j 0.-5.j 0.-0.j 0.+0.j 0.+1.j]
[0.-0.j 0.-0.j 0.-4.j 0.-0.j 0.+3.j 0.-0.j 0.-0.j 0.+1.j]
[0.-0.j 0.-0.j 0.-0.j 0.+2.j 0.-0.j 0.-1.j 0.-1.j 0.-0.j]]
dm_yham(N,nn=1,mode='homogeneous',dy=1.0, condition='periodic')
Parameters
Implementation
for i in range(0,2**N):
PN
# Column index of matrix, i=1 Dry (σi σi+r − σi σi+r )
z x x z
for j in range(i,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
inn1=0.0
if dec==i:
inn1=(-1)**col[k]
col2=col.copy()
col2[k]=1-col2[k]
dec=qobj.binary_decimal(col2)
inn2=0.0
The Dzyaloshinskii-Moriya interaction 213
if dec==i:
inn2=(-1)**col[k1]
ham[i,j]=ham[i,j]+((inn1-inn2)*dy)
ham[j,i]=ham[i,j]
else:
# Checking number of Dryi
interactions are valid according to BC
if condition=='periodic':
assert len(dy)==N,\
"The entered values of magnetic strengths are not equal to
number of spins"
else:
assert len(dy)==N-nn,\
"The entered values of magnetic strengths are not equal to
number of spins"
PN
# Row index of matrix, i=1 Dry (σi σi+r − σi σi+r )
i z x x z
for i in range(0,2**N):
PN
# Column index of matrix, i=1 Dry (σi σi+r − σi σi+r )
i z x x z
for j in range(i,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
inn1=0.0
if dec==i:
inn1=(-1)**col[k]
col2=col.copy()
col2[k]=1-col2[k]
dec=qobj.binary_decimal(col2)
inn2=0.0
if dec==i:
inn2=(-1)**col[k1]
ham[i,j]=ham[i,j]+((inn1-inn2)*dy[k])
ham[j,i]=ham[i,j]
return ham
Example
In this example, we are finding the DM Hamiltonian with random interaction couplings
(Dr=(1,1,1)) in the Y direction with periodic boundary condition.
ham=hamobj.dm_yham(3)
print(ham)
The preceding code prints the matrix elements of the required Hamiltonian as shown below.
[[ 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 2. 0. -2. 0. 0.]
214 One-Dimensional Quantum Spin-1/2 Chain Models
[ 0. 0. 0. -2. 0. 0. 2. 0.]
[ 0. 2. -2. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 2. -2. 0.]
[ 0. -2. 0. 0. 2. 0. 0. 0.]
[ 0. 0. 2. 0. -2. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0.]]
dm_zham(N,nn=1,mode='homogeneous',dz=1.0, condition='periodic')
Parameters
Implementation
"""
# Checking entered boundary condition, PBC and OBC
assert condition == 'periodic' or condition == 'open'
# Number of interaction depending on the boundary condition
if condition == 'periodic':
kn=N
else:
kn=N-nn
# Only two modes are allowed, homogeneous and inhomogeneous
assert mode == 'homogeneous' or mode == 'inhomogeneous',\
"Entered mode is invalid"
# Checking number of spins N ≥ 1
assert N >= 1, "number of spins entered is not correct"
# Checking valid rth interaction
assert nn<=N-1 and nn>=1,"Not valid interaction"
ham=np.zeros([2**N,2**N],dtype=np.complex_)
if mode == 'homogeneous':
PN x y y x
# Row index of matrix, i=1 Drz (σi σi+r − σi σi+r )
for i in range(0,2**N):
PN x y y x
# Column index of matrix, i=1 Drz (σi σi+r − σi σi+r )
for j in range(i,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k]=1-col2[k]
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
inn1=complex(0.0,0.0)
inn2=complex(0.0,0.0)
if dec==i:
inn1=complex(0,1)*(-1)**col[k1]
inn2=complex(0,1)*(-1)**col[k]
216 One-Dimensional Quantum Spin-1/2 Chain Models
ham[i,j]=ham[i,j]+((inn1-inn2)*dz)
ham[j,i]=np.conjugate(ham[i,j])
else:
# Checking number of Drzi
interactions are valid according to BC
if condition=='periodic':
assert len(dz)==N,\
"The entered values of magnetic strengths are not equal to
number of spins"
else:
assert len(dz)==N-nn,\
"The entered values of magnetic strengths are not equal to
number of spins"
PN x y y x
# Row index of matrix, i=1 Drz (σi σi+r − σi σi+r )
i
for i in range(0,2**N):
PN x y y x
# Column index of matrix, i=1 Drz (σi σi+r − σi σi+r )
i
for j in range(i,2**N):
col=qobj.decimal_binary(j,N)
for k in range(0,kn):
k1=k+nn
# For wrapping the Hamiltonian
if k1>=N:
k1=k1-N
col2=col.copy()
col2[k]=1-col2[k]
col2[k1]=1-col2[k1]
dec=qobj.binary_decimal(col2)
inn1=complex(0.0,0.0)
inn2=complex(0.0,0.0)
if dec==i:
inn1=complex(0,1)*(-1)**col[k1]
inn2=complex(0,1)*(-1)**col[k]
ham[i,j]=ham[i,j]+((inn1-inn2)*dz[k])
ham[j,i]=np.conjugate(ham[i,j])
return ham
Example
In this example, we are finding the DM Hamiltonian with random interaction couplings
(Dr=(1,1,1)) in the Z direction with periodic boundary condition.
ham=hamobj.dm_zham(3)
print(ham)
The preceding code prints the matrix elements of the required Hamiltonian as shown below.
[[0.-0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
[0.-0.j 0.-0.j 0.+2.j 0.+0.j 0.-2.j 0.+0.j 0.+0.j 0.+0.j]
[0.-0.j 0.-2.j 0.-0.j 0.+0.j 0.+2.j 0.+0.j 0.+0.j 0.+0.j]
[0.-0.j 0.-0.j 0.-0.j 0.-0.j 0.+0.j 0.+2.j 0.-2.j 0.+0.j]
[0.-0.j 0.+2.j 0.-2.j 0.-0.j 0.-0.j 0.+0.j 0.+0.j 0.+0.j]
[0.-0.j 0.-0.j 0.-0.j 0.-2.j 0.-0.j 0.-0.j 0.+2.j 0.+0.j]
[0.-0.j 0.-0.j 0.-0.j 0.+2.j 0.-0.j 0.-2.j 0.-0.j 0.+0.j]
[0.-0.j 0.-0.j 0.-0.j 0.-0.j 0.-0.j 0.-0.j 0.-0.j 0.-0.j]]
The Dzyaloshinskii-Moriya interaction 217
Using our codes in this chapter, we can construct the above Hamiltonian as follows. We
have taken the example of N = 3, J = 1, Dz = 1 and r = 1 under PBC.
hamz=hamobj.dm_zham(3)
ham_hie=hamobj.heisenberg_hamiltonian(3)
ham=ham_hie+hamz
In the above program, the variable ‘ham’ contains the required Hamiltonian matrix.
7
Generating Random Matrices and Random Vectors
In this chapter we give the numerical recipes to construct random numbers following a
particular distribution and also construct random states and density matrices, here we have
used the Gaussian and the uniform distribution. These are the most important distributions
used in constructing random quantum states [138] as we progress through the chapter.
Moreover, there are recipes to construct random matrices which occur in random matrix
theory [139–141] and boderlining areas of quantum information. At the end of the chapter,
recipes to construct the random density matrix are also given which will be very useful for
people working on random quantum mixed states [142, 143]. The examples indicating the
output using these recipes are not given for a specific reason that everytime the user prints
an output, it will be different as they are from random processes. In this chapter we will be
primarily discussing the class, QRandom. The class is written inside the Python module
chap7_randommatrix.py. The class QRandom contains all the methods pertaining to
generating random matrices from the uniform and Gaussian distribution. To import the
class and create its objects, you have to write the following code,
# Importing the QRandom class
from QuantumInformation import QRandom
where µ, σ 2 are the mean and variance of the probability distribution, respectively. In
Python, we use Numpy’s random number routine to generate numbers from uniform or
Gaussian distribution.
7.1.1 For a real random matrix whose elements are chosen from the
Gaussian distribution
random_gaussian_rvec(tup,mu=0,sigma=1)
Parameters
[in] sigma sigma is the standard deviation of the distribution. Its de-
fault value is 1
Implementation
def random_gaussian_rvec(self,tup,mu=0,sigma=1):
"""
Construct a real random state from a Gaussian distribution
Input:
tup: tuple holds the dimension of the output matrix.
mu: it is the average value of the gaussian distribution.
sigma: it is the standard deviation of the gaussian distribution
Output:
gauss_state: it the gaussian distributed real state
"""
gauss_state=np.random.normal(mu, sigma, tup)
return gauss_state
Example
state_gauss=qrobj.random_gaussian_rvec((3,3))
In the above example, state_gauss is a 3×3 real matrix which stores the random numbers
from the Gaussian distribution with zero mean and unit standard deviation.
7.1.2 For a complex random matrix whose real and imaginary parts are
individually chosen from the Gaussian distribution
random_gaussian_cvec(tup,mu=0,sigma=1)
Random number generator for uniform distribution in the range (a,b) 221
Parameters
Implementation
def random_gaussian_cvec(self,tup,mu=0,sigma=1):
"""
Construct a complex random state from a Gaussian distribution
Input:
tup: tuple holds the dimension of the output matrix.
mu: it is the average value of the gaussian distribution.
sigma: it is the standard deviation of the gaussian distribution
Output:
gauss_state: it the gaussian distributed complex state
"""
# Real part selected from Gaussian distribution
rpart=np.random.normal(mu, sigma, tup)
# Complex part selected from Gaussian distribution
cpart=np.random.normal(mu, sigma, tup)
state_gauss=np.zeros(tup,dtype=np.complex_)
state_gauss=rpart+(complex(0,1)*cpart)
return state_gauss
Example
state_gauss=qrobj.random_gaussian_cvec((3,3))
In the above example, state_gauss is a 3 × 3 complex matrix which stores the random
numbers from the Gaussian distribution with zero mean and unit standard deviation.
7.2.1 For a real random matrix whose elements are chosen from uniform
distribution
random_unifrom_rvec(tup,low=0.0,high=1.0)
Parameters
[in] low It is the lowest value of the range. Its default value is 0
[in] high It is the highest value of the range. Its default value is equal
to 1
Implementation
def random_unifrom_rvec(self,tup,low=0.0,high=1.0):
"""
Construct a real random state from a uniform distribution
Input:
tup: tuple holds the dimension of the output matrix.
low: it is the lower bound of the uniform distribution.
high: it is the upper bound of the uniform distribution
Output:
uniform_state: it the uniform distributed real state
"""
uniform_state=np.random.uniform(low=low, high=high, size=tup)
return uniform_state
Example
state_uniform=qrobj.random_unifrom_rvec((3,3),low=1,high=2)
In the above example, state_uniform is a 3×3 real matrix which stores the random numbers
chosen from a uniform distribution in the range (1,2).
7.2.2 For a complex random matrix whose elements are chosen from
uniform distribution
random_unifrom_cvec(tup,low=0.0,high=1.0)
Random real symmetric matrices 223
Parameters
[in] low It is the lowest value of the range. Its default value is 0
[in] high It is the highest value of the range. Its default value is equal
to 1
Implementation
def random_unifrom_cvec(self,tup,low=0.0,high=1.0):
"""
Construct a complex random state from a uniform distribution
Input:
tup: tuple holds the dimension of the output matrix.
low: it is the lower bound of the uniform distribution.
high: it is the upper bound of the uniform distribution
Output:
uniform_state: it the uniform distributed complex state
"""
# Real part chosen from uniform distribution
uniform_rpart=np.random.uniform(low=low, high=high, size=tup)
# Complex part chosen from uniform distribution
uniform_cpart=np.random.uniform(low=low, high=high, size=tup)
uniform_state=uniform_rpart+(complex(0,1)*uniform_cpart)
return uniform_state
Example
state_uniform=qrobj.random_unifrom_cvec((2,2),low=-1,high=1)
In the above example, state_uniform is a 2 × 2 complex matrix which stores the random
numbers chosen from a uniform distribution in the range (−1, 1).
random_symmetric_matrix(size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0)
Parameters
Implementation
def random_symmetric_matrix(self,size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0):
"""
Construct a random symmetric matrix
Input:
size: it is a tuple (a,b), where a in number of rows and b is
number of columns of the symmetric matrix
distribution: it is the type of distribution
Output:
smatrix: symmetric matrix
"""
Random complex Hermitian matrices 225
Example
random_smatrix=qrobj.random_symmetric_matrix(size=(3,3),\
distribution='gaussian',\
mu=2,sigma=0.5)
1
B= (A + A† ) (7.3)
2
random_hermitian_matrix(self,size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0)
226 Generating Random Matrices and Random Vectors
Parameters
Implementation
def random_hermitian_matrix(self,size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0):
"""
Construct a random Hermitian matrix
Input:
size: it is a tuple (a,b), where a in number of rows and b is
number of columns of the hermitian matrix
distribution: it is the type of distribution
Output:
hmatrix: hermitian matrix
"""
# Checking whether entered mode for distribution is correct or not
Random unitary matrices 227
assert re.findall("^gaussian|^uniform",distribution),\
"Invalid distribution type"
hmatrix=np.zeros([size[0],size[1]],dtype=np.complex_)
if distribution=='gaussian':
for i in range(0,hmatrix.shape[0]):
for j in range(0,hmatrix.shape[1]):
hmatrix[i,j]=complex(np.random.normal(mu, sigma, size= None
),\
np.random.normal(mu, sigma, size= None))
else:
for i in range(0,hmatrix.shape[0]):
for j in range(0,hmatrix.shape[1]):
hmatrix[i,j]=complex(np.random.uniform(low=low, high=high,\
size=None),np.random.uniform(low=low, high=high,\
size=None))
hmatrix=hmatrix+np.matrix.conjugate(np.matrix.transpose(hmatrix))
hmatrix=0.5*hmatrix
return hmatrix
Example
random_hmatrix=qrobj.random_hermitian_matrix(size=(3,3),\
distribution='uniform',\
low=5,high=10)
random_orthogonal_matrix(size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0)
228 Generating Random Matrices and Random Vectors
Parameters
Implementation
def random_orthogonal_matrix(self,size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0):
"""
Construct a random orthogonal matrix
Input:
size: it is a tuple (a,b), where a in number of rows and b is
number of columns of the orthogonal matrix
distribution: it is the type of distribution
Output:
omatrix: orthogonal matrix
"""
# Checking whether entered mode for distribution is correct or not
assert re.findall("^gaussian|^uniform",distribution),\
"Invalid distribution type"
if distribution == 'gaussian':
Random unitary matrices 229
Example
random_omatrix=qrobj.random_orthogonal_matrix(size=(3,3),\
distribution='uniform',\
low=2,high=3)
In the above example, random_omatrix is a 3 × 3 real orthogonal matrix which stores the
random numbers selectively chosen from the uniform distribution chosen from the range
(2,3).
random_unitary_matrix(size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0)
Parameters
Implementation
def random_unitary_matrix(self,size,distribution="gaussian",mu=0,\
sigma=1,low=0.0,high=1.0):
"""
Construct a unitary matrix
Input:
size: it is a tuple (a,b), where a in number of rows and b is
number of columns of the unitary matrix
distribution: it is the type of distribution
Output:
umatrix: unitary matrix
"""
# Checking whether entered mode for distribution is correct or not
assert re.findall("^gaussian|^uniform",distribution),\
"Invalid distribution type"
umatrix=np.zeros([size[0],size[1]],dtype=np.complex_)
if distribution == 'gaussian':
for i in range(0,umatrix.shape[0]):
for j in range(0,umatrix.shape[1]):
umatrix[i,j]=complex(np.random.normal(mu,sigma,size=None),\
np.random.normal(mu,sigma,size=None))
else:
for i in range(0,umatrix.shape[0]):
for j in range(0,umatrix.shape[1]):
umatrix[i,j]=complex(\
np.random.uniform(low=low,high=high,size=None),\
np.random.uniform(low=low,high=high,size=None))
umatrix,r=np.linalg.qr(umatrix,mode='complete')
return umatrix
Real Ginibre matrix 231
Example
random_umatrix=qrobj.random_unitary_matrix(size=(3,3),distribution='
gaussian')
Parameters
Implementation
def random_real_ginibre(self,N):
"""
Construct a real Ginibre matrix
Input:
N: dimension of the NxN Ginibre matrix
Output:
real ginibre matrix
"""
return np.random.normal(0.0, 1.0, size= (N,N))
Example
random_ginibre=qrobj.random_real_ginibre(2)
Parameters
Implementation
def random_complex_ginibre(self,N):
"""
Construct a complex Ginibre matrix
Input:
N: dimension of the NxN complex Ginibre
Output:
cginibre: complex ginibre matrix
"""
cginibre=np.zeros([N,N],dtype=np.complex_)
for i in range(0,cginibre.shape[0]):
for j in range(0,cginibre.shape[1]):
cginibre[i,j]=complex(np.random.normal(0.0, 1.0, size= None),\
np.random.normal(0.0, 1.0, size= None))
return cginibre
Example
random_ginibre=qrobj.random_complex_ginibre(2)
random_real_wishart(N)
Parameters
Implementation
def random_real_wishart(self,N):
"""
Construct a real Wishart matrix
Input:
N: dimension of the NxN real Wishart matrix
Output:
rwishart: real Wishart matrix
"""
g=self.random_real_ginibre(N)
rwishart=np.matmul(g,np.matrix.transpose(g))
return rwishart
Example
random_wishart=qrobj.random_real_wishart(2)
random_complex_wishart(N)
Parameters
Implementation
def random_complex_wishart(self,N):
"""
Input:
N: dimension of the NxN complex Wishart matrix
Output:
cwishart: complex Wishart matrix
"""
g=self.random_complex_ginibre(N)
cwishart=np.matmul(g,np.matrix.conjugate(np.matrix.transpose(g)))
return cwishart
Example
random_wishart=qrobj.random_complex_wishart(3)
P = (p1 , p2 , · · · , pn ) (7.5)
Parameters
subroutine RANDPVEC(d,state)
Implementation
def random_probability_vec(self,N):
"""
Constructs a random probability vector
Input:
N: dimension of the probability vector.
Output:
prob_vec: The probability vector
"""
prob_vec=np.random.uniform(low=0,high=1.0,size=N)
norm=prob_vec.sum()
prob_vec=prob_vec/norm
return prob_vec
Example
pvec=qrobj.random_probability_vec(5)
In the above example, pvec is the 5 × 1 required probability vector which is normalized.
where cj ’s are the random numbers, either real or complex chosen from any Pn random dis-
tribution, here we use the uniform and Gaussian distribution, such that j=1 |cj |2 = 1.
Note that, |ji is the computational basis. Random quantum states are solely defined ana-
lytically based on the normalization condition only which is identical to the equation of a
n-dimensional unit sphere. This probability P distribution is proportional to the Dirac delta
n
function such that the P (c1 , c2 , . . . , cn ) ∝ δ( j=1 |cj |2 − 1).
random_qrstate(N,distribution='gaussian',\
mu=0,sigma=1,low=0.0,high=1.0)
236 Generating Random Matrices and Random Vectors
Parameters
Implementation
def random_qrstate(self,N,distribution='gaussian',\
mu=0,sigma=1,low=0.0,high=1.0):
"""
Constructs a random real pure quantum state
Input:
N: number of qubits
distribution: it is the type of distribution
Output:
qrstate: real quantum state
"""
assert re.findall("^gaussian|^uniform",distribution),\
"Invalid distribution type"
if distribution=='gaussian':
qrstate=self.random_gaussian_rvec(2**N,mu=0,sigma=1)
else:
qrstate=self.random_unifrom_rvec(2**N,low=0.0,high=1.0)
norm=np.matmul(np.matrix.transpose(qrstate),qrstate)
qrstate=qrstate/np.sqrt(norm)
return qrstate
Random pure quantum state vector 237
Example
rstate=qrobj.random_qrstate(4,distribution='uniform',\
low=0.0,high=10.0)
In the above example, rstate is the 24 × 1 real random quantum state vector whose entries
are selectively chosen from the uniform distribution, in the range (0,10).
random_qcstate(self,N,distribution='gaussian',\
mu=0,sigma=1,low=0.0,high=1.0)
Parameters
Implementation
def random_qcstate(self,N,distribution='gaussian',\
mu=0,sigma=1,low=0.0,high=1.0):
"""
Constructs a random complex pure quantum state
238 Generating Random Matrices and Random Vectors
Input:
N: number of qubits
distribution: it is the type of distribution
Output:
qcstate: complex quantum state
"""
assert re.findall("^gaussian|^uniform",distribution),\
"Invalid distribution type"
if distribution=='gaussian':
qcstate=self.random_gaussian_cvec(2**N,mu=0,sigma=1)
else:
qcstate=self.random_unifrom_cvec(2**N,low=0.0,high=1.0)
norm=abs(np.matmul(\
np.matrix.conjugate(\
np.matrix.transpose(qcstate)),\
qcstate))
qcstate=qcstate/np.sqrt(norm)
return qcstate
Example
cstate=qrobj.random_qcstate(4,distribution='gaussian',\
mu=2,sigma=1)
In the above example, cstate is the 24 ×1 complex random pure quantum state vector whose
elements are selectively chosen from the Gaussian distribution with mean equal to 2 and
standard deviation equal to 1.
The matrix ρ is by construction Hermitian, positive definite and normalized. The ensemble
so generated is called the Hilbert–Schmidt ensemble.
subroutine RANDDMR(d,state)
Random density matrices 239
Parameters
[out] rden It is array of dimension (0:2N −1,0:2N −1) which is the ran-
dom real density matrix
Implementation
def random_rden(self,N):
"""
Constructs a random real pure density matrix
Input:
N: number of qubits
Output:
rden: real random density matrix
"""
rden=self.random_real_wishart(2**N)
rden=rden/np.trace(rden)
return rden
Example
rden=qrobj.random_rden(4)
In the above example, the rden variable holds a 24 × 24 real density matrix.
subroutine RANDDMC(d,state)
Parameters
[out] cden It is array of dimension (0:2N −1,0:2N −1) which is the ran-
dom complex density matrix
Implementation
def random_cden(self,N):
"""
Constructs a random complex pure density matrix
Input:
240 Generating Random Matrices and Random Vectors
N: number of qubits
Output:
cden: complex random density matrix
"""
cden=self.random_complex_wishart(2**N)
cden=cden/np.trace(cden)
return cden
Example
cden=qrobj.random_cden(4)
In the above example, the cden variable holds a 24 × 24 complex density matrix.
Bibliography
[1] B. Venners, The making of Python: A conversation with guido van rossum, part
i, Artima Developer (Jan. 2003). url: http://www. artima. com/intv/pythonP. html
(2003).
[2] A. Scopatz and K. D. Huff, Effective computation in physics: field guide to research
with Python (“O’Reilly Media, Inc.” 2015).
[3] Y. Kanetkar, Let us C (BPB publications 2018).
[4] Y. P. Kanetkar, Let us C++ (BPB Publications 1999).
[5] S. J. Chapman, Fortran 90/95 for scientists and engineers (McGraw-Hill Higher
Education 2004).
[6] M. S. Ramkarthik and P. D. Solanki, Numerical Recipes in Quantum Information
Theory and Quantum Computing: An Adventure in Fortran 90 (CRC Press 2021).
[7] C. Hill, Learning scientific programming with Python (Cambridge University Press
2020).
[8] D. J. Pine, Introduction to Python for science and engineering (CRC Press 2019).
[9] W. McKinney, Python for data analysis: data wrangling with Pandas, NumPy, and
IPython (“O’Reilly Media, Inc.” 2012).
[10] A. Géron, Hands-on machine learning with Scikit-Learn, Keras, and TensorFlow: con-
cepts, tools, and techniques to build intelligent systems (O’Reilly Media 2019).
[11] D. Ascher and M. Lutz, Learning Python (O’Reilly 1999).
[12] T. E. Oliphant, A guide to NumPy, volume 1 (Trelgol Publishing USA 2006).
[13] F. Alted, I. Vilata, S. Prater, V. Mas, T. Hedley, A. Valentino, and J. Whitaker,
Pytables user’s guide, Cárabos Coop 2007 (2002).
[14] D. Y. Chen, Pandas for everyone: Python data analysis (Addison-Wesley Professional
2017).
[15] M. Wiebe, M. Rocklin, T. Alumbaugh, and A. Terrel, Blaze: building a foundation
for array-oriented computing in python, in Proceedings of the 13th Python in science
conference. Ed. by Stéfan van der Walt and James Bergstra (2014), pp. 99–102.
[16] D. Phillips, Python 3 object oriented programming (Packt Publishing Ltd 2010).
[17] E. Anderson, Z. Bai, C. Bischof, L. S. Blackford, J. Demmel, J. Dongarra, J. Du Croz,
A. Greenbaum, S. Hammarling, A. McKenney et al., LAPACK users’ guide (SIAM
1999).
241
242 Bibliography
[64] R. Jozsa, Fidelity for mixed quantum states, Journal of Modern Optics 41 (1994).
[65] C. A. Fuchs and C. M. Caves, Ensemble-dependent bounds for accessible information
in quantum mechanics, Physical Review Letters 73 (1994).
[66] Q. Wang, Y. Tian, W. Li, L. Tian, Y. Wang, and Y. Zheng, High-fidelity quantum
teleportation toward cubic phase gates beyond the no-cloning limit, Phys. Rev. A 103,
062421 (2021).
[67] X. Xu, S. C. Benjamin, and X. Yuan, Variational circuit compiler for quantum error
correction, Phys. Rev. Applied 15, 034068 (2021).
[68] S. Chen, L. Wang, S.-J. Gu, and Y. Wang, Fidelity and quantum phase transition for
the Heisenberg chain with next-nearest-neighbor interaction, Physical Review E 76,
061108 (2007).
[69] Z.-H. Chen, Z. Ma, F.-L. Zhang, and J.-L. Chen, Super fidelity and related metrics,
Central European Journal of Physics 9, 1036 (2011).
[70] W. K. Wootters, Statistical distance and Hilbert space, Physical Review D 23 (1981).
[71] A. Uhlmann, The “transition probability” in the state space of a str-algebra, Reports
on Mathematical Physics 9 (1976).
[72] D. Bures, An extension of Kakutani’s theorem on infinite product measures to the ten-
sor product of semifinite w star algebras, Transactions of the American Mathematical
Society 135 (1969).
[73] C. J. Isham, Lectures on quantum theory mathematical and structural foundations
(Imperial College Press 1995).
[74] C. Cohen-Tannoudji, B. Diu, and F. Laloe, Quantum mechanics, vol-1 (Wiley VCH
2006).
[75] R. Horodecki, P. Horodecki, M. Horodecki, and K. Horodecki, Quantum entanglement,
Reviews of Modern Physics 81 (2009).
[76] E. Schrödinger, Discussion of probability relations between separated systems, Math-
ematical Proceedings of the Cambridge Philosophical Society 31 (1935).
[77] E. Schrödinger, Die gegenwärtige situation in der quantenmechanik, Naturwis-
senschaften 23 (1935).
Bibliography 245
[78] J. A. Wheeler and W. H. Zurek, Quantum theory and measurement (Princeton Uni-
versity Press 2014).
[79] B. M. Terhal, M. M. Wolf, and A. C. Doherty, Quantum entanglement: A modern
perspective, Physics Today 56 (2003).
[80] S. Sachdev, Quantum phase transitions (Cambridge University Press 2011).
[81] J. Keating and F. Mezzadri, Random matrix theory and entanglement in quantum
spin chains, Communications in mathematical physics 252, 543 (2004).
[82] T. Roscilde, P. Verrucchi, A. Fubini, S. Haas, and V. Tognetti, Studying quantum spin
systems through entanglement estimators, Physical review letters 93, 167203 (2004).
[90] O. Gühne and G. Tóth, Entanglement detection, Physics Reports 474 (2009).
[91] M. B. Plenio and S. S. Virmani, An introduction to entanglement measures, Quantum
Information & Computation 7 (2007).
[92] P. Barkataki and M. S. Ramkarthik, A set theoretical approach for the partial tracing
operation in quantum mechanics, International Journal of Quantum Information 16
(2018).
[93] D. Bruß and C. Macchiavello, How the first partial transpose was written?, Founda-
tions of Physics 35 (2005).
[139] P. Forrester, Log-gases and random matrices (Princeton University Press 2005).
[140] M. L. Mehta, Random matrices (Academic Press 2004).
[141] J. Maziero, Fortran code for generating random probability vectors, unitaries, and
quantum states, Frontiers in ICT 3 (2016).
249
250 Subject index