Mahendra Verma - Practical Numerical Computing Using Python_ Scientific & Engineering Applications (2021)
Mahendra Verma - Practical Numerical Computing Using Python_ Scientific & Engineering Applications (2021)
Preface
Chapter One - Introduction
Introduction to Computing
Computer Hardware
Supercomputers & Computation Complexity
Computer Software
Brief Overview of Python
Anaconda Python, Notebooks, and Prutor
Applications of Computers
Chapter Two - Python Data Types: Integers and Floats
Integers
Floating Point and Complex Numbers
Python Variables and Logical Variables
Chapter Three - Python Data Types: Strings, List, and Arrays
Character & Strings
List
Numpy Arrays
Dictionary
Mutable and Immutable Objects in Python
Chapter Four - Control Structures in Python
Simple Statements
Conditional Flows in Python
Looping in Python
Chapter Five - Functions in Python
Functions in Python
Python Namespace and Scope
Recursive Functions
Chapter Six - Solving Complex Problems Using Python
Good Progamming Practices
Prime Numbers
Searching and Sorting
Chapter Seven - Plotting in Python
Matplotlib & Field Plots
Miscellaneous Plots
Animation using Python
Chapter Eight - Input/Output in Python
Reading & Writing Text Files in Python
Reading & Writing Numerical Data in Python
Chapter Nine - Errors & Nondimensionalization
Error Analysis
Nondimensionalization of Equations
Numerical Methods
Chapter Ten - Interpolation
Lagrange Interpolation
Splines
Chapter Eleven - Numerical Integration
Newton-Cotes Formulas
Gaussian Quadrature
Python's Quad & Multidimensional Integrals
Chapter Twelve - Numerical Differentiation
Computing Numerical Derivatives
Chapter Thirteen - Ordinary Differential Equation: Initial Value Problems
General Overview
Euler Forward Method, Accuracy & Stability
Implicit Schemes
Higher-order Methods
Multistep Method
Solving a System of Equations
Stiff Equations
Chapter Fourteen - Fourier Transform
Fourier Transform
One-dimensional Discrete Fourier Transforms
Mutlidimensional Fourier Transform
Chapter Fifteen - Spectral Method for PDEs
Solving PDEs Using Spectral Method: Diffusion Equation
Solving Wave, Burgers, and KdV Equations
Spectral Solution of Naiver-Stokes Equation
Spectral Solution of Schrödinger Equation
Chapter Sixteen - Solving PDEs using Finite Difference Method
General Overview & Diffusion Equation Solver
Solving Wave Equation
Burgers and Navier-Stokes Equations
Schrodinger equation
Chapter Seventeen - Solving Nonlinear Algebraic Equations
Root Finders
Chapter Eighteen - Boundary Value Problems
Shooting Method
Eigenvalue Calculation
Chapter Nineteen - Solvers for Laplace and Poisson Equations
Solving Laplace Equation
Solving Poisson Equation
Chapter Twenty - Linear Algebra Solvers
Solution of Algebraic Equations
Eigenvalues and Eigenvectors
Chapter Twenty-One - Monte Carlo Methods and Data Science
Random numbers
Integration Using Random Numbers
Regression Analysis
Applications in Statmech
Machine Learning
Epilogue
Appendix A: Errors in Lagrange Interopolation
Appendix B: Improving Accuracy Using Richardson Method
References
Preface
At present, friendly, yet, powerful tools have been developed to program
equally powerful computers. The programming language Python, one such
tool, is simple to learn, but very powerful. It is being used in wide range of
applications: scientific computing, data analysis, machine learning (ML) and
artificial intelligence (AI), internet programming, GUI, etc. Consequently,
researchers are employing Python for numerical computing, as well as AI
and ML.
Keeping this in mind, I chose Python as the programming language for
teaching numerical computing in my Computational Physics course
(PHY473A). We also use Python for writing large softwares including
parallel computing and post processing. The present book is a compilation of
the course material and the tools developed in our computational laboratory.
The contents and the usage of the book is discussed below.
Contents of the book: The book is in two parts. The first part covers the
Python programming language in a reasonable detail, while the second part
contains the numerical algorithms and associated Python codes. The book has
discussion on all the numerical tools: interpolation, integration,
differentiation, solvers for ordinary and partial differential equations,
Fourier transforms, boundary value problems, linear algebra, and Monte
Carlo methods. In addition, I also include plotting tools, error analysis,
nondimensionalization, and an overview of computer hardware and software.
The computer programs in the book have been tested. For convenience,
the codes are available at the website https://sites.google.com/view/py-comp
. The website will also host affiliated material, such as PPTs, video lectures,
color figures, etc.
Usage of the book: This book is suitable for advanced undergraduate and
graduate students. It does not require any previous training in programming,
but it does require basic understanding of calculus and differential equations.
The material could be covered in 40 lectures at a fast pace.However, I
recommend that the instructor and students can skip topics that they find
complex and somewhat unnecessary.
Programming is learnt by practice. Hence, I strongly urge the students to
program enthusiastically, including the examples and exercise of the book. In
my course, we used Prutor for evaluating the exercises submitted by the
students. We plan to provide access to the exercises of the course on Prutor
(https://prutor.ai/mkvatiitkanpur/# ).
Acknowledgements: For the significant contents of the book, I am grateful to
the course TAs (teaching assistants), especially Soumyadeep Chatterjee,
Abhishek Kumar, Manohar Sharma. Special thanks to PHY473A’s
enthusiastic students, whose deep questions led to clarity in the presentation
in the book. I also thank Mani Chandra for introducing me to Python way
back in 2008, and to other students of Simulation and Modelling Laboratory,
where coding is done for fun.
Next, thanks to Python creator, Guido van Rossum, and many developers
for creating such a wonderful programming environment, that too for free! I
am grateful to Anancoda for the free Python distribution, and to the creators
of Scrivener that made writing this book a joy. The front cover was designed
using Canva software. I gratefully acknowledge Wikipedia and Wikieducator
for Figures 3, 4, 5, 33, and 138 of the book.
I am also thankful to my friends, Rahul Garg, Prachi Garg,
Harshawardhan Wanare, Rajesh Ranjan, Anurag Gupta for the
encouragement, and Manmohan Dewbanshi for help during the writing of this
book. Finally, special thanks to Nitin Soni and his publication team at
Adhyyan Books for the assistance in publication of this book.
Feedback request: I request the readers to send their comments and
feedback to me at [email protected] . Even though I have tried hard to make this
book free of errors, I am sure some lacuna still remain. I will be grateful if
such errors are brought to my attention.
Mahendra Verma
IIT Kanpur
CHAPTER ONE
Introduction
INTRODUCTION TO COMPUTING
FOR AN EFFICIENT use of a car, it is best to know some of its details: its
milage, power of the engine, nature of the brakes, etc. Similarly, an optimal
use of a computer requires knowledge about its memory capacity and power
of the processors. In this chapter, we provide a basic overview of a
computer and its components that are critical for numerical computations.
Figure 2: Inside Rome 7742 processor, black bidirectional arrows are buses
that transmit data both ways.
Memory: The data and programs reside in computer’s memory (the green
strip inside the motherboard of Figure 1), also called random access
memory (RAM). The CPU reads the program and data from RAM and write
the results back on it. Note that RAM is active only when the laptop is
powered on, hence it is a temporary memory. We need to write the data to the
hard disk for permanent storage.
A laptop or desktop has RAM in the range of 4 Gigabytes to 64
Gigabytes. Note that 1 byte = 8 bits, and
1 Kilobyte = 1 KB = 210 ≈ 103
1 Megabyte = 1 MB = 220 ≈ 106
1 Gigabyte = 1 GB = 230 ≈ 109
1 Terabyte = 1 TB = 240 ≈ 1012
1 Petabyte = 1 PB = 250 ≈ 1015
1 Exabyte = 1 EB = 260 ≈ 1018
The clock speed of RAM ranges from 200 MHz to 400 MHz, which is
slower than processor’s clock speed. The fastest RAM available at present,
DDR4 (double data rate 4), transfers data at the rate of 12.8 to 25.6
Gigabits/second, which is quite slow compared to CPU’s data processing
capability. Following strategies are adopted to offset this deficiency of
RAM:
1. The motherboard has multiple channels or roads for data travel between
the CPU and the RAM.
2. The CPU has its own memory called cache . The data which is needed
immediately is kept in the cache for a faster access.
Hard disk (HD) and Solid-state disk (SSD): HD and SSD are permanent
storage of a computer. Programs and data reside here safely after the
computer is powered off. When a computer is powered on, the required
programs and data are transferred from the HD to the RAM; this process is
called boot up . Hence, the CPU, RAM, and hard disk continuously interact
with each other.
A hard disk is an electro-magnetic device in which magnetic heads read
data from the spinning magnetic disks. In these devices, the data transfers rate
to RAM is 100-200 Megabytes (MB) per second. Due to the moving parts,
HDs are prone to failure, specially in laptops during their movements. In the
market, we can buy HDs with capacities ranging from 1 TB to 20 TB.
On the other hand, a SSD is purely an electronic device with no spinning
parts. Hence, SSDs are safer than hard disks, but they cost more. The data
transfer rate in SSD is around 500 MB per second. The capacity of SSD
ranges from 128 GB to 1 TB.
Graphical processing units (GPU): It is efficient to off-load video and
image processing to a specialised processor, called Graphical processing
units (GPU ). GPUs have many processors that perform certain operations,
such as matrix rotation and Fourier transform, very efficiently. In the last 15
years, GPUs are also being used for supercomputing. See Section
Supercomputers & Compute Complexity.
Conceptual Questions
Exercises
1. List the following for your laptop/desktop and your mobile phone: RAM
size, CPU clock speed, Hard disk, and/or SSD capacity.
2. It is best to see the parts of an opened-up desktop. However, do not
open your laptop because it is tricky.
3. Read more about the processors.
SUPERCOMPUTERS & COMPUTATION
COMPLEXITY
Example 1: We need to multiply two arrays A and B of sizes 104 x104 and
store the result in array C . For this problem we need 3 arrays of 108
elements each. Storage of 3x108 real numbers requires 8x3x108 = 24x108
bytes of storage, which is 0.24 GB. For 105 x105 arrays, the corresponding
requirement is 24 GB.
The simplest algorithm for multiplication of two N xN arrays requires
approximately N 3 multiplications and additions. Therefore, for N = 104 , we
need 1012 floating-point multiplications and additions. We estimate the peak
performance of a typical laptop with CPU with 4 cores to be 50 GFLOPs.
Hence, in the best case scenario, the 2x1012 floating-point operations would
require 2x1012 /(50x109 ) ≈ 40 seconds. Here, the prefactor 2 is for the
addition and multiplication.
The retrieval and storage the array elements from/to memory require
additional time. In addition, a laptop/desktop also performs other operations
such as system management, internet browsing, email checking, etc. The
processor works on these tasks in a round-robin and time-sharing manner.
Consequently, we expect that multiplication program to take much larger than
40 seconds. However, we do not expect the run to go much beyond several
(say 10) minutes.
For N = 105 , the time complexity will be 1000 x 40 seconds ≈ 666
minutes ≈ 11 hours. Hence, the space and time requirements for N = 105 are
respectively 24 GB and 11 hours that go beyond the capabilities of a typical
laptop.
Example 2: For weather prediction, a computer solves the equations for the
flow velocity, temperature, humidity, etc. For the same, Earth’s surface is
divided into a mesh, as shown in Figure 5. High-resolution simulations
employ grid resolution of 3 km x 3 km that leads to 12000x12000 horizontal
grid points. Suppose, we take 1000 points along the vertical direction, then
the total number of grid points for the simulation is 144x109 . At each grid
point, we store the three components of the velocity field, pressure,
temperature, humidity, and C02 concentration. Hence, to store these seven
variables at each grid point, we need 8x7x144x109 = 8.064 TB of memory,
which is way beyond the capacity of a laptop/desktop. Clearly, we need a
supercomputer for weather prediction. The estimation of time requirement
for a weather code is quite complex, and it is beyond the scope of this book.
Figure 5: For weather simulation, a sample grid on the surface of the Earth. Courtesy: Wikipedia
********************
Conceptual Questions
1. How are the supercomputers helping scientists and engineers in their
research?
2. How does one estimate the peak computational performance of a
supercomputer? Why don’t we achieve performance close to the peak
value?
3. Why is memory access a major bottleneck for supercomputers?
Exercises
1. Memory management
2. Process management
3. Management of input/output devices (keyboard, display, printer, mouse,
etc.)
4. Computer security
5. Management of application softwares (to be described below)
6. Interactions with users via input/output devices
7. Compilation and execution of user programs
The leading OS of today’s computers are Unix and Windows . MacOS , the
OS of Apple Computers, is a variant of Unix. Unix itself consists of many
programs, which are categorised into two classes: Unix Kernel and Unix
Shell . See Figure 6 for an illustration. Note that OS of mobile devices—
iOS, Android , and Windows —have limited capabilities.
Figure 6: A schematic diagram of various components of Unix OS.
Application Softwares
1. Get numbers A and B from the memory and put them into the CPU
registers.
2. Add the numbers and put result A+B into another register.
3. Transfer the result from the register to the memory.
In [1]:
x=3
In [2]:
x
Out[2]: 3
In [3]:
y=9
In [4]:
print(x+y)
12
In the above, the statements after In [1], In [2], In [3], and In [4] are typed by
the user, while Out [2] and the number 12 after In [4] are the response of the
interpreter. Note that the interpreter replies to the user instantly, unlike
compliers who respond to the user after executing the complete code.
In the next section we will provide an overview of Python programming
language.
********************
Conceptual Questions
(base) ~/python
Python 3.7.4 (default, Aug 13 2019, 15:17:50)
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on
darwin
Type "help", "copyright", "credits" or "license" for more
information.
>>> 2+3/4
2.75
>>>
In [1]:
2+3/4
Out[1]: 2.75
In this book we will deal with Numpy, Math, Matplotlib , and Scipy
modules extensively. To use these libraries, we need to import them. An
illustration of import operation is given below. Here, math module is
imported as ma.
In [7]:
ma.factorial(5)
Out[7]: 120
In [**1**]: ? plot
In [**2**]: ? sqrt
Creating a Python File Using an Editor
In [115]: run
sum_of_digits.py
In [116]:
sum_digits(128)
Out[116]: 11
Python vs. C
********************
Conceptual Questions
IN THIS SECTION, we describe some of the popular ways to install and run
Python.
Anaconda Python
Prutor
Prutor , a short form for Program Tutor , is an intelligent tutoring system for
programming. This platform, developed by Prof. Amey Karkare and his team
at IIT Kanpur/. Prutor is hosted at https://prutor.ai . See Figure 10 for an
illustration of a programming session on Prutor.
*******************
Excercises
Flows
Computers are used heavily for solving flow problems. Some of the leading
examples are
Human body: Medical scientists are studying heart, brain, blood flow,
etc. using computers.
Complex Physics
********************
Conceptual Questions
1. How are computers useful in science and engineering?
Exercises
1. Identify home appliances at your house and in your classrooms that rely
on computers.
2. Identify computer applications that have not been listed in this section.
3. Discuss some of the engineering applications where computers are
being used.
CHAPTER TWO
1. Integer
2. Floating point number
3. String
4. List
5. Arrays
6. Dictionary
Representation of Integers
(b N -1 b N -2 … b 2 b 1 b 0 )2 = b N -1 ⨉ 2N-1 + b N -2 ⨉ 2N-2 +…
+ b 2 ⨉ 22 + b 1 ⨉ 21 + b 0 ⨉ 20 , ….(1)
(h N -1 h N -2 … h 2 h 1 h 0 )2 = h N -1 ⨉16N-1 + h N -2 ⨉16N-2 +…
+h 2 ⨉162 +h 1 ⨉161 +h 0 ⨉160 . .… (2)
The decimal number (251)10 in hexadecimal is (FB)16 :
We employ decimal number system in our daily lives, but computers employ
binary number system. Hence, we need to convert one representation to
another. Conversion from binary to decimal, and from hex to decimal are
straightforward. We can employ Eq. (1) and Eq. (2) for these conversions.
The converse, conversion from decimal to binary, is performed as
follows. Suppose we want to convert (19)10 to binary. Since 16 < 19 < 32,
we write 19 = 16+3. Now 3 = 2+1. Therefore, (19)10 = 16+2+1 = (10011)2 .
Following similar process, we convert a decimal number to hex system. For
example, (251)10 = 16x15 + 11. Therefore, (251)10 = (FB)16 .
Python provides functions for binary, octal, and hex equivalents of a
decimal number. For example, the following functions provide binary, octal,
and hex equivalents to decimal number (100)10 .
In [18]: x=100
In [21]:
bin(x)
Out[21]: '0b1100100'
In [22]:
oct(x)
Out[22]: '0o144'
In [23]:
hex(x)
Out[23]: '0x64'
In the above code segment, 0b, 0o, and 0x represent respectively the binary,
octal, and hex representations. Using inverse functions, we can obtain
decimal representations of numbers in base 2, 10, or hex. For example,
Out[86]: 14
Integers in computers
In [4]:
x=12345678901234567890123456789
In [5]:
x
Out[5]: 12345678901234567890123456789
In [7]:
x*10
Out[7]: 123456789012345678901234567890
In the above example, the integer x contains 29 digits, which is beyond what
can be represented with 4 bytes or 8 bytes in C programming language.
The addition, subtraction, multiplication, and integer-division operators
in Python are +, −, *, and // respectively. The expression a b is evaluated in
Python as a**b with ** as the power operator. Note that the operator / is used
for real division (to be discussed in the next section). For example, 95//45 =
2, but, 95/45= 2.111111111111111.
Often we have expressions involving several integers and operators.
Under such situations, we follow the rules given in Table 2.
Note that
In [45]: 2**31-
1
Out[45]: 2147483647
In [46]:
5//3
Out[46]: 1
In [47]:
5/3
Out[47]: 1.6666666666666667
In [48]:
3+16//9//3
Out[48]: 3
Using the same method, we deduce that −1, −2 are stored as 1111 and
1110 respectively. See Table 3 for the full list.
[Table 3: 1’s and 2’s compliments of binary numbers from 0000 to 0111 with
4 bits. ]
a − b = a + (2N − b) − 2N .
That is, we add a and two’s complement of b , which is (2N − b ). The
subtraction of 2 N is trivially achieved by bit overflow.
********************
Conceptual Questions
Exercises
1. What are the largest and smallest signed and unsigned integers that can
be stored in 16-bit, 32-bit, 64-bit, and 128-bit machines?
2. Convert the following binary numbers to decimal numbers. Verify your
results using Python functions.
a. 11001 (b) 11111111 (c) 1001001
3. Convert the following decimal numbers to binary numbers. Verify your
results using Python functions.
a. 100 (b) 129 (c) 8192
4. Convert the following decimal numbers to hexadecimal numbers. Test
your results using Python functions.
a. 100 (b) 129 (c) 8192
5. Convert the following hexadecimal numbers to decimal numbers. Test
your answers with Python functions.
a. 1F (b) DD (c) F54 (d) 555
6. Assume a 1-byte storage for positive and negative integers. Compute 1’s
and 2’s compliments for the following binary numbers. What do these
numbers and their 2’s complement represent in decimal system?
a. 00101010 (b) 01111111 (c) 00001111
7. Perform the following subtraction using 2’s compliment and verify your
result: 01111111 − 00001111
8. Evaluate the following integer expressions in Python:
a. 9//2, 9/2, 4−6−3, 4−(6−3), 10+90//7, 4//3*7, 4*7//3, 9*2**3
9. Two server have 16GB and 96GB RAM. What are the respective RAM
sizes in decimal representation? How many integers can be stored in the
RAM of the servers?
FLOATING POINT AND COMPLEX
NUMBERS
(b n b n −1 … b 0 . b −1 … b − m )2 = (b n x2n + b n −1 x2 n −1 +…
+ b 0 + b −1 x2−1 + … + b − m x2− m ) . …(3)
Here, similar to the decimal system, the middle point, called binary point ,
separates the integer and fraction. For example, as shown in Figure 12(b),
(10.11)2 = 1x21 + 0x20 + 1x2−1 + 1x2−2 = 2 + 0 + 0.5 + 0.25
= (2.75)10 .
Similarly,
where b is the base. Note that we need sign bits for both fraction (called
mantissa) and exponent. For a unique representation, we need to make certain
rules for the mantissa. For example, for the decimal system, we may impose
a condition that the integer part is zero, and that the first digit after the
decimal must be nonzero. Hence, the number 0.540x10E is allowed, but
0.054x10E is not allowed; here E is the exponent. This is because 0.054x10E
is represented as 0.54x10E−1 .
With the above rule, in decimal system, the largest and smallest positive
numbers that could be represented using 2 digits each for mantissa and
exponent are 0.99x1099 and 0.10x10−99 respectively. Thus, the range of
numbers in floating point representation is much larger than that in fixed point
representation. Another important property of this number system is that for a
given exponent E, the gap between two consecutive real numbers is 0.1x10E ,
but the gap jumps by a factor of 10 when the exponent is incremented by 1.
Interestingly, the above multiscale feature is observed in nature. In the
animal kingdom, first comes viruses whose sizes are in nanometers, after
which comes bacteria, whose size is of the order of micrometers. Then
comes insects (of size cms) and mammals (of size meters). The sizes of the
animals within a class vary gradually, but there are jumps in the size when
we go from one class of animals to another. Physical systems too exhibit
similar multiscale structures. We start with nuclei (at femtometer) and then go
to atoms and molecules (nanometers), polymers (microns), …, objects for
daily use (meters), oceans and planets (1000 kms), stars (106 km), galaxies
(light years), and universe (109 light years). Thus, the floating point number
system is suitable for representing natural systems.
Binary system is used in computers. For floating-point binary numbers, a
constraint is imposed that the integer part of the mantissa is always 1. For
example, 1.1x 2−2 is a valid representation. However, 101.1x 2−2 and 0.0111x
2−2 are invalid; they can be written as 1.011x 20 and 1.11x 2−4 respectively.
With 5 bits each for mantissa and exponents (apart from sign bits), the largest
and smallest numbers representable in the binary system are 1.11111x231 and
1.00000x2−31 respectively; in decimal system, they translate to (1.96875)x231
and 1.0x2−31 respectively. Similar procedure can be used to represent
numbers in hex and octal number systems.
Next, we discuss the details of real number representation in a computer.
In [95]:
float.hex(1.5)
Out[95]: '0x1.8000000000000p+0'
In Out [95], `x’ stands for hex. The mantissa for (1.5)10 in binary is 100…
(with 51 zeros), which corresponds to 13 hex digits, 8000000000000. The
exponent is 0, as is evident from p+0. Similarly,
In [1]:
float.hex(15.0)
Out[1]: '0x1.e000000000000p+3'
In [2]:
float.hex(150.0)
Out[2]: '0x1.2c00000000000p+7'
In [3]:
float.hex(-150.0)
Out[3]: '-0x1.2c00000000000p+7'
In [5]:
float.hex(0.8)
Out[5]: '0x1.999999999999ap-1'
In [6]:
float.hex(0.375)
Out[6]: '0x1.8000000000000p-2'
In [7]:
float.hex(1/3)
Out[7]: '0x1.5555555555555p-2'
In [8]:
(1.1).hex()
Out[8]: '0x1.199999999999ap+0'
The above results are consistent with the fact that (15.0)10 = 1.111x23 . Thus,
mantissa is E in hex and it is written as e in Out[1]), while the exponent is 3.
Next, (150.0)10 = 1.0010 1100x27 with the mantissa as 2C, and the exponent
is 7. Note that +150.0 and −150.0 differ only in the sign bit.
The Python function, float.fromhex() is the inverse function, that is from
hex to float. For example,
In [4]:
float.fromhex('0x1.199999999999ap+0')
Out[4]: 1.1
In [5]:
float.fromhex('0x1.e000000000000p+3')
Out[5]: 15.0
In [165]:
math.inf
Out[165]: inf
In [170]:
math.nan
Out[170]: nan
In [154]:
float.fromhex(myzero)
Out[154]: 0.0
Since E = 2047 is reserved for the infinity, the largest positive floating-point
number has E = 2046 or exponent = 1023. Therefore,
fmax = (1.111…1)2 x 21023 = (1.FFFFFFFFFFFFF)16 x 21023
≈ (1.7976931348623157)10 x 10308
In [106]: float.fromhex('0x1.FFFFFFFFFFFFFp+1023'
)
Out[106]: 1.7976931348623157e+308
In [104]:
sys.float_info.max
Out[104]: 1.7976931348623157e+308
In [105]:
(sys.float_info.max).hex()
Out[105]: '0x1.fffffffffffffp+1023'
Note that 1023 x log 10 (2) ≈ 308. The smallest positive float represented in a
computer is computed in a similar manner. Since E = 0 is reserved for zero,
we take E = 1 for the smaller float number. Also, mantissa = 0 for this case.
Therefore,
fmin = (1.000 … 000)2 x 2−1022 ≈ (2.2250738585072014)10 x 10−308
The corresponding Python statements are
In [19]: float.fromhex('0x1.0p-
1022')
Out[19]: 2.2250738585072014e-308
In [106]:
sys.float_info.min
Out[106]: 2.2250738585072014e-308
In [107]:
sys.float_info.min.hex()
Out[107]: '0x1.0000000000000p-1022'
Also note that the numbers beyond the allowable range are either zero or
infinite, depending on whether the number is lower than the minimum number
or larger than the maximum number.
In [27]: print(1e308)
1e+308
In [28]: print(1e309)
inf
In [29]: print(3*1e308)
Inf
In [33]: print(1e-500)
0.0
The floating point operators along with their precedence are listed in Table
4. Keep in mind the difference between the integer division (//) and real
division (/).
The precedence rules for the float operators are given below. These rules are
similar to those for integer operators.
In [113]:
int(13.5)
Out[113]: 13
In [115]:
round(13.6)
Out[115]: 14
In [21]:
round(13.5)
Out[21]: 14
In [21]:
round(13.1)
Out[21]: 13
In [23]:
0.8*3
Out[23]: 2.4000000000000004
In [24]: 0.8*3-
2.4
Out[24]: 4.440892098500626e-16
In [25]:
1.5*3
Out[25]: 4.5
In [26]: 1.5*3-
4.5
Out[26]: 0.0
Computer-generated 3*0.8 does not match with exact value of 2.4. The
difference 0.8*3-2.4 is nonzero, and it is around 10−16 , which is the machine
precision. The above error is because 0.8 is not representable exactly in
binary system ((0.8)10 = (0.CC …)x ). On the contrary, 1.5*3 matches with its
exact value 4.5 because 1.5 is represented exactly in a computer.
For more precise calculations, special tricks are employed to achieve
better computing precision than 16 digits. For example, π has been computed
with precision of billions of digits using advanced mathematical tricks. In
Section Error Analysis we will discuss errors and precision in more detail.
We relate the precision of a computer to the finite number of states
representable by 64 bits. With 64 bits, we can represent 264 distinct numbers,
irrespective of schemes (fixed point, float point, or whatever). The rest of the
numbers can be stored only approximately. This is the origin of error in
floating-point arithmetics. Note that integers do not have round-off errors
because they are represented accurately, as long as they are within the limit,
e.g., −263 to 263 −1 for a 64-bit machine.
Complex Float
In [12]: z = 1.0+2.0j
√
where 1.0 and 2.0 are the real and imaginary parts of z, and j represents √-1.
The operations on complex numbers are illustrated using the following
Python statement. Here, * performs complex multiplication, while the
functions abs () and conj () return the absolute value and complex conjugate
of the complex number.
In [13]: x*x, x*2, abs(x), conj(x)
Out[13]: (2j, (2+2j), 1.4142135623730951, (1-1j))
With this we close our discussion on Python’s floats and complex numbers.
*******************
Conceptual Questions
Exercises
In [1]: x=8,
Out[2]: 8
In [3]:
x**3
Out[3]: 512
In [4]: print(x,
x**3)
8 512
In [6]:
x
Out[6]: 5.5
After the statement In [5], x is associated with the new number 5.5, which is
a real number. Note that data-type of a Python variable is not fixed; after
assigning a Python variable to an integer, it can be reassigned to a real
number, or a string, or an array (strings and arrays will be discussed in the
next chapter).
In Python, the integers, as well as other data types, are objects. The
location and size of the objects can be determined using the Python methods
(or functions), id() and sys.getsizeof() respectively. For example,
In [67]: x =
100
In [68]:
sys.getsizeof(x)
Out[68]: 28
In [76]:
id(x)
Out[76]: 4563550448
In [77]:
y=x
In [78]:
id(y)
Out[78]: 4563550448
The address of the object 100 is 4563550448. That is, 100 is stored at this
memory location. Note that after In [77], both the variables x and y are labels
to the same object 100, hence, they have the same address. In Section
Mutable and Immutable Objects in Python we cover this topic in more detail.
Note that the size of the data object 100 is 28 bytes. Since x ≤ 230 −1, 4
bytes are used to store x, while the remaining 24 bytes are used for storing
other attributes of the object. Note that the integers which are greater than 230
but less than 260 take 8 bytes to store. Larger integers require even larger
number of bytes.
We can clear all the Python objects of a python session using the reset
function:
In [10]:
reset
We remark that several Python statements can be written in a single line with
semicolons separating them. For example,
In the print statement, “x =“, “y = “, and “z =“ are strings that are printed as
they are.
In [28]: 5 <
6
Out[28]: True
In [29]:
5>6
Out[29]: False
In [33]: 5 ==
6
Out[33]: False
In [34]: 5 ==
5
Out[34]: True
In the above expressions, <, >, == are comparison operators that compares
two operands and return True or False. See Table 5 for a list of comparative
operator.
1. A and B yield True only if both the operands are True. Otherwise, the
result is False. For example, True and True = True.
2. A or B yield True if any one of the operands is True. A or B is False if
both A and B are False. For example, True or False = True.
3. A xor B is true if and only if its arguments differ from each other. For
example, True xor True = False.
4. not A is opposite of A . For example, not True = False.
Note that the logical operations are analogous to its English usage. Also note
that xor operator is not available in Python. Refer to Table 6 for the results of
these operators.
[
]
[Table 6: Truth table (T = True and F = False )]
In [40]: month =
5
In [41]:
is_summer
Out[41]: True
is_summer is a logical variable that takes value True when the month is May
(5th month), June (6th month), or July (7th month); it is False otherwise. After
this, we illustrate the and operator.
In [7]: angles_equal =
True
In [8]: sides_equal =
True
In [10]:
is_square
Out[10]: True
In [14]:
is_square
Out[14]: False
A quadrangle is a square if and only if all the sides, as well as angles, of the
quadrangle are equal. The boolean variable is_square is True when both the
conditions are True. The variable is_square is false when either of the two
conditions is false. The above example illustrates operator and .
After this, we illustrate the not operator. A person is well if he/she is not
sick. Hence is_well = not (is_sick ).
In [16]: is_sick =
True
In [17]: is_well =
not(is_sick)
In [18]:
is_well
Out[18]: False
In [96]: P =
True
In [97]: Q =
False
In [98]: P and
Q
Out[98]: False
In [99]: P or Q and
P
Out[99]: True
Out[100]: False
In [112]: bool(0),
bool(0.0)
Bitwise Operations
At the end of this section we discuss bitwise operations on binary bits. Since
1 and 0 are treated as True and False respectively, we can extend the logical
operations listed in Table 6 to binary numbers. For example, 1 & 1 = 1,
where & stands for the bitwise AND operator. The symbols used for other
binary operators are listed in Table 5.
We illustrate the above operations as follows:
Note that shift left by m bits yields multiplication by 2m , while shift right by
m bits yields integer division by 2m . Thus, we can perform fast
multiplication or division using bit-shift operators. Such tricks are used in
computers.
********************
Conceptual Questions
Exercises
1. In Ipython, type the statement: y = 89. Obtain the memory location where
89 is stored.
2. Given three logical variables, A = True, B=True, and C=False ,
evaluate the following logical expressions:
A and C, A or C, not C, A and B and C, A and B or C,
not A or B or C , bool (10.0), bool (0)
3. a. Suppose x = 5 and y = 10. What are the outputs of the following
logical expressions?
⁃ x < y, x <= y, x == y, x != y, x y, xy
4. State the results of the following bitwise operations:
7 & 9, 7 | 8, 7 ^ 9, ~7, ~9, 6 << 2, 6 >> 2
CHAPTER THREE
Characters in Python
In [1]:
x='2'
Out[2]: 50
In [36]:
chr(50)
Out[36]: '2'
Encoding/Decoding in Python
In [8]:
x
In [70]:
'SS'+'T'
Out[70]: 'SST'
In [71]:
5*'S'
Out[71]: 'SSSSS'
In [121]: y= "abs
\ncde"
In [123]:
print(y)
abs
cde
In [126]:
print('\u0905')
अ
In [132]:
print('\x55')
Note that 0905 (hex ) is a unicode representation for Hindi letter अ, and 55
(hex) is a representation of roman letter U.
Two more important issues related to the string data type are in order. To
continue a string to the next line, one can use \ to indicate a line break to the
interpreter. For example,
In [177]:
r'$\alpha$'
Out[177]: '$\\alpha$'
In [178]:
print(r'$\alpha$')
$\alpha$
This feature is useful while printing symbols using latex (see Section Plotting
with Matplotlib). Without r, the character a of alpha is not printed because \a
represents an empty character (a special character).
In [175]:
print('$\alpha')
$lpha
We can read a string from the keyboard using the function input ().
In [135]:
x
Out[135]: '10'
Note that input () returns a string. Hence, in the above statement, x is not an
integer. For the same, we need convert the input to an integer using int ()
function as follows:
In [139]:
x
Out[139]: 10
Note that the input () function need not have an argument, as shown below.
However, it is better to use a prompt, such “Enter the value of x =”, because
we want the interpreter to talk to us all the time.
In [25]: x =
input()
10
In [156]: print('x = ', x,'; ', 'y = ', y, '; ', 'z = ',
z)
x = 5 ; y = 10.0 ; z = hello
In the above example, the final output is a string containing all the objects
and separators.
In the above examples, {} are black boxes, whose values are filled using the
arguments of the format(). We can also specify which arguments of format()
fills which {}. For In [24], the indices 0 and 1 correspond to ‘Verma’ and
‘Vijay’ respectively.
In the above examples, the arguments are either strings or integer. We can
also have formatted floats with specified number of digits before and after
the decimal point. For example, {:2.2f} prints 2 digits before and after the
decimal point.
In [26]: print('Pi is
{:2.2f}'.format(12.3456))
Pi is 12.35
In [28]: print('Pi is
{:.2e}'.format(12.3456))
Pi is 1.23e+01
Conceptual Questions
Exercises
1. A book contains 5 million characters. Estimate the memory requirement
for storing this book in a computer.
2. Using python formatting, print floating point numbers to 5 significant
digits after decimal. Write them in both float and exponential formats.
3. What are the outputs of the print() function with the following Python
expressions?
2*’Hi’, ‘Hi’ + 2020, ‘Hi’+’2020’, 2*3*’Hi”, ‘Hi’ + ‘ friend’,
‘x’+’y’+’=5’, ‘$\alpha’, ‘$\beta’, r‘$\alpha’, r’$\beta’, ‘xyz\babc’,
'abc\axyz'
4. What is the output of the following Python statement?
In [75]: x=5; y=10; print('x+y’);
In [76]: print(r’$\alpha\beta$’)
In [77]: print(’$\alpha\beta$’)
In [78]: print(‘{:.3e}’.format(20.00159))
In [79]: print(‘{:2.3f}’.format(20.00159))
In [80]: print(‘{:1.3f}’.format(20.00159))
5. What are the differences between the following two Python statements?
x = input(“Enter the value of x =“)
x = float(input(“Enter the value of x =“))
LIST
A PYTHON LIST is an ordered set of objects. The ordered set implies that
the elements of the list have their assigned places and they cannot be
interchanged. Also note that the elements of a list could of different data
types.
For a list with N elements, the items of the list can be accessed using
indices 0, 1, .., N –1; or using indices –N , –N +1, …,–1. Note that the index
in Python starts from 0, and that the last element of the list can be accessed
using index N –1 or –1. See the following example and Table 9 for an
illustration.
In [187]: y = [1,2,'hi']
The functions associated with a list are given in the following table.
Usage of list functions:
In [192]: a = [1, 3, 5, 9,
15]
In [193]:
a.append(21)
In [194]:
a
In [196]:
a.insert(2,4)
In [197]:
a
In [199]:
a.reverse()
In [200]:
a
In [213]:
a
Two useful list functions of Python are size (a ) and len (a ). For one-
dimensional list, these functions yield the size of its argument, a.
In [90]:
len(a)
Out[90]: 7
In [91]:
size(a)
Out[91]: 7
Note that we can alter a list, i.e., change some of its elements. Hence, a list is
a mutable (or changeable) object. Consequently, the id of a list remains the
same after its elements have been altered. For example,
In [2]:
id(a)
Out[2]: 140602671466208
In [3]:
a[0]=500
In [4]:
id(a)
Out[4]: 140602671466208
Contrast these observations with those for integers and floats. When a
variable associated with an integer or a float was assigned to another
integer/float, its id changed as well. Also note that a string is an immutable
object, and hence its contents cannot be changed, as shown in the following
example. However, you may convert a string to a list using the list function,
and then change the contents of the list.
In [9]:
x='abcd'
In [11]:
x[0]='S'
In [12]:
x=list('abcd')
In [13]:
x
In [14]:
x[0]='S'
In [15]:
x
Out[15]: ['S', 'b', 'c', 'd']
Slicing
1. start : Start position of slice. This argument is optional, and its default
value is 0.
2. end : End position of slice. This argument is mandatory.
3. step : Step of slice. This argument is optional, and its default value is 1.
For example,
In [236]: s =
slice(1,4)
In [237]:
x[s]
Out[237]: [15, 9, 5]
In [238]: s = slice(1,4,2)
In [239]:
x[s]
Out[239]: [15, 5]
Note that index of a slice goes up to end −1. For example, In [237] lists x [1]
… x [3], not till x [4].
Python offers a shortcut to slice function. We can skip defining the
intermediate variable s , and directly apply x [start:end:step]. For example, x
[1:4:2] yields the same result as In [239]. That is,
In [169]:
x[1:4:2]
Out[169]: [15, 5]
In [228]:
x[1:4]
Out[228]: [15, 9, 5]
In [229]: x[1:-1]
# *j* = −1 or end−1, hence last item is
skipped.
Out[229]: [15, 9, 5, 4, 3]
In [232]:
x[:3]
In [233]:
x[4:]
Out[233]: [4, 3, 1]
In [234]:
x[4:-1]
Out[234]: [4, 3]
In [235]:
x[4::-1]
In [236]:
x[:-1]
Function range()
Note that range () does not return a list . It produces the integers on demand.
This is to ensue that range() can take a very large integer as an argument.
Note however that we can access the elements of range object by casting it
into a list .
Examples:
In [239]:
x=range(1,10,2)
In [240]:
x[2]
Out[240]: 5
In [241]:
list(x)
Out[241]: [1, 3, 5, 7, 9]
Python has a module called array , which is similar to list with a difference.
All the elements of array are of same data type, and they are stored at
contiguous memory locations. The allowed data types for array are signed
char, unsigned char, Py_UNICODE, signed short, unsigned short, signed
int, unsigned int, signed long, unsigned long, signed long long, unsigned
long long, float, double . We need to specify the data type code for them in
the function. See Table 11 for a list of data types along with their codes
(https://www.geeksforgeeks.org/python-arrays/ ).
Usage:
In [248]: import array as arr
In [250]:
a[0]
Out[250]: 1
In [251]:
type(a)
Out[251]: array.array
In [244]: y =
(1,2,3)
In [245]:
y[1]
Out[245]: 2
In [246]:
y.pop()
********************
Conceptual Questions
Exercises
1. Given a list y = [10, 20, 40, 60], what are the Python outputs for the
following list operations? For each operation, start with the original y.
y.pop(), y.append(10), y.reverse(), y.insert(2,3)
2. Given a list y = [10, 11, 12, 13, 14, 15, 16], what are the Python outputs
for the following slicing operations?
y [0:2], y [1:3], y [2:−1], y [:−1], y [::−1], y [:2], y [-1:3],y
[-1:3:-1]
3. What are the outputs of the following Python statements?
range(10), range(5,10), range(1,10,3), range(5,1,−1)
4. For a string s = “The Sun”, what are the outputs of s[0], s[2], s[3],
s[−1], s[-2]?
NUMPY ARRAYS
import numpy as np
from numpy import *
Or, more simply, import pylab by invoking the following command at the
terminal prompt (see Section Brief Overview of Python).
ipython --pylab
Python statements in this section and in some later parts of the book
correspond to either the later option or import using from numpy import \ *.
We meant to keep our discussion and syntax as simple as possible by
avoiding constructs such as np.array (), np.len ().
Among the numpy functions, array () creates an array. The method to
access array elements is same as that for a list .
In [265]:
y=array([5,9])
In [266]:
y[1]
Out[266]: 9
In [267]: y.dtype # yields data type of
y
Out[267]: dtype('int64')
Out[269]: 2
In [270]:
len(y)
Out[270]: 2
In [275]:
y=array([1.0,2])
In [276]:
y.dtype
Out[276]: dtype('float64')
We can employ real functions, e.g., sqrt, log, log10, sin, cos, on the array
elements. For example, for y of In [265].
In [448]:
sqrt(y)
Out[448]: array([2.23606798, 3. ])
The +, *, / operations on two arrays yield respective element-by-element
addition, multiplication, and division of the two arrays.
In [449]: z =
array([1.0,4.0])
In [450]:
y+z
In [451]:
y*z
In [452]:
y/z
In [271]: z =
y+3
In [272]:
z
For a numpy array a, the functions sum(a) provides the sum of all the
elements of the array, while prod(a) provides the product of all the elements.
These functions work for integer as well as float arrays.
In [7]: a=
[2,3,6]
In [8]:
sum(a)
Out[8]: 11
In [9]:
prod(a)
Out[9]: 36
Also note that the integer and float lists can be converted numpy arrays as
follows:
In [312]: y=
[3,4]
In [313]: array(y)
Out[313]: array([3, 4])
Similarly, we can convert a numpy array to a list using the list () function:
In [21]:
z=array([10.0,20.0])
In [22]:
list(z)
In [23]:
z
In [311]:
linspace(1,10,6)
In [317]:
arange(5.0)
Note that
In Numpy, int64 (8-byte integers) and float64 (8-byte floats) are default
integers and floats. In addition, Numpy offers arrays of the types listed in
Table 12. If our data consists of positive integers in the range 0 to 255, then it
is best to use uint8 that takes much less computer memory. Also, the
operations on uint8 are much faster than those on int64 . To create an int8
array x , we insert an argument dtype=‘u1' in the array function. See the
example below.
In [189]:
x=array([1,3,5],dtype=‘u1')
In [190]:
x.dtype
Out[190]: dtype(‘uint8')
To create numpy arrays with other data types, use the appropriate option
listed in the first column of Table 12.
Multi-dimensional Arrays
In [25]: x=array([[4,5,6],
[7,8,9]])
In [26]:
x
Out[26]:
array([[4, 5, 6],
[7, 8, 9]])
456
789
We can access the elements of a 2D array using two indices [i,j ] or [i ][j ].
A diagram representing the variations of the two indices of the array x is
shown below. Both the indices (for rows and columns) start from 0.
The first index represents the row, while the second index represents the
column. In Python, the data is stored in such a way that the column index
moves faster than the row index. For example, the elements of array x is
stored in sequence as [0,0], [0,1], [0,2], [1,0], [1,1], and [1,2]. Note that the
array elements can be also accessed as [0][0], [0][1], [0][2], [1][0], [1][1],
and [1][2]. This arrangement, called row-major order , is also followed in C
and C++. However, the array storage in Fortran follows column-major
order, that is, its elements are stored as [1,1], [2,1], [1,2], [2,2], [1,3], [2,3].
Note that the Fortran index starts from 1.
We illustrate the Python indexing using the following statement:
5, 5
In [301]:
x
Out[301]:
array([[0, 1],
[1, 0]])
Out[316]: -1.0
In [323]:
y=eig(x)
In [324]:
y
Out[324]:
(array([ 1., -1.]), array([[ 0.70710678, -0.70710678],
[ 0.70710678, 0.70710678]]))
In Out [324], y [0] = [1., –1] are the eigenvalues of x , while y [1] contains
two lists that are the associated eigenvectors of eigenvalues 1 and –1. Note
that y is a three-dimensional array, whose elements are accessed as follows:
In [101]:
y
Out[101]:
array([[[1, 2],
[3, 4]],
[[5, 6],
[7, 8]]])
In [102]:
y[1,1,1]
Out[102]: 8
The following numpy functions are used to extract size and shape of an array,
as well as create new arrays.
size (a, axis = None ): Returns the number of elements of array a along the
argument “axis” . However, size (a ) yields the total number of elements of
array a .
shape (a ): Returns array’s shape, which is a tuple whose entries are the
numbers of elements along the axes of the array.
zeros (shape, dtype=float ): Returns a new array of a given shape and type
filled with zeros.
ones (shape, dtype=float ): Returns a new array of given shape and type
filled with ones.
vstack (tuple ): Stacks numpy arrays along the first dimension (row-wise).
column_stack (tuple ): Stacks numpy arrays along the second dimension
(column-wise).
empty (shape, dtype=float ): Returns a new empty array of given shape and
type.
random.rand (shape ): Returns a float array of given shape with random
numbers sampled from a uniform distribution in [0,1].
random.randn (shape ): Returns a float array of given shape with random
numbers sampled from a Gaussian (normal) distribution with zero mean and
unit standard deviation.
random.randint(l ow , high=None, size=None, dtype=int ): Returns an
integer array of given size with random numbers sampled from a uniform
distribution in [low, high ). If high is None, the random numbers are sampled
from [0, low ).
concatenate (a1, a2, …): Concatenates arrays given as arguments. Its short
form is c_.
Examples:
In [357]: a =
ones((3,3))
In [358]:
a
Out[358]:
array([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
In [359]:
size(a)
Out[359]: 9
In [360]:
shape(a)
Out[360]: (3, 3)
We can use the function resize () to convert an array from one shape to
another.
In [361]: a =
ones((4,4))
In [362]:
a
Out[362]:
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
In [363]:
a.resize(2,8)
In [364]:
print(a)
[[1. 1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1. 1.]]
In [204]:
random.rand(3,2)
Out[204]:
array([[0.03303746, 0.2236998 ],
[0.20059688, 0.1309394 ],
[0.68722795, 0.54985967]])
In [207]: random.randint(2,5,
size=10)
Out[207]: array([3, 4, 3, 2, 2, 4, 3, 4, 2, 4])
In [1]: x =
array([1,2,3])
In [2]: y =
2*x
In [3]:
vstack((x,y))
Out[3]:
array([[1, 2, 3],
[2, 4, 6]])
In [29]:
column_stack((x,y))
Out[29]:
array([[1, 2],
[2, 4],
[3, 6]])
In [7]: a =
ones((4,4))*4
In [8]: c =
array([1,1,1,1])
Out[10]:
array([[4., 4., 4., 4., 1.],
[4., 4., 4., 4., 1.],
[4., 4., 4., 4., 1.],
[4., 4., 4., 4., 1.]])
In [1]: a =
ones((2,8))
In [2]:
a
Out[2]:
array([[1., 1., 1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1., 1., 1.]])
In [3]:
a.transpose()
Out[3]:
array([[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]])
Examples:
In [375]:
y
Out[375]:
array([[ 1. , 2.26666667, 3.53333333, 4.8 ],
[ 6.06666667, 7.33333333, 8.6 , 9.86666667],
[11.13333333, 12.4 , 13.66666667, 14.93333333],
[16.2 , 17.46666667, 18.73333333, 20. ]])
In [381]:
y[0,:]
In [382]:
y[0,0:2]
In [383]:
y[0,0:3:2]
In [405]:
y[0:2,1:3]
Out[405]:
array([[2.26666667, 3.53333333],
[7.33333333, 8.6 ]])
Examples:
In [418]:
y[ia,ja]
Out[418]:
array([[ 6.06666667, 2.26666667],
[12.4 , 8.6 ]])
In [1]: row_x =
linspace(0,0.3,4)
In [2]: col_y =
linspace(0,0.2,5)
In [3]: X,Y=meshgrid(row_x,
col_y)
In [4]:
X
Out[4]:
array([[0. , 0.1, 0.2, 0.3],
[0. , 0.1, 0.2, 0.3],
[0. , 0.1, 0.2, 0.3],
[0. , 0.1, 0.2, 0.3],
[0. , 0.1, 0.2, 0.3]])
In [5]:
Y
Out[5]:
array([[0. , 0. , 0. , 0. ],
[0.05, 0.05, 0.05, 0.05],
[0.1 , 0.1 , 0.1 , 0.1 ],
[0.15, 0.15, 0.15, 0.15],
[0.2 , 0.2 , 0.2 , 0.2 ]])
In [6]:
X[0,:]
In [7]:
Y[:,0]
Out[7]: array([0. , 0.05, 0.1 , 0.15, 0.2 ])
In [13]:
X1
Out[13]:
array([[0. , 0. , 0. , 0. , 0. ],
[0.1, 0.1, 0.1, 0.1, 0.1],
[0.2, 0.2, 0.2, 0.2, 0.2],
[0.3, 0.3, 0.3, 0.3, 0.3]])
In [14]:
Y1
Out[14]:
array([[0. , 0.05, 0.1 , 0.15, 0.2 ],
[0. , 0.05, 0.1 , 0.15, 0.2 ],
[0. , 0.05, 0.1 , 0.15, 0.2 ],
[0. , 0.05, 0.1 , 0.15, 0.2 ]])
In [16]:
X1[:,0]
In [17]:
Y1[0,:]
The x and y meshgrid would be more complex for a nonuniform mesh. The
function meshgrid () is useful for making surface plots and contour plots .
See Section Matplotlib & Field Plots for more details.
Vectorization
A processors can perform many operations in a single clock cycle if the data
is arranged consecutively in the memory. The speedup by this process is
significant for multicore processors and supercomputers. We illustrate this
idea using a simple example where we multiply two numpy arrays of same
size.
a = np.random.rand(10**8)
b = np.random.rand(10**8)
c=(a*b)
a = np.random.rand(10**8)
b = np.random.rand(10**8)
c = np.empty(10**8)
t1 = datetime.now()
c=(a*b)
t2 = datetime.now()
print (“for vectorised ops, time = “, t2-t1)
t1 = datetime.now()
for i in range(10**8):
c[i] = a[i]*b[i]
t2 = datetime.now()
print (“for loop, time = “, t2-t1)
In my MacBook Pro 2014 model, vectorised array multiplication took 2.10
seconds, while the multiplication by an explicit loop took 20.06 seconds.
Thus, vectorised array multiplication is approximately 10 times faster than
the loop-based array multiplication. We remark that we could also capture
time using timeit module:
import timeit
t1 = timeit.default_timer()
********************
Conceptual Questions
1. What are the differences between the features of a Python list and a
Numpy array?
2. Contrast the array access methods of Python, C, and Fortran.
Exercises
In [3]:
myclass['Rahul']
Out[3]: 101
In [9]:
var['x']
Out[9]: 4533028272
1. The keys of a dictionary are unique, but the values are not. Note that
values may repeat in a dictionary.
2. Dictionaries are unordered, and hence the items of a dictionary cannot
be accessed using indices. Rather, the value of an item is accessed using
its key.
3. Dictionaries are changeable. That is, we can delete an entry of the
dictionary; modify an element corresponding to a key; etc. Note that the
keys cannot be changed, but they can be deleted.
In [12]: myclass['Radha'] =
104
In [13]:
myclass
In [15]: del
myclass['Rahul']
In [16]:
myclass
Out[21]: 'good'
In [23]:
french_eng
In [10]:
sqr
********************
Conceptual Questions
1. What are the key differences between Numpy arrays and Python
Dictionary?
Exercises
In [32]: x =
10
In [33]:
id(x)
Out[33]: 4477371824
In [33]:
id(10)
Out[33]: 4477371824
In [34]:
id(x)
Out[34]: 4477371824
In [35]: id(11)
Out[35]: 4477371856
In [36]:
sys.getsizeof(10)
Out[36]: 28
In [38]:
id(y)
Out[38]: 4477371824
In [39]: x =
100
In [40]:
id(x)
Out[40]: 4477374704
In [52]:
id(100)
Out[52]: 4477374704
In [53]:
id(y)
Out[53]: 4477371824
int main() {
int x =10;
int y;
int *px, *py;
px = &x;
y = x;
py = &y;
x = 100;
}
In the examples discussed earlier, the objects 10, 11, and 100 cannot be
altered. Such objects are called immutable objects . In fact, among the
Python data types, integer, float, complex , string, and tuples are immutable.
The value of an immutable object is unchangeable once it has been created.
However, lists and arrays are mutable objects because their contents can be
altered (discussed later in this section). In the following discussion we will
discuss the subtleties of immutable and mutable Python objects.
In [147]:
x=5
In [148]:
print(id(x))
4540589328
In [149]: x =
x**2
In [150]: print(x,
id(x))
25 4540589968
In [152]: a, b =
5,6
In [153]: print(id(a),
id(b))
4540589328 4540589360
In [156]: print(a, b,
tmp)
6 5 5
In [303]: a, b =
5,6
In [304]: print(id(a),
id(b))
4540589328 4540589360
In [305]: b,a =
a,b
In [306]: print(id(a),
id(b))
4540589360 4540589328
In [307]:
a,b
Out[307]: (6, 5)
Lists and arrays are mutable objects, that is, these objects can be altered
after their creation. We illustrate their mutability using the following
examples.
In [1]: a =
[1,2,3]
In [2]:
b=a
In [3]: print(id(a),
id(b))
4630230512 4630230512
In [4]:
a.pop(1)
Out[4]: 2
In [6]: print(a,
b)
[1, 3] [1, 3]
In [7]: print(id(a),
id(b))
4630230512 4630230512
In the above statements, the variables a and b point to the list [1,2,3], and
they have the same address (4630230512). In statement In [4], we pop the
element 2 from the list. Consequently, both a and b refer to the new list [1,3].
Note that the addresses of a and b remain the same after pop (see In[7]). See
Figure 20 for an illustration. Contrast the above feature with the earlier
example where the address of x got changed on reassignment.
We create a copy of a using the statement:
In [8]: c =
a[:]
In [88]: a =
[5,6]
In [166]: x =
array([1,2,3])
In [167]:
print(id(x))
140562475313552
In [168]: x =
x**2
In [169]: print(x,
id(x))
[1 4 9] 140562590651216
The operation x **2 creates the array [1, 4, 9], which is a new object.
Therefore, id ( x ) changes to a new address after the execution of In [168].
Similar behaviour is observed for other array functions, such as sin ( x ), cos
( x ).
Example 5: The following code exchanges two lists a and b . The exchange
process is exactly same as that for Example 2.
In [160]: print(id(a),
id(b))
140562475473008 140562569714096
In [163]: tmp = a; a = b; b =
tmp
In [164]: print(a, b,
tmp)
[10, 20] [1, 2] [1, 2]
********************
Conceptual questions
Exercises
In the present section and next two sections, we will cover these
structures along with simple example. We start with sequential statements.
In the following sequential code segment, a set of Python statements are
written one after the other. These statements are separated by semicolons (;).
Python interpreter executes these statements one after the other.
In [482]: x=50; \
...: y=100; \
...: print(x,' '
,y)
50 100
Exercises
In [491]: x =
3
x is odd
In [493]: x=2
x is even
In [37]: yr = 1984
In [37]: yr = 1984
In [39]: if (yr%400 == 0):
...: print('A leap year')
...: elif (yr%4 ==0) & ~(yr%100==0):
...: print('A leap year')
...: else:
...: print('Not a leap year')
...:
A leap year
Exercises
for loop
Example 1: The following Python code prints the names of all students in the
student_list .
Rahul
Mohan
Shyam
Shanti
Figure 24 illustrates the control flow of the for loop. The loop is carried
out for all students in student_list. We start with index = 0, and continue till
the student_list is exhausted. Note that the initialisation of index and
increment of indices are not required in the code segment; they are performed
by the Python interpreter internally.
In [510]: n = 5
...: fact=1
...: for i in range(2,n+1):
...: fact = fact*i
...:
Factorial of 5 = 120
Here, range (2,n +1) creates a list containing integers 2 to n (excluding n
+1). Hence, for loop is carried out for i ranging from 2 to n .
In [42]: A = [6, 0, 4, 9, 3]
In [43]: max = -inf
In [44]: for x in A:
...: if (x > max):
...: max = x
In [45]:
max
Out[45]: 9
In this example, we initialise max to −inf (−∞). After that we loop over all
the elements of A. If the element > max , then we replace max with the new
element. In the end, max contains the maximum number of the list.
Another way to compute the maximum is as follows:
In [43]: max =
A[0]
In [46]:
max
Out[46]: 9
Here, we start with max = A [0], and loop from A [1] to the end of the array.
As before, max is update with the new element if it is larger than the current
max . This is an index-based solution.
Example 4: In an integer array, send the maximum element to the end of the
array.
n [46]: A = [6, 0, 4, 9,
3]
In [48]:
A
Out[48]: [0, 4, 6, 3, 9]
In this example, among A [i ] and A [i +1], the larger number is pushed to the
right. The loop is carried out from i =0 to n −2, where n is the length of the
array A. The above process is similar to the bubbling operation in which the
lightest element rises to the top.
while loop
In [514]: n = 5
...: fact = 1
...: i = 2
...: while (i <= n):
...: fact = fact*i
...: i = i+1
...:
The while loop is executed until the running index i <= n . The loop
terminates as soon as i = n +1.
In [526]: while b:
...: a, b = b, a%b
...:
In [527]:
print(a)
23
In the above algorithm, b < a . The loop is continued till b = 0. Within the
loop, a ⟵ b and b ⟵ a%b. Refer to wikipedia for details on Euclid’s
algorithm.
It is mandatory that all loops in a program terminate. If not, the loop will
continue forever. Such a loop, called infinite loop , must be avoided at all
cost. An example of infinite loop is given below.
In [54]:
i=1
You can easily check that the above loop does not terminate. You will have to
kill the program using Cntrl-C (control-C). We may wish for a system
software that can detect an infinite loop in any program; this is called halting
problem. Unfortunately, using a mathematical proof, Turing showed that it is
impossible to write such a program. This celebrated theorem is important an
construct in theoretical computer science.
A loop can be broken using a break statement. For example, we can
break the above infinite loop using the following break structure.
In [263]: i=1
In [266]: while (i>0):
...: i = i+2
...: if (i>100):
...: break
Python allows usage of else after for or while loop. The else block is
executed when the loop finishes normally. We illustrate this structure using a
code that tests if number n is a prime or not. In this program we use a
property that one of the factors of a prime number must be less than or equal
to √n.
n=15
Functions in Python
FUNCTIONS IN PYTHON
def factorial(n):
fact = 1
for i in range(2,n+1):
fact = fact*i
return fact
*Usage:*
In [**16**]: factorial(8)
Out[**16**]: 40320
In [**17**]: factorial(9)
Out[**17**]: 362880
Example 2: A function that returns the sum the digits of an integer. Here, str
(n ) converts integer n to a string of digits, while int (k ) converts digit-
characters (e.g., ‘1’) to the corresponding integers.
import numpy as np
# Returns sum of the digits of n
def sum_digit(n):
digits_n = list(str(n))
# list of digits of n [Char entries]
my_sum = 0
for k in digits_n:
my_sum += int(k)
return my_sum
*Usage:*
In [94]:
sum_digit(456)
Out[94]: 15
my_max = -inf
for x in A:
if (x > max):
my_max = x
return my_max
*Usage:*
In [92]: a=[5, 0, 9,
4]
In [93]:
max_array(a)
Out[93]: 9
def is_harshad(n):
return not(n % sum_digit(n))
*Usage:*
In [95]:
is_harshad(21)
Out[95]: True
def is_armstrong(n):
digits_n = list(str(n))
# list of digits of n [Char entries]
sum_cube = 0
for k in digits_n:
sum_cube += int(k)**3
return (sum_cube == n)
*Usage:*
In [96]:
is_armstrong(153)
Out[96]: True
In [97]:
is_armstrong(154)
Out[97]: False
def is_palindrome(x):
return(x == x[::-1])
Usage:
In [100]:
is_palindrome('ABCD')
Out[100]: False
In [101]:
is_palindrome('ABCCBA')
Out[101]: True
In [141]:
func(2,mysqr)
Out[141]: 4
In [142]:
func(2,mycube)
Out[142]: 8
Lambda Function
In [59]: my_deriv(my_sqr, 1,
0.1)
Out[59]: 2.0000000000000004
A simpler version using lambda function is as follows. In this version, we
can skip the definition of my_sqr().
Here, the quad() function integrates the lambda function lambda x: x in the
interval 0 and 1.
Examples 7 and 8 illustrate the power of lambda function. In the next section,
we will discuss namespace and scope of variables.
********************
Conceptual Questions
Exercises
Namespace in Python
In [65]:
outer()
1
10 100
Module Import
To use Python modules, we need to import them, which can be done in three
ways:
import module : This way we make the module available in the current
program as a separate namespace . Here, we need to refer to a function of
the module using module.function . For example,
In [2]: math.factorial(5)
Out[2]: 120
from module import item : Here, the item of the module is referred to in the
namespace of the current program. For example,
In [4]: factorial(10)
Out[4]: 3628800
import module as alias: For convenience, the module is renamed using an
alias. For example,
In [7]:
ma.factorial(5)
Out[7]: 120
...: x = 1 #local
...: print(x)
...:
...: inner()
In [65]:
outer()
1
Note that different namespaces or scopes (global, enclosing, local) segregate
the variables so as to clearly differentiate them. The Python interpreter first
searches for the variable inside a function. If it isn’t available, then the
interpreter searches for the variable in the enclosing function’s scope. After
that, the interpreter looks for the variable in the global scope. At last, the
interpreter searches for the name in built-in scope. If the variable isn’t
available even after that, an error message is flashed.
After this, we discuss how arguments are passed to Python functions.
In [91]: a =
100.0
In [92]:
id(a)
Out[92]: 4846932752
In [93]:
test_fn(a)
inside function, step 1: 100.0 4846932752
inside function, after x = 999.0: 999.0 4846935088
In [94]: print(a,
id(a))
In [216]: b=
[1,2,3]
In [218]:
id(b)
Out[218]: 140562590748992
In [219]:
change_array(b)
In [6]: a = 5
In [209]:
id(a)
Out[209]:
4540589328
In [234]:
my_sqr(a)
Global variables
A Python variable that is declared in the main program (outside functions) is
called a global variable (see Figure 27). Such variables have global scope .
In Python, the global variables have the following properties.
In [262]: count = 0
In [266]:
f()
count = 0
In [268]: f()
UnboundLocalError: local variable 'count' referenced before
assignment
Example 4: In the following code, count is passed as a parameter to f ().
In [274]:
count=0
In [275]:
f(count)
In [277]: print(count,
id(count))
0 4540589168
At the start of the function, count is a global variable. However, after the
execution of the statement, count = count +1, the variable count points to a
new object, which is 1. The variable count (1) now becomes a local
variable, and the above increment has no effect on the global variable count .
Look at the id (count ) at various stages.
def swap_not_working(a,b):
temp = a
a = b
b = temp
print (a, b, temp, id(a), id(b), id(temp))
return a, b
In [323]:
a
Out[323]: 5
In [324]:
b
Out[324]: 50
In [325]: id(a),
id(b)
Out[325]: (4453578000, 4453579440)
In [329]:
swap_not_working(a,b)
The latter statement passes function’s a and b (that are swapped) to global
variables a and b.
Another version (simpler one) of swap function that works is given
below.
# Swap a and b
def swap(a,b):
return b,a
Usage: a , b = swap(a,b)
In [308]: a, b = b,a
a[:] = b
# copies elements of b into a
b[:] = temp
# copies elements of tmp to b
In [365]: a =
[1,2]
In [366]: b =
[10,20]
In [367]: id(a),
id(b)
In [369]:
swap_list(a,b)
In [371]: a, b, id(a),
id(b)
Out[371]: ([10, 20], [1, 2], 140716150723760,
140716130405904)
In the function swap_list ( a, b ), the lists temp and a behave differently than
the previous swap example because list is mutable.
In [47]: b
= array([1,2,3])
In [48]:
id(b)
Out[48]: 140708012226560
In [50]:
print(my_sqr(b))
[1 4 9] 140708036299952
[1 4 9]
In [51]: print(b,
id(b))
[1 2 3] 140708012226560
********************
Conceptual Questions
1. What are the differences between Python’s local and global variables?
2. Arguments to a Python function could be immutable and mutable
objects. What are their behavioural differences?
Exercises
1. Execute all the codes given in this section and verify the results.
2. What are the outputs of the print() in the following code segment?
def mult(a,b)
a *= b
return(a)
X = 3
mult(x,4)
print(x)
a=np.array([4,8])
print (mult(a,4))
print(a)
RECURSIVE FUNCTIONS
def factorial(n):
if (n==1):
return 1
else:
return n*factorial(n-1)
It works as follows:
factorial(3) = 3*factorial(2),
factorial(2) = 2*factorial(1),
factorial(1) = 1.
*factorial*(2) = 2*1 = 2,
*factorial*(3) = 3*2 = 6. (Answer)
The above recursive implementation of factorial (n ) yields the same result
as in Example 1 of Section Functions in Python. However, the recursive
function is quite expensive because it makes forward calls to itself, and then
performs reverse substitutions. Each function call requires significant
bookkeeping (e.g., saving local variables at each stage).
Recursion does not provide any real benefit in the implementation of
factorial function. However, some problems are much easier to solve using
recursion than using iteration. One such problem is Tower of Hanoi .
def tower_of_hanoi(n,source,dest,intermediate):
if (n==1):
print("transfer disk ", source , " dest ", dest)
else:
tower_of_hanoi(n-1, source, intermediate, dest)
print("transfer disk ", source , " dest ", dest)
tower_of_hanoi(n-1, intermediate, dest, source)
In [2]: run
tower_of_hanoi.py
In [3]:
tower_of_hanoi(3,"A","B","C")
transfer disk A to B
transfer disk A to C
transfer disk B to C
transfer disk A to B
transfer disk C to A
transfer disk C to B
transfer disk A to B
Figure 31: Illustrations of the three steps for solving the tower of Hanoi
problem.
Based on the steps of the algorithm, we can estimate the number of moves
required to transfer n disks. If N (n ) is the total number of moves to transfer
n disks, then the recursive definition yields the following relation between N
(n ) and N (n −1):
N (n ) = 2 N (n −1) +1 …(!eq(toh))
and N (1) = 1
We attempt solution of the form N (n ) = a n + b. The condition N (1) = 1
yields a+b =1. Substitution of N (n ) = a n + b in Eq. (#eq(toh)) and usage of
a+b=1 yields
a n = 2 a n -1 + 2 − a.
For n = 2, the above equation becomes a 2 = a + 2, whose solutions are a =
−1 and 2. The former solution is invalid because N (n ) is an increasing
function of n . Therefore,
N (n ) = 2n −1, …(!eq(toh_soln))
which is a valid solution of Eq. (#eq(toh)). Thus, N (n ) = 2n −1 is the time
complexity of the above algorithm.
We remark that Tower of Hanoi has an iterative solution as well.
However, it is much more complex to implement. The recursive solution is
very intuitive and straight forward.
The code that produces the Sierpinski triangle with fractal degree of
fractal_degree is given below. It employs turtle module of Python. The turtle
could move forward by a distance d (forward (d)), turn left by any angle θ
(left (θ)), turn right by an angle ζ (right(ζ)), go to a point (x,y) (goto (x,y )),
and perform several other tasks. Refer to the turtle manual for more details.
import turtle
colormap = ['red','green','blue',
'white','yellow','violet','orange']
sierpinski([vortex_coords[1],
mid_point(vortex_coords[0], vortex_coords[1]),
mid_point(vortex_coords[1], vortex_coords[2])],
fractal_degree-1, fractalTurtle)
sierpinski([vortex_coords[2],
mid_point(vortex_coords[2],vortex_coords[1]),
mid_point(vortex_coords[0],vortex_coords[2])],
fractal_degree-1, fractalTurtle)
def main():
fractalTurtle = turtle.Turtle()
Win = turtle.getscreen()
vortex_coords = [[-200,-100],[0,200],[200,-100]]
sierpinski(vortex_coords,3,fractalTurtle)
Win.exitonclick()
main()
The above code appears quite complex, but it is not so. Some of the key
features of the code are as follows.
1. The centre of the largest triangle is at the origin, while its three vortices
are at (−200,−100), (0,200), (200,−100). The vortices are stored in the
array vortex_coords . The vortices of the inner triangles are computed
iteratively.
2. In main(), we create fractalTurtle using the function turtle.Turtle ();
this fractalTurtle is passed as an argument to the functions sierpinski ()
and drawTriangle () because the same turtle moves in these functions.
3. Using the array colormap , we set the colours of the triangles of levels
3, 2, 1, 0 as white, red, blue, and green respectively. Note that level n
triangles are at the top of level n −1 triangles, hence only the remnants
of the earlier triangles are visible in the figure.
4. The color-filling of the triangles is achieved by the functions fillcolor
(), begin_fill (), and end_fill (). The colors are filled after the triangles
have been drawn.
Since the turtle moves on turtle console, the above code cannot run on
Ipython . It is best to run this code on Spyder that supports turtle console.
Example 4: Pingala-Virahanka-Fibonacci (PVF ) sequence is defined
recursively as follows:
f (n ) = f (n −1) + f (n −2)
with f (0) = 1 and f (1) = 1. Using this definition, we compute f (2) onwards
as 2, 3, 5, 8, and so on. Asymptotically (for large n ), f (n ) = C a n with a
satisfying
a 2 = a + 1,
whose solution is a = (√5+1)/2, which is called golden ratio .
The Pingala-Virahanka-Fibonacci sequence is used to describe growth
models. For example, as shown in Figure 33, the numbers of petals in
flowers are PVF numbers. Also, the growth of the flowers and shells follow
the PVF spiral, which is shown in the first subfigure of Figure 33. See
https://en.wikipedia.org/wiki/Fibonacci_number for more details.
Figure 33 : Illustrations of Pingala-Virahanka-Fibonacci sequence and
spirals in nature. From https://wikieducator.org/images/f/f1/Fibonacci.png .
Credits: wikieducator.
The following recursive code generates the Pingala-Virahanka-Fibonacci
sequence:
def pingala(n):
if (n==0):
return 1
elif (n==1):
return 1
else:
return (pingala(n-1)+pingala(n-2))
Usage:
In [5]:
pingala(3)
Out[5]: 3
In [6]:
pingala(4)
Out[6]: 5
Recursion is a powerful tool for many computational problems, e.g. binary
search, Fast Fourier Transform, etc. However, majority of scientific
algorithms employ iterative solutions because they are faster than their
recursive counterparts.
With this we end our discussion on recursive functions.
********************
Conceptual questions
Exercises
1. Think about the problem at hand before you start to program. In fact,
first solve the problem, and then write an algorithm, after which start
to code .
2. A computer has a fast processor, but it is dumb. It has to be told exactly
what is to be done. A step-by-step process to arrive at the final solution
is called an algorithm . It is important to write down an algorithm
before we start to code. This way, we arrive at a correct and error-free
code.
3. Think simple! Simple solutions are easy to code and easy to explain to
colleagues. Simple codes often yield efficient results, and they are easy
to modify. KISS—Keep it simple, stupid !—is an important proverb in
computer programming. Follow this guideline! When you get back to
your code after a year, you should be able to understand it without
strain.
4. Think carefully about extreme cases. For example, your search program
should work even when the item is not in the list.
5. Choose variable names wisely. For example, use mass as a variable
name to denote the mass of a particle. Do not use x for mass! Your code
should be readable.
6. Follow consistent choice of variables. For example, use i , j, k for loop
variables consistently.
7. Comment your code, but avoid excessive commenting.
8. Computer programming is an art. Write beautiful codes! Of course,
mastering this art takes a lot of effort, practice, and patience.
For further guidance on the art of programming, refer to the following books:
**function** is_prime(n):
**for** i: 0 to √n
if (n%i == 0):
n is non-prime; return
**else** # after the loop
n is indivisible by a factor.
Declare n to be prime; return
Code:
import numpy as np
# Test if i divides n.
# i ranges from 3 to sqrt(n) in steps of 2.
for i in range(3, int(np.sqrt(n))+1, 2):
if (n%i == 0):
return 0
else:
return 1
Code:
import numpy as np
def prime(n):
nos = list(range(2,n+1))
# Sieve containing integers from 2 to n
prime_index = 0
prime_now = nos[prime_index]
# prime_now = 2 is the first prime
Algorithm:
prime_no = [2,3,5, …]
function prime_factor(n):
factor = [] # empty arrays
i=0
while (n > 1):
loop:
For prime(i),
if (prime[i] divides n):
n ← n/prime[i]; Append prime[i] to factor.
increment i
return factor
Code:
import numpy as np
return factor_array
Exercises
Many search algorithms are quite complex. Google has become world
famous and very rich due to their sophisticated search algorithms. Such
complex schemes are beyond the scope of this book; here, we present two
elementary search algorithms.
As we show below, search in sorted and unsorted arrays have different
time complexities. Note that a sorted array is one in which the elements
(numbers) are arranged either in ascending or descending order. We describe
the respective algorithms below.
An unsorted array has no pattern. Hence, we need to look for the element to
be searched in the whole array one by one. Algorithmically, it is prudent to
start searching from the beginning of the array and continue to search till the
element is found. This algorithm is called a linear search . The following
code performs a linear search of element x in array A . The function returns
the array index of x. The linear search process is illustrated in Figure 34.
Algorithm:
Code: The function returns the array index of x. It returns −1 if x does not
belong to A .
for i in range(len(A)):
if (A[i] == x):
return i
*Usage:*
In [382]: A = ['Rohit', 'Sita', 'Ahilya',
'Laxmi']
In [383]: linsearch(A,
'Sita')
Out[383]: 1
In [384]: linsearch(A,
'Ahilya')
Out[384]: 2
In [385]: linsearch(A,
'Jaya')
Out[385]: -1
How many comparisons are required in the above search operation? The
best and worst scenarios require 1 and n comparisons respectively. It is easy
to verify that the average number of comparison is n/2. Hence, the time
complexity of linear search algorithm is O(n), where O stands for “order of”.
Figure 34: Linear search of an element x in list A = [Rohit, Sita, Ahilya,
Laxmi]. Here, Y and N represent yes and no .
It is faster to search for an element in a sorted array. The algorithm makes use
of the sorted nature of the array. Let us we assume that the integer array A is
sorted in an ascending order.
Idea: First we locate the mid index of the array and denote it by mid . If x =
A [mid ], then search is complete. If x < A [mid ], then we search in the left
half of the array, else we search in the right half of the array. The above
procedure is recursively applied to the subarrays (left half or right half). We
continue till we succeed. In case of failure, return −1 indicating that the
element is not in the list. This procedure is called binary search .
Code: The function returns the array index of x. It returns −1 if x does not
belong to A .
*Usage:*
In [72]: A = [1,3,7,8,11,15,20,25,30]
In [73]:
binsearch(A,11)
Out[73]: 4
In [74]:
binsearch(A,35)
Out[74]: -1
In [75]:
binsearch(A,9)
Out[75]: -1
mid = (lower+upper)/2
if ((x < A[lower]) | (x>A[upper])):
return -1
elif ((lower==upper) & (A[mid] != x)):
return -1
else:
if (A[mid] == x):
ans = mid
elif (x < A[mid]):
ans = binsearch_recursive(A,lower,mid-1,x)
else:
ans = binsearch_recursive(A,mid+1,upper,x)
return ans
Usage:
In [76]:
binsearch_recursive(A,0,8,11)
Out[76]: 4
for i in range(n):
# A[n-i-1:n-1] is sorted.
# So go only up to n-i-2.
for j in range(0, n-i-1):
*Usage:*
In [76]: run bubble_sort.py
In [77]: A = [8,7,3,10,6]
In [78]: bubble_sort(A)
In [79]:
A
Figure 36: Sorting of array A = [8,7,3,10,6] using bubble sort. The shaded
list indicates the numbers that have been sorted.
Exercises
1. Compare the time complexities (number of comparisons) of linear and
binary searches for n = 16, 256, 4096, 230 .
2. How many comparisons are required to bubble sort an array of size n ?
3. Given an integer array, write a Python function that prints the fourth
largest integer of the array.
4. Write a Python function that sorts an integer array in descending order.
5. Consider a list of strings, for example, A = [“Python”, “C”, “Java”,
“Pascal”]. Write a computer program to sort A .
CHAPTER SEVEN
Plotting in Python
MATPLOTLIB & FIELD PLOTS
Using Pyplot
$ipython --pylab
We employ the following Python statements to plot sin(x) for x = [0, 2π].
In [114]:
x=linspace(0,2*pi,100)
In [115]:
y=sin(x)
In [118]:
savefig("test.pdf")
We save the plot in the file test.pdf using the function savefig(). We
display the plot in Figure 37. Note that Python allows the plot to be saved in
png, jpg, and svg formats as well.
It is much more convenient to put the axis labels, legends, etc. in a file
and then run the file in Ipython. It is easier to tinker some Python statements
in the file, rather than modify the features interactively in Ipython. In the
following, we present contents of a file that plots y = x2 and y = x3 .
import numpy as np
import matplotlib.pyplot as plt
from pylab import rcParams
rcParams['figure.figsize'] = 5, 3
# figure of the size 5in x 3in
x = np.linspace(-1,1,40)
y = x**2
y1 = x**3
plt.axhline(0, color='k')
plt.axvline(0, color='k')
# Axis properties
plt.xlim(-1,1)
plt.ylim(-1,1)
# Limits of x and y axes
plt.xlabel(r'$x$', fontsize=20)
plt.ylabel(r'$y$', fontsize=20)
# Axis labels
plt.xticks(np.linspace(-1, 1, 5, endpoint=True))
# Ticks on the xaxis.
# 5 points in the interval (-1,1).
plt.legend(loc=4)
# Legends appear at the fourth quadrant,
# which is the bottom right
plt.tight_layout()
# Helps in fitting plots within
# the figure cleanly
plt.savefig('plot2d.pdf')
# Save the plot in a file plot2d.pdf
plt.show()
# Show the plot on the console.
Figure 38: Plot of y = x 2 and y = x 3 vs. x .
The generated pdf file is shown in Figure 38. In the code, the comments
provide a brief description of the Matplotlib functions, such as legend, xlim,
xlim. In the following we make several important remarks about the plot
script:
1. Key arguments of the plot() function are
a. color: Color of the curve, which could be blue (b), green (g), red (r),
cyan (c), magenta (m), yellow (y), black (k), or white (w).
b. linewidth (lw): Line width of the curve in units of points. Note that 72
points = 1 inch.
c. marker: Markers could one of the following: hline (‘-‘), point (‘.’),
Circle (‘o’), triangle_down (‘v), triangle up (‘^’), triangle_left (‘<‘),
triangle_right (‘>’), octagon (‘8’), square (’s’), pentagon (‘p’), star (’*’),
hexagon (‘h’ or ‘H’), diamond (‘d’ or ‘D’), plus (‘+’), x (‘x’), and more. See
https://matplotlib.org/api/markers_api.html for more details.
d. markersize: Size of the above markers.
e. linestyle: solid (‘-‘), dashed (‘- -‘), chained (‘-.’), dotted (‘:’)
f. label: Labels are used for the legends.
2. plt.xticks, plt.yticks: We can place the axis ticks at the desired
locations.
3. plt.show(): Display all the open figures.
4. plt.legend(loc = x): Place legend at location given by x. See Table 13
for the location code.
Table 13: Location code for placing legends in a plot
Location Location
string code
‘best’ 0
‘upper right’ 1
‘upper left’ 2
‘lower left’ 3
‘lower right’ 4
‘right’ 5
‘center left’ 6
‘center right’ 7
‘lower center’ 8
‘upper centre’ 9
‘center’ 10
Objects of Matplotlib
1. Figure object
2. Axes object
Figure is the top level object containing all the elements of a figure. A
Figure object contains one or more Axes objects with each axes object
representing a plot inside the figure. Figure class declaration is as follows:
class matplotlib.figure.Figure(figsize=None, dpi=None,
facecolor=None, edgecolor=None, linewidth=0.0, frameon=None,
subplotpars=None, tight_layout=None, constrained_layout=None)
We create a figure object of size 5 inch x 5 inch using the following Python
statement:
A single plot or several plots are embedded inside the figure. This is done by
a function add_subplot (). It adds an Axes to the figure. The function
add_subplot can be called in several ways:
Nrows = ncols = 2
and the axes objects ax1, ax2, ax3, ax4 are created using
ax1 = fig.add_subplot(2,2,1)
ax2 = fig.add_subplot(2,2,2)
ax3 = fig.add_subplot(2,2,3)
ax4 = fig.add_subplot(2,2,4)
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0,1,100)
Figure 39: Plot produced by Example 1. It illustrates the usage of Axes and
subplots.
An important point to note in the figure is control of the xticks and yticks
in the figure.
ax1.xaxis.set_major_locator(plt.MaxNLocator(3))
ax2.yaxis.set_major_locator(plt.MaxNLocator(6))
The above statements produce 3 xticks and 6 yticks respectively. The other
interesting options for the above functions are NullLocator, LogLocator,
AutoLocator, FixedLocator . The other ticks function is Tick formatter. For
example, NullFormatter () puts no labels on ticks. Refer to Matplotlib
documentation for more details.
Python offers features for producing variety of plots. In the following
discussion, we will show how to make plots to present scalar and vector
fields.
Visualising z=f(x,y)
1. Contour plot
2. Density plot
3. Surface plot
x = np.linspace(-10,10,100)
y = np.linspace(-10,10,100)
xv, yv = np.meshgrid(x,y)
z = (xv**2 + yv**2)/2
The function ax.contour (xv, yv, z , 6) produces six contour lines . ax.clabel
() controls the formatting of contour labels . Alternatively, the function
produces contours with color map . The resulting plot is show in Figure 40.
Figure 40: Contour plot of function z = x 2 + y 2 . The contours are labelled.
Density plot: We can also make density plot of the function z = x 2 + y 2
using the functions imshow (), pcolor (), and pcolormesh (). The function
pcolormesh () is faster than pcolor (). The code segment is given below, and
the plots are shown in Figure 41.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
x = np.linspace(-1,1,100)
y = np.linspace(-1,1,100)
xv, yv = np.meshgrid(x,y)
z = (xv**2 + yv**2)/2
divider = make_axes_locatable(ax1)
cax2 = divider.append_axes("right", size="5%", pad=0.05)
fig.colorbar(c1, cax=cax2)
# Using pcolor
ax2 = fig.add_subplot(1,2,2)
ax2.set_title('using pcolormesh')
c2 = ax2.pcolormesh(xv,yv, z)
ax2.set_xlabel('$x$',size=12)
ax2.set_aspect(aspect=1)
#Another way to set aspect ratio
1. The arrays xv and yv provide the x and y coordinates at the grid points.
They are 2D arrays. We compute z at each grid point using xv and yv
arrays.
2. The colormap is resized to dimension of the plot using
make_axes_locatable(). There are variety of colormaps. Choice of
colormap is subjective. For some advice on colormaps, refer to
https://matplotlib.org/tutorials/colors/colormaps.html and
https://www.kennethmoreland.com/color-advice/
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# import matplotlib.cm as cm
from pylab import rcParams
rcParams['figure.figsize'] = 7, 3.5
x = np.linspace(-2,2,100)
y = x.copy()
xv, yv = np.meshgrid(x,y)
z = np.exp(-(xv**2 + yv**2))
fig = plt.figure()
ax1 = fig.add_subplot(121, projection = '3d')
ax1.plot_surface(xv, yv, z, rstride=5, cstride=5, color =
'm')
Vector Plots
The following code segment produces 2D and 3D vector plots for the fields
v = (−x , −y ) and v = (−x , −y , −z) respectively. Here, we employ the
function quiver ().
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm
from pylab import rcParams
rcParams['figure.figsize'] = 7, 3.5
fig = plt.figure()
ax1 = fig.add_subplot(121, aspect ='equal')
ax1.quiver(xv, yv, -xv, -yv)
#3D vector plot for the field v = (-x, -y)
ax2 = fig.add_subplot(122, projection = '3d')
x = np.linspace(-L,L,5)
y = x
z = x
xv, yv,zv = np.meshgrid(x,y,z)
ax2.quiver(xv, yv, zv, -xv, -yv, -zv)
The 2D and 3D vector plots are shown in the left and right sides of
Figure 43, respectively.
Figure 43: (Left figure) 2D vector plot of the vector field v = (−x , −y ).
(Right figure) 3D vector plot of the vector field v = (−x , −y , −z).
With this, we end this section on scalar and vector plots.
********************
Exercises
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from pylab import rcParams
rcParams['figure.figsize'] = 3,3.5
fig = plt.figure()
axes = fig.add_subplot(111, projection = '3d')
t = np.linspace(0, 8 * np.pi, 1000)
b = 1
r = b
x = r * np.sin(t)
y = r * np.cos(t)
z = b*t
Radial-polar Plot
Using Python we can make plots in radio-polar (r-θ ) coordinate system. The
following code segment plots conic-section curves, which are described by
r = 1/(1+ e cos(θ))
where e is the eccentricity of the curve. In Figure 44(b) we plot the conic-
section curves for e = 0, 0.5, 1 and 3.
import numpy as np
import matplotlib.pyplot as plt
from pylab import rcParams
rcParams['figure.figsize'] = 4,3
phi = np.linspace(0,2*np.pi,100)
r1 = 1.0/(1 + 0*np.cos(phi)) #Circle
r2 = 1.0/(1 + 0.5*np.cos(phi)) #Ellipse
r3 = 1.0/(1 + 1*np.cos(phi)) #Parabola
r4 = 1.0/(1 + 3*np.cos(phi)) #Hyperbola
fig = plt.figure()
axes = fig.add_subplot(111, projection = 'polar')
axes.set_title('Polar plot')
axes.plot(phi, r1, lw=1, color='r', label=r'$e = 0$')
axes.plot(phi, r2, lw=1.25, color='g', label=r'$e = 0.5$')
axes.plot(phi, r3, lw=1.5, color='b', label=r'$e = 1.0$')
axes.plot(phi, r4, lw=2, color='m', label=r'$e = 3.0$')
plt.ylim(0, 10)
axes.set_rgrids([5,10], angle=180)
plt.legend()
plays a key role. The projection = 'polar' instructs the interpreter to make a
radial-polar plot. The resulting plot is shown in Figure 44(b).
Histogram
Python helps us plot a histogram of a given data set. The following code
creates a histogram of random numbers which are Gaussian distributed with
mean of ½ and standard deviation of 1.
The above code generates 105 random numbers with Gaussian distribution
with average of ½ and standard deviation of 1.0. After this, the numbers are
divided into 50 (num_bins ) bins and a histogram is created, which is
displayed in Figure 45. The function plt.hist () provides the following
information about the histogram:
Pie diagram
We create a Pie diagram using the following code. Imagine that a student
obtains 90, 80, 60, 90 marks (out of 100 each) in Physics, Chemistry,
Mathematics, and in Hindi. Using Pie diagram we can illustrate the
distribution of student’s marks (from the total marks) in various subjects. The
function
pie(y,labels=mylabels)
creates a pie diagram of elements of array y, and labels the elements using
the entries from the list mylabels . The plot is show in Figure 46.
In [5]: y =
array([90,80,60,90])
"""
Matplotlib Animation Example
# First set up the figure, the axis, and the plot element we
want to animate
fig = plt.figure()
ax = plt.axes(xlim=(0, 2), ylim=(-0.5, 1.5))
line, = ax.plot([], [], lw=2)
anim.save('gaussian.gif',writer='matplotlib.animation.PillowW
riter')
Exercises
Input/Output in Python
READING & WRITING TEXT FILES IN
PYTHON
Files in Python
Python supports many access modes to a file. Some of them are listed in
Table 14. We will discuss these modes in some detail in the following
discussion.
[Table 14: Access modes for files ]
Writing Text to a File
Python treats a text as a set of strings . We write strings to a file using the
following set of statements.
In [100]: f = open('data.txt',
"w")
In [101]: f.write("Hi!
\n")
Out[101]: 5
In [104]:
f.close()
In [105]: !cat
data.txt
Hi!
This is month of June.
It is very hot here.
Expect rains next month.
The first statement creates file object f, which is associated with the
physical file data.txt. We write a single string using function f.write() and
multiple lines using f.writelines().
Note that f.write(20) will give an error because 20 is not a string.
Instead, you need to use f.write(’20’). The file data.txt has four lines that are
separated by the nextline character ‘\n’. We can view the data.txt using cat or
vi commands, e.g., vi data.txt.
Another way to write data to a file is as follows. We can use the print
function with an additional argument, file = file_name, to send the data to the
file whose name is file_name. For example, the following statement prints
variables x and y to file f.
In [111]:
f2.close()
In the above example, the variables x and y are stored as a string. After
the above operations, file’s contents are “5 6 \n”.
Now, we read the contents of the file data.txt using readline() and read()
functions.
In [127]: f = open('data.txt',
"r+")
In [128]: print(f.readline())
...:
print(f.readline())
Hi!
In [129]:
print(f.read())
In [130]:
f.seek(0)
Out[130]:
0
In [131]: str_read =
f.read()
In [132]:
str_read
In [133]:
f.seek(0)
Out[133]: 0
Hi!
This is month of June.
It is very hot here.
Expect rains next month.
f = open('data.bin', 'wb')
f = open('data.bin', 'rb')
bin_str_read = f.read()
f.close()
1. The function bytearray () returns an object bin_str for the string “ABC
ह ”. This procedure however requires an encoding scheme. In the above
example, we employ utf-8 scheme (see Section Character & Strings).
The other prominent encoding schemes are ascii, latin-1, utf16, and
utf32 .
2. bin_str is an array of bytes: 41 42 43 e0 a4 b9, with the first three bytes
corresponding to characters ‘A’, ‘B’, ‘C’ respectively, and the last three
bytes for the unicode character ‘ह ’ (see Section Character & Strings).
We obtain these values from the terminal using hexdump command. The
numbers in the left of the lines (e.g., 0000000) are the byte addresses.
⁃ $hexdump data.bin
⁃ 0000000 41 42 43 e0 a4 b9
⁃ 0000006
3. bin_str Note that we cannot write a string to a binary file. For example,
f.write(‘Hi’) is not allowed for ‘data.bin’ file. We can write only binary
strings to a binary file.
4. Ipython prints the following where b of b’ABC’ represents binary.
⁃ In [206]: bin_str
⁃ Out[206]: bytearray(b'ABC\xe0\xa4\xb9')
⁃ In [207]: bin_str_read
⁃ Out[207]: b'ABC\xe0\xa4\xb9'
We remark that it is convenient to use text format, rather than binary format,
for working with text or strings.
********************
Exercises
1. Write a poem in a text file, and save it in a file named “poem.txt”. Read
back the poem and store it as a list of strings. Make sure that the read
text is same as the original text.
READING & WRITING NUMERICAL
DATA IN PYTHON
Such datasets are saved or stored on a hard disk. Subsequently, they are
read by a computer program for further processing.
Numerical data can be stored on a hard disk in various formats. Some of
the popular formats are ASCII, binary, HDF, HDF5, CSV, NASACDF, etc. A
brief description of some of the above formats are given below.
import numpy as np
x = np.arange(0,2*np.pi,0.01)
y = np.sin(x)
np.save('data_x.npy', x)
np.save('data_y.npy', y)
x_read_txt = np.load('data_x.npy')
y_read_txt = np.load('data_y.npy')
np.savetxt('data_x.txt', x)
np.savetxt('data_y.txt', y)
x_read_txt = np.loadtxt('data_x.txt')
y_read_txt = np.loadtxt('data_y.txt')
The functions save() and load() work with .npy files, while savetxt() and
loadtxt() work with .txt files. Note that, by default, ASCII real data is saved
in exponential format. However, we can save the data in float format with
finite precision using fmt option. In the following, we save x to the file
data.txt. Using format, we save only 3 digits after the decimal.
After this we discuss how to save and read numbers in binary format.
Writing to and reading from binary files (with .bin extension) are described
below. For reading a binary file, we need to provide the number of bytes to
be read as an argument to the file. In the following example, the number of
bytes is 629*8 because each double-precision float takes 8 bytes of storage.
f=open('sin.bin', 'wb')
f.write(x); f.write(y); f.close()
f=open('sin.bin', 'rb')
xraw=f.read(629*8) # no of data elements = 629
yraw=f.read(629*8) # each float takes 8 bytes
import struct
x_read_bin=np.array(struct.unpack('d'*629, xraw))
y_read_bin=np.array(struct.unpack('d'*629, yraw))
The operations f.read (629*8) creates two strings, xraw and yraw , but not
floating-point arrays. We employ the struct module to convert the above
strings to float arrays. The function struct.unpack () returns a tuple
containing the data. The character ‘d’ in the argument of struct.unpack ()
stands for the double precision, while 629 is the number of data elements to
be read.
In the above example, we read arrays containing real numbers in double
precision. However, Python can read/write integers, single-precision real
numbers, and boolean arrays as well. We need to employ ‘f’ for float, ‘i’ for
integer, and ‘?’ for _Bool as arguments of struct.unpack () function.
Input/Output for HDF5 data
In the following code segment we write two arrays x and y to a HDF5 file
'data.h5’. Here we use h5py module of Python. The file object hf is created
to write the arrays to the hard disk. The file object hf contains information
about the file size, file type (integer, float, character), and the data.
import numpy as np
import h5py
Often we need to import images and rework them. Here we show how to
read an image in python using mpimg function.
In [69]: imag =
mpimg.imread('amaltas.jpg')
In [70]: plt.imshow(imag)
Exercises
Precision and accuracy are often considered to be the same thing. However,
they are not the same! Contrasting these two terms yields interesting insights
into the error analysis, as we show below.
A marksman aims to hit the bullseye all the time. See Figure 49 an
illustration. The distance of a shot from the bullseye is a measure of
inaccuracy, while the scatter of shots is a measure of imprecision. Accurate
shots are close to the centre, while precise ones are clustered together. In
Figure 49, case (a) corresponds to accurate and precise shooting, (b) to
precise but inaccurate shooting, (c) to accurate but imprecise shooting, and
(d) to inaccurate and imprecise shooting. Needless to say that a marksman
desires accurate and precise shots at the target.
Even though both accuracy and precision of shots depends on the quality
of the gun and the skills of the marksman. Note, however, that a good
marksman can shoot accurately, but the precision of the shot depends on the
quality of the gun. For example, sniper shots are precise. Note that precision
is related to random errors, while accuracy to systematic errors. More on
these errors in later part of the section.
In later part of this section, we will show similar features in numerical
computation. Now, we discuss some important issues—precision of an
instrument, significant digits, and quantification of errors.
Error is defined as the difference between the actual value (Vactual ) and the
measured or computed value (Vcomp ), that is
Error = Vactual − Vcomp .
Absolute error is defined as the absolute value of the difference between the
actual value (Vactual ) and the numerical value (Vnum ), that is
Absolute error = |Vactual − Vcomp |.
Relative error is defined as the ratio of absolute error and the actual value:
Relative error = |Vactual − Vcomp |/|Vactual |
Relative error is a better measure of errors. For example, an error of 0.1 kg
for the weight of a baby is significant, but it is not so for an elephant.
After this discussion, we categorise the errors encountered in numerical
computations.
where ⟨⟩ stands for averaging. The above analysis shows that we need to be
cautious while subtracting nearly equal numbers.
Following similar procedure we can estimate the relative error in a
multiplication operation. For the operation r = x y , we deduce that
In [2]: 1.5*3-
4.5
Out[2]: 0.0
In [3]: 1.1*1.5-
1.65
Out[3]: 2.220446049250313e-16
1.5*3 is accurate with zero error, but 1.1*1.5 has error of the order of 10−16 .
This is because 1.5 and 3 are accurately represented by computers, but 1.1 is
not. As we show in Section Floating Point and Complex Numbers, (1.1)10 =
(1.1999… )x with 12 ocurrences of 9. Hence, the truncation error in the
representation of (1.1)10 is of the order of 9x16−14 ≈ 1.25x10−16 . The net
error 1.1*1.5 is of the order of this truncation error (see Eq.
(#eq(error_mult))). Further discussion on this topic will take us beyond the
scope of this book.
In [83]: float32(1.1000001)-
float32(1.1)
Out[83]: 1.1920929e-07
In [85]: (float32(1.1)-
float32(0.8))/float32(0.3)-1
Out[85]: 0.0
In [86]: (float32(1.1000001)-float32(1.1))/float32(1e-
7)-1
Out[86]: 0.1920928955078125
In [13]: 1.1-
0.8
Out[13]: 0.30000000000000004
In [14]: 1.1000001-
1.1
Out[14]: 9.999999983634211e-08
In [16]: (1.1-0.8)/0.3-
1
Out[16]: 2.220446049250313e-16
In [17]: (1.1000001-1.1)/1e-7-
1
Out[17]: -1.6365788724215236e-09
The situation is much better for double precision. For 1.1000001−1.1, the
relative error is approximately 10−9 , which is close to zero. For double
precision, the relative error is significant for 1.100000000000001−1.1.
In [89]: 1.100000000000001-
1.1
Out[89]: 8.881784197001252e-16
In [90]: (1.100000000000001-1.1)/1e-12
-1
Out[90]: -0.9991118215802999
That is, the relative error is significant when the difference between the two
numbers is close to the machine precision.
A question is why worry about such small numbers. It turns out that we
encounter such small numbers in physics. For example, the atomic and
nuclear sizes are in nano (10−9 ) and femto meters (10−15 ) respectively.
Therefore, we need to be cautious in quantum calculations that involve small
numbers. In the next section, we will present strategies for overcoming the
above issues using nondimensionalization.
a x2 + b x + c = 0
are
For small c, √(b 2 -4ac ) is close to |b|. Hence, one of the solutions is close to
zero, which is vulnerable to round-off errors. For a = b = 1, and c = 10−15 ,
single precision yields
In [1]: c=1e-
15
In [2]: x1=float32((-1-float32(sqrt(1-
float32(4*c))))/2)
In [3]: x2=float32((-1+float32(sqrt(1-
float32(4*c))))/2)
In [4]: print(x1,
x2)
-1.0 0.0
In [42]: c=1e-
15
In [43]: x1=(-1-sqrt(1-
4*c))/2
In [44]: x2=(-1+sqrt(1-
4*c))/2
In [7]: print(x1,
x2)
-0.999999999999999 -9.992007221626409e-16
For double precision, we will face problems when c is smaller than 10−30 .
Example 5: For large x , √(x+1) − √x →0. Hence, it is best to evaluate the
above using
√(x+1) − √x = 1/(√(x+1) + √x).
For x = 1018 , √(x+1) − √x = 0, but the correct answer is
1/(√(x+1) + √x) = 5 x 10−10 .
In the following code, we compute cos(x ) and exp(x ) for x = 1 using series
solution with n terms. We vary n from 1 to 19.
import numpy as np
import matplotlib.pyplot as plt
import math
plt.figure(figsize = (3,2))
N = 20
x = 1
# for exp(x)
print ("Exp(x) expansion for x=1")
##
error_expx = []
sum = 0
for n in range(0,N):
sum += x**(n)/math.factorial(n)
error_expx.append(np.exp(x)-sum)
print (n, sum, np.exp(x)-sum, x**
(n+1)/math.factorial(n+1))
x_axis = np.arange(1,N+1,1)
error_expx = np.array(error_expx)
# for cos(x)
print ("cos(x) expansion for x=1")
error_cosx = []
sum = 0
for n in range(0,N):
sum += (-1)**n * x**(2*n)/math.factorial(2*n)
error_cosx.append(np.cos(x)-sum)
print (n, sum, np.cos(x)-sum, x**
(2*n+2)/math.factorial(2*n+2))
x_axis = np.arange(1,N+1,1)
error_cosx = np.array(error_cosx)
plt.semilogy(x_axis, abs(error_cosx),'b.-', lw = 2,
label='error in $\cos(x)$')
plt.xticks([1,4,8,12,16,20])
plt.xlabel(r'$n$',fontsize=10)
plt.ylabel('Errors',fontsize=10)
plt.tight_layout()
plt.show()
We compute the numerical errors for each n and plot them. As shown in
Figure 51, cos(x ) converges quickly to the actual answer. For n = 8, the
error is approximately 1.11x10−16 . However, the convergence is slow for
exp(x ); the accuracy of 10−16 is achieved for n = 17.
The above series expansions describe simple approximate solutions.
Ramanujam came up with an amazing series solution for π that yields correct
answer up to 7 significant digits with three terms only (reference:
wikipedia.org ):
In later parts of the book, we will discuss various approximate solutions for
computing derivatives, integrals, roots of equations etc.
[Figure 51: Errors in the series solution for cos(x) (circles) and exp(x)
(triangles) for various n ’s of the series.]
In Figure 52 we illustrate how one approaches the correct answer using
better and better approximations. A marksman improves his/her accuracy
with better techniques, while better algorithms improve the accuracy of a
numerical solution. In the examples of sin(x ), we obtain better solution by
increasing the number of terms of the series solution.
Figure 52: A schematic diagram illustrating how to obtain accurate solution
successively using better and better algorithms.
Conceptual Questions
Exercises
1. Compute the round off errors for the following operations: 1.1*8, 1/3*7,
1.5*2, 0.2*9.
2. Count the significant digits for the following real numbers.
0.0019, 0.00190, 01.980
3. Using Python code, find roots of quadratic equation a x2 + b x + c = 0
for a = 1, b = 2000.00001, and c=0.002. Employ 32-bit float variables.
Compare your numerical result with exact results.
4. Compute cos(x ) for x =1.0 using a series solution by taking various
number of terms (n ). Plot the errors as a function of n . Compare the
results with those for sin(x) discussed in this section.
5. Compute exp(−x ) for x=1.0 using a series solution and compute the
errors. Compare the results with those for exp(x ) discussed in this
section.
NONDIMENSIONALIZATION OF
EQUATIONS
The above solution appears quite complex with so many parameters (m, g, γ,
v x (0), v y (0)). Therefore, we nondimensionalize this solution using the
following transformations. We take the velocity scales along the x and y
directions as v x (0) and v y (0) respectively; the time scale as v y (0)/g , and
the length scales along the x and y directions as v x (0)v y (0)/g and [v y (0)]2
/g respectively. In terms of these units, the nondimensionalized time and
space coordinates are
t’=t/ [v y (0)/g ] ,
x’=x/ [v x (0)v y (0)/g ] ,
y’=y/ [(v y (0))2 /g ] .
Substitution of the above in Eq. (<$n#eqn:projectile_x>) and Eq.
(<$n#eqn:projectile_y>) ¯yields the following solution:
where α = γv y (0)/(mg ) is the only parameter of the nondimesionalized
system. The initial velocity is (v x (0), v y (0)) = (1,1) in the nondimensional
units.
We can analyse (x’, y ’) for various α ’s. The resulting trajectories are
exhibited in Figure 53. Note that we can easily convert (x’,y’,t’ ) to (x,y,t )
using the above transformations and compute the real trajectories.
ẍ+ (ω 0 )2 x = (F 0 /m ) cos(ω f t ).
or
which is the nondimensionalized Schrödinger equation, a simpler version of
the original equation. Some of the nondimensionalized wave functions of the
Hydrogen atom are
*******************
Conceptual Questions
Exercises
Interpolation
LAGRANGE INTERPOLATION
Lagrange Interpolation in 1D
,
where L 0 (x ) and L 1 (x ) are linear functions of x , and they have the
following properties: L 0 (x 0 ) = 1, L 0 (x 1 ) = 0, L 1 (x 0 ) = 0, and L 1 (x 1 ) =
1. Note that in general, P 2 (x ) ≠ f (x ). However, P 2 (x ) = f (x ) when f (x )
is a linear function of x .
Now we generalise the above discussion to n data points: (x 0 ,y 0 ), (x 1
,y 1 ), …, (x n −1 ,y n −1 ). Lagrange constructed the following interpolation
function passing through these points:
where
Usage:
xarray = np.array([3,4])
yarray = np.array([1/3.0,1/4.0])
P2 = Lagrange_interpolate(xarray,yarray,x)
xarray = np.array([2,5])
yarray = np.array([1/2.0,1/5.0])
f = interpolate.interp1d(xarray,yarray)
# f contains the interpolation function.
xinter = np.arange(2.1,5,.1)
P2_scipy = f(xinter)
# P2_scipy is an array containing interpolated values for
xinter array.
Lagrange Interpolation in 2D
Third step: We substitute the above expression in P (x,y ) that yields the
desired interpolation function:
.
Hence,
where
Conceptual Questions
Exercises
Now we impose an additional condition that the first derivative at the nodes
is continuous at both sides. Applying this condition at x = x i , i.e., f ’ i (x i ) =
f ’ i -1 (x i ), we obtain
which yields
We obtain (n− 2) linear equations for nodes at i = 1…(n− 2). However we
have n unknowns [f ’’ (x i ) for i = 0…(n− 1)]. To solve this problem, we
use one of the following boundary conditions:
With one of the above four boundary conditions, we have equal number of
unknowns and linear equations. These equations can be solved using matrix
methods that will be discussed in Section Solution of Algebraic Equations. In
the following example, we take a simple case with four points that can be
solved analytically.
Example 1: We work out the splines for the data of Example 1 discussed in
Section Lagrange Interpolation. We consider the same four points {(2,1/2),
(3,1/3), (4,1/4) (5,1/5)} as before. Let us use the free-end boundary
condition (f 0 ’’ = f 3 ’’ =0) for this example. Note that h i = 1. The equations
for the splines at the intermediate nodes are
The solution of the above equations are f 1 ’’ = 3/25 and f 2 ’’ = 1/50. Using
these values and f ’’ 0 = f ’’ 3 = 0, we construct the piece-wise functions and
plot them together. The plot is shown in Figure 61 as a black dashed curve.
The code for computation of f (x ) is given below.
Figure 61: For the data {(2,1/2) (3,1/3), (4,1/4) (5,1/5)}, the plots of splines
computed using free-end boundary condition (black dashed curve) and
Scipy’s CubicSpline (solid red curve). These curves are close to each other,
and to the actual function 1/x . See Figure 62 for errors.
The following Python code computes the splines using free-end boundary
condition (as described above) and using Scipy’s interpolate.CubicSpline
function. The function generated using interpolate.CubicSpline function is
shown as red solid curve in Figure 61.
def f(i,x):
first_term = (fdd[i]*(xd[i+1]-x)**3 + fdd[i+1]*(x-
xd[i])**3)/6
second_term = (yd[i]-fdd[i]/6)*(xd[i+1]-x) + (yd[i+1]-
fdd[i+1]/6)*(x-xd[i])
return first_term + second_term
# range of x
x0r = np.arange(2,3.1,0.1)
x1r = np.arange(3,4.1,0.1)
x2r = np.arange(4,5.1,0.1)
y0r = f(0,x0r);
y1r = f(1,x1r);
y2r = f(2,x2r);
# Using splines
cs=interpolate.CubicSpline(xd,yd)
x=np.arange(2,5.1,0.1)
y = cs(x)
Even though the splines generated by free-end boundary condition and Scipy
functions are quite close to the actual function, 1/x , there are small errors (E
(x ) = f (x ) − P (x )), which are plotted in Figure 62. The black-dashed and
solid-red curves represent the errors for the two cases. The accuracy of
Lagrange polynomial and splines computed using interpolate.CubicSpline
are comparable. However, the errors of splines computed using free-end
boundary condition is quite significant. This is because f ’’ (x 0 ) = 1/4, not
zero, as is assumed in free-end boundary condition. The error at the other
end, x = 5, is less damaging because f ’’ (x 3 ) = 2/25 ≈ 0.
Figure 62: The plots of error E (x ) = f (x )−P (x ) for the splines calculated
using free-end boundary condition (black dashed curve), using CubicSpline
(solid red curve), and using Lagrange polynomial with 4 points (solid black
curve).
B-Splines is another prominent spline, but it is not covered in this book. For
B-splines, you can use Python’s scipy function interpolate.splrep () to
compute the spline parameters (tck ), and then compute the curve by
supplying x array to interpolate.splev(x, tck).
In summary, splines provide smooth interpolating functions for a large
number of points. However, these computations require solution of matrix
equation. Fortunately, the matrix is tridiagonal that can be easily solved using
methods that will be described in Section Solution of Algebraic Equations.
In this chapter, we have assumed that the sampling points (x i , y i ) have
zero noise. In the presence of random noise, regression analysis are used to
determine the underlying function f (x ). We will discuss regression in
Section Regression Analysis.
*******************
Conceptual Questions
Exercises
Numerical Integration
NEWTON-COTES FORMULAS
where w i ’s are the weights and x i ’s are abscissas. The integration schemes
can be classified into two broad categories:
Newton-Cotes Formulas
… (1)
where
… (2)
are the coefficients for m th degree NC scheme. Since L j (x )’s are
independent of the data f (x j ), we conclude that C j ( m ) ’s are independent of
f (x j ). For f (x ) = 1, Eq. (#eq(Newton_Cotes_def)) yields I =(b −a ).
Hence, we conclude that
Using the above formula we deduce that C 0 (1) = C 1 (1) = ½. This method,
called trapezoid rule , is accurate up to machine precision for linear
functions. The blue-coloured region of Figure 64(a) is the numerical integral,
and the white area A below f (x ) is the error in the integral.
Figure 64: (a) Trapezoid rule. (b,c) Simpson’s rule. The numerical integral
is represented by the blue coloured region.
For m = 2, we employ the quadratic Lagrange polynomial, P 3 (x ), with
abscissas at x=a, (a+b )/2, b . Note that h = (b−a )/2. Hence,
Therefore, C 0 (2) = C 2 (2) = ⅓ and C 1 (2) = 4/3. This method, called Simpson’s
⅓ rule , is accurate when f (x ) is a polynomials of third-degree or lower
(see Figure 64(b,c)). In figure(c), for a cubic f (x ), the filled region C and
unfilled region B cancel each other. The coefficients C j ( m ) for larger m are
derived similarly using higher-order Lagrange polynomials. These
coefficients are listed in Table 15.
Table 15: Coefficients for the Newton-Cotes integration schemes.
The methods for m =2 and 3 are called Simpson’s ⅓ and Simpson’s 3/8 rules
respectively. We can obtain the coefficients for all m ’s using Python’s
scipy.integrate.newton_cotes (m, 1), where m is the degree of the scheme.
The argument 1 indicates that the abscissas are equally spaced. The function
returns a tuple whose first element is a list of coefficient, while the second
element is the prefactor for the error, which will be discussed later in this
section.
Example 1: A code segment below illustrates how to compute ∫0 ^1 x 4 dx (1
is the upper limit of the integral) using NC method. The integral is exact (0.2)
for m = 4. However, for m = 2 and 3, the integrals are
0.20833333333333331 and 0.20370370370370366 respectively.
0.2
Error Analysis for Newton-Cote’s Method
Hence, the error in the integration by the Newton-Cotes scheme with n points
is
Hence, the integral of Eq. (23) is O (h n +1 ) for even n , but it vanishes for
odd n. Note however that E n ≠ 0 for odd n unless f (x) is a polynomial of
degree n or lower. Hence, to estimate the error for odd n , we approximate f
(x ) with the (n +1)th order Lagrange polynomial that passes through the
points (a, a+h, a+2h, ..., b, b+h ), but integrate E n (x ) in the interval [a ,b ].
Hence, a more accurate estimate of the error for odd n is
.
We illustrate the above estimation for n = 3 with a = 0, b= 1, and h =½.
For these parameters, Eq. (23) ields
m Error
1 (h 3 /12) f ’’ (ζ )
m Error
2 (h 5 /90) f ’’’’ (ζ )
3 (3h 5 /80) f ’’’’ (ζ )
4 (8h 7 /945) f ’ (6) (ζ )
5 (275h /12096) f ’ (ζ
7 (6)
)
6 (9h 9 /1400) f ’ (8) (ζ )
Figure 65: Example 6: Errors in integral ∫0 ^(π/2) sin(x )dx computed using
Newton-Cotes method with n = 2,3,4,5,6 points.
In Figure 65, we plot the errors in the integral computation. The figure shows
that error drops when we go from 2 to 3, but not so much from 3 to 4.
Similarly, error does not drop significantly when n increases from 5 to 6.
This is because the integrals using odd n ’s are accurate up to O(h n +2 ),
which is also the accuracy with n +1 even points. See Table 16 for details.
Multi-interval Newton-Cotes Method
For a large interval (b-a ), it is practical to divide it into many smaller
segments, and then employ NC method to each of the segments. For example,
we divide an interval (a,b ) into n− 1 segments, and employ trapezoid rule to
all of them. This operation yields the following formula for the integral:
We compute ∫0 ^(π/2) sin(x )dx using the above formula by dividing the
interval (0,π/2) into 2 to 9 intervals and employing Trapezoid rule on each
segment. In Figure 66 we plot error E h vs. h for h = (π/2)/2 to (π/2)/9. As
shown in the figure, E h is proportional to h 3 , which is consistent with the
error estimate of trapezoid rule, E h = (h 3 /12) f ’ (2) (ζ ).
Figure 66: Plot of error E h vs. h for the integral ∫0 ^(π/2) sin(x )dx when h
varies from (π/2)/2 to (π/2)/9. The green line represents h 3 curve.
We already know the values of the integrals in the above illustrative
examples. Hence, we could study the errors for various m and h . The
computation, however, becomes tricky in practice when the integral is
unknown. Here, we need to choose h and degree of NC method with care. In
such a scenario, for a given NC method, we compare I (h ) with I (h /2). If
the error, I (h ) − I(h /2), is less than the required tolerance, then we can take
I (h /2) as the final numerical value of the integral. Also, it is better to use
NC method with odd n’ s because they yield better accuracy compare to even
n ’s.
With this, we close our discussion on the Newton-Cotes method. In the
next section we will describe Gaussian quadrature, which is more accurate
than the Newton-Cotes method.
*******************
Conceptual Questions
Exercises
where a and b are the limits of the integral, h (x ) is the weight function for
the integral (different from the weights w i for the integral), and γ i ’s are
constants. In this scheme, the N abscissas are the roots of φ N (x ), while the
weights are
Therefore,
When f (x ) is a polynomial of degree higher than 2N −1, the integral with the
quotient (Eq. (#eq(quotient_GQ))) is not zero.
Step 2: We simplify the integral further. φ N (x ) is a polynomial of degree N ,
hence it has N roots (assume real), which are taken to be abscissas {x j }with
j = 0:N −1. Note that
Since r N −1 (x ) is a polynomial of degree N −1 or lower, we expand it using
Lagrange polynomials whose knots are located at x j ’s. That is,
Therefore,
where
Hence,
When f (x ) is a polynomial of degree higher than 2N −1, the integral with the
quotient (Eq. (#eq(quotient_GQ))) is not zero. This integral is the error. A
division of a general f (x ) with φ Ν (x ) yields
where
This is the error in the integral. The other terms of q (x ) (j ≠ N ) vanish due
to the orthogonality properties of the polynomials. After algebraic
manipulation we derive the error as
where ζ is an intermediate point in [a.b].
In the following discussion, we discuss specific implementations of
Gaussian quadrature.
Quadrature Based on Legendre Polynomials
The roots x j and the weights w j can be computed using the polynomials. In
Table 18, we list them for N = 1 to 5. Python’s module
scipy.special.roots_legendre () provides these quantities (see the Python
code below). Note that the weights and abscissas of Example 1 match with
that shown in Table 18.
Table 18: The roots of Legendre polynomials, and the weights for the
Gaussian quadrature
Nx j wj
2 ±1/√3 1
30 8/9
±1/√(3/5) 5/9
± 0.34785
4
0.339981 5
± 0.56888
0.861136 9
50 128/225
0.47862
±0.538467
9
0.23692
±0.90618
7
Now the above integral can be computed using Legendre polynomials in the
interval [−1,1].
for n in range(2,6):
xi = roots_legendre(n)[0]
wi = roots_legendre(n)[1]
yi = f(xi)
In = sum(yi*wi)
print(n, In, In-2/9)
Laguerre-Gauss Quadrature
Figure 68: Plots of (a) Laguerre polynomials and (b) Hermite polynomials in
semiology format. The thickness of the curves decrease as the oder the
polynomials are increased.
For the above integral, we employ Laguerre polynomials (L m (x )) whose
orthogonality relation is
we deduce that
Nx j wj
0.58578
2 0.853553
6
3.41421 0.146447
0.41577
3 0.711093
5
2.29428 0.278518
6.28995 0.0103893
0.32254
4 0.603154
8
1.74576 0.357419
Nx j wj
4.53662 0.0388879
0.00053929
9.39507
5
5 0.26356 0.521756
1.4134 0.398667
3.59643 0.0759424
7.08581 0.00361176
12.6408 0.00002337
Example 5: For a stationary state of Hydrogen atom, the average value (or
expectation value) of function f (r ), ⟨f (r )⟩, is defined as
,
where ψ (r ) is the wave function. The ground state of the H-atom is ψ 1,0,0 =
exp(−x )/√π, where r’ = r/r a is nondimensional radius with r a is the Bohr
radius. For this state,
Hermite-Gauss Quadrature
Nx j wj
Nx j wj
2 ±(√2)/2 (√π)/2
30 2(√π)/3
±(√6)/2 (√π)/6
±0.52464
4 0.804914
8
0.081312
±1.65068
8
50 0.945309
±0.95857
0.393619
2
0.019953
±2.02018
2
We observe that ⟨1⟩=1 for all the wave functions, indicating that they are
normalised. In addition, we observe that for ψ n , ⟨x 2 ⟩ = n+½ . Therefore,
average potential potential energy ⟨U ⟩ = ⟨x 2 ⟩/2 = (n+½ )/2. Using Virial
theorem, we infer that ⟨T ⟩ = ⟨U ⟩ = (n+½ )/2. Hence, the total energy = ⟨T ⟩
+⟨U ⟩ = (n+½ ). In dimensional form, the total energy for ψ n is (n+½ )ħω.
In summary, Gaussian quadratures are much more accurate than the Newton-
Cotes methods. In the next section, we will cover multidimensional
integration.
*******************
Conceptual questions
Exercises
Multidimensional Integrals
We illustrate the computational domain (xy plane) in Figure 69. Here, the x
coordinates vary from a to b , while, for a given x , the y coordinates vary
from 0 to g (x ).
As shown in the figure, for the x integral, we choose abscissas at {x 0 , x 1
, …, x n −1 }. After that, at each abscissa, we choose knots along the y
direction. The coordinates of y -abscissas are {y 0 , y 1 , …, y m −1 }. Now,
the integral is performed as follows:
Note that the weights along the y -direction will depend on the x coordinate.
The above formula can be implemented using 1D integrals discussed earlier.
Also, we can easily generalise the above formalism to three and higher
dimensions.
(0.8862269254527579, 7.101318390472462e-09)
Here, the tuple contains the value of the integral and the associated error.
Python’s functions trapz and simps integrate numerical data using
trapezoid and Simpson’s rules. For example,
In [92]: y =
[1,2,3]
Out[95]: 4.0
which has finite limits. Incidentally, both the integrals, ∫0 ∞ exp(-x2 )dx and
Eq. (34), can be computed quite easily using Gauss quadrature.
In the above equation, the first term is zero, while the second term is
nonsingular.
With this, we end our discussions on numerical integration.
*******************
Exercises
Numerical Differentiation
COMPUTING NUMERICAL
DERIVATIVES
The derivatives of the above function at both the points, x i and x i +1 , are the
same. Yet, by notation, we write them as
,
where h i = x i +1 −x i . Here, the forward difference is the derivative
computed at x i looking toward the forward point x i+ 1 . The backward
difference is the derivative computed at x i+ 1 looking backwards towards x i
. See Figure 70 for an illustration. The computed derivatives or slopes (the
dashed lines of the figure) are approximations to the actual derivatives (solid
lines).
The aforementioned complex expressions get simplified when the points are
equidistant, that is, when h i− 1 = h i = h . Under this assumption, the first
derivatives at the three points are
The second derivatives at the three points are equal (see Eq.
(#eq(deriv_3_points))):
Note the usefulness of Lagrange interpolation formulas for the derivative
computations. We can also compute f ’’(x i ) by employing central-difference
scheme to numerical values of first derivative:
with the coefficients C m listed in Tables 23 and 24. As shown in the Table,
C m ’s follow the following properties.
We can estimate the errors in the numerical derivatives using the error
formula for the Lagrange polynomials, which is Eq.
(#eq(error_Lagrange_poly)) of Section Lagrange Interpolation. For n data
points, the error is
We can also derive the formulas for the derivatives using Taylor’s series,
which is
In [207]:
y
In [210]: gradient(y,2) #
gradient(y)/2
Conceptual questions
Exercises
Figure 71: The total time is divided into N− 1 steps. The time and x at the i
th
marker are t i and x ( i ) respectively.
An integration of Eq. (#eq(first-order-ODE)) from t n to t n+ 1 yields
These methods, called ODE solvers, prescribe how to advance from time t n
to t n+ 1 . We illustrate the Euler forward , Euler backward , and midpoint
methods in Figure 72.
Figure 72: Scheatic diagrams exhibiting (a) Euler’s forward method, (b)
Euler’s backward method, and (c) midpoint method. The dashed lines
touching x (t ) represent the slopes at the respective points. ε n is the error
between the actual value and the computed value at t = t n +1 .
*******************
Conceptual Questions
x ( n +1) = x ( n ) +(Δt ) f (x ( n ) , t n )
def f(x,t):
return -100*x
tinit = 0
tfinal = 1
dt = 0.01
initcond = 10
t,x = Euler_explicit(f, tinit, tfinal, dt, initcond)
We take x(0) = 1, final time = 10, and (Δt ) = 0.01. The numerical result,
shown in Figure 73(a) as a solid blue curve, matches quite well with the
exact result exp(−t ), which is shown as red dashed curve.
Example 2: We solve ż = −iπz numerically using z(0) = 1. The exact
solution of this equation is z (t ) = cos(πt ) − i sin(πt )). We take the final
time = 10, and (Δt ) = 0.01. In Figure 73(b) we plot Im(z ) vs. Re(z ), where
z (t ) is the numerical value of z at time t . The figure shows that |z | increases
with time. This is contrary to t,he exact solution for which |z (t )| = 1. We will
discuss the instability of the solution in later part of this section.
Euler's forward method captures the first two terms of Eq. (38). Hence, for
every time step, the error in this scheme is
Error = ε n = (½)(Δt )2 ẍ (x (n ),t n ) …(39)
Since the systematic errors for ODE solvers tend to add up, the cumulative
error in N steps is estimated to be (½)N (Δt )2 ẍ (x (n ), t n ). Hence,
Net error ≈ (½)N (Δt )(Δt ) ẍ (x (n ), t n ) ≈ (½)T (Δt ) ẍ (x (n ), t n )
Figure 75: Solution of ẋ = −100x using Euler’s forward method using (Δt ) =
0.021 (unstable, blue oscillatory curve) and 0.001 (stable, green falling
curve). The exact result (dashed black curve) is close to the solution for (Δt )
= 0.001.
Based on the stability issues, numerical schemes are classified into the
following categories:
Figure 76: The stable region (blue disk) and unstable region (grey zone
outside the circle) for Euler’s forward method.
So far we dealt with stability of a linear equation ẋ = αx . How is this result
relevant to nonlinear equations, such as ẋ = −t exp(−x ) of Example 3? For
such equations, we linearize f (x,t ) around (x ( n ) ,t n ) that yields
Now we shift the origin to (x ( n ) ,t n ) using a transformation: x’ = x − x ( n )
and t’ = t− t n . In terms of these variables, the differential equation is
ẋ’= β + αx’ + γt’ … (!eq(stability_linearized))
*******************
Conceptual Questions
Exercises
for k in range(n-1):
x[k+1] = ((100*dt+1)-np.sqrt((100*dt+1)**2
-4*dt*x[k]))/(2*dt)
return t,x
To analyse the stability of the above equation, we start with the equation ẋ =
αx. For this equation,
*******************
Conceptual Questions
1. Show that the accuracies of forward and backward Euler methods are
the same.
2. What are the advantages and disadvantages of backward or implicit
ODE solvers?
Exercises
This value is now substituted for x ( n +1) of the RHS of Eq. (46), that is,
x = np.linspace(-3,3,100)
y = x.copy()
xv, yv = np.meshgrid(x,y)
zv = xv + 1j*yv
g = 1 + zv + zv**2/2
gmag = abs(g)
In Figure 78(a) we plot the above boundary. In the figure, the inner region of
the oval is stable, while the region outside the boundary is unstable.
Figure 78 : The boundary between the stable and unstable regions of (a) the
predictor-corrector, and RK2 methods; (b) RK4 method. The regions inside
the curves are stable, while those outside are unstable.
Runge-Kutta Methods
k 1 = (Δt ) f (x ( n ) , t n ); x ( n +½)* = x ( n ) + k 1 /2 .
Now, we substitute the above in Eq. (47) that yields
for k in range(n-1):
xmid = x[k] + f(x[k],t[k])*dt/2
x[k+1] = x[k] + f(xmid,t[k]+dt/2)*dt
return t,x
initcond = 1.1
t_RK2,x_RK2 = RK2(f, tinit, tfinal, dt, initcond)
Thus, RK2 method involves two f () computations per step. It is easy to show
that this scheme has the same accuracy (O ((Δt )2 )). Regarding stability, It is
easy to show that the stability condition for the RK2 method is same as that
for the predictor-corrector method. See Figure 78(a) for an illustration.
Third-order RK method (RK3): RK3 scheme involves more intermediate
steps than RK2 method. The predictors for RK3 are
k 1 = (Δt ) f (x ( n ) ,t n ); x ( n +½)* = x ( n ) + k 1 /2 ,
k 2 = (Δt ) f (x ( n +½)* ,t n+ ½ ); x ( n +1)** = x ( n ) −k 1 + 2 k 2 ,
k 3 = (Δt ) f (x ( n +1)** ,t n +1 ).
Now, the corrector for RK3 is
x ( n +1) = x ( n ) + (1/6)(k 1 + 2k 2 +2k 3 + k 4 ) .
Another version of RK3 is as follows:
k 1 = (Δt ) f (x ( n ) ,t n ); x ( n +⅓)* = x ( n ) + k 1 /3 ,
k 2 = (Δt ) f (x ( n +⅓)* ,t n+ ⅓ ); x ( n +2/3)** = x ( n ) + (2/3) k 2 ,
k 3 = (Δt ) f (x ( n +2/3)** ,t n +2/3 ) ,
x ( n +1) = x ( n ) + (1/4)(k 1 +3k 3 ) .
RK3 method is third-order accurate with error O ((Δt )4 ). Note that RK3
involves three f () computations per step.
Fourth-order RK method (RK4): The predictors for RK4 are
k 1 = (Δt ) f (x ( n ) ,t n ); x ( n +½)* = x ( n ) + k 1 /2 ,
k 2 = (Δt ) f (x ( n +½)* ,t n+ ½ ); x ( n +½)** = x ( n ) + k 2 /2 ,
k 3 = (Δt ) f (x ( n +½)** ,t n+ ½ ); x ( n +1)*** = x ( n ) + k 3 ,
k 4 = (Δt ) f (x ( n +1)*** ,t n +1 ) .
for k in range(n-1):
tmid = t[k]+dt/2
k1 = dt*f(x[k],t[k])
xmid_1 = x[k] + k1/2
k2 = dt*f(xmid_1,tmid)
xmid_2 = x[k] + k2/2
k3 = dt*f(xmid_2,tmid)
xend = x[k] + k3
k4 = dt*f(xend,t[k+1])
initcond = 1.1
t_RK4,x_RK4 = RK4(f, tinit, tfinal, dt, initcond)
where H.O.T. stands for higher order terms. The above equation shows that
RK4 method is fourth-order accurate, and it has error O((Δt )5 ). RK4
method involves four f () computations per step.
Using Eq. (49) we deduce that the boundary between the unstable and
stable regions for RK4 method is
|1+α (Δt )+ (α (Δt ))2 /2+(α (Δt ))3 /6+(α (Δt ))4 /24| = 1 .
We construct the boundary using a similar code as that used for predictor-
corrector. We exhibit this boundary in Figure 78(b). The region inside
(outside) the boundary is stable (unstable).
def f(x,t):
return x**2-x
xinit = np.array(1.1)
x=odeint(f,xinit,t)
Conceptual questions
1. What are the advantages and disadvantages of RK2 and RK4 methods
over implicit schemes?
Exercises
leapfrog method
where c 1 and c 2 are constants that are determined using initial condition.
Among the two solutions, ρ 1 is genuine, but ρ 2 is a spurious. This feature is
evident from the Taylor expansion of ρ 1,2 (for small Δt ):
where H.O.T. represents higher order terms. For small α (Δt ), ρ 2 ≈ −1,
hence (ρ 2 )n will induce oscillations in x (t ). This is a signature of the
spurious solution. Thus, the leapfrog scheme is unconditionally unstable due
to the presence of ρ 2 . However, if the above instability arising due to ρ 2
could be suppressed (to be described below), then the solution would be
second-order accurate.
We can suppress ρ 2 in a following manner. A first-order ODE requires
only one initial condition. Hence, we can tweak x (0) and x (1) so as to make c
2 = 0. For example, our x and x (1) satisfy the following relations:
(0)
x (0) = c 1 + c 2 ,
x (1) = c 1 ρ 1 + c 2 ρ 2 .
If x (1) = ρ 1 x (0) , the constant c 2 = 0, and c 1 = x (0) . This strategy can be used
to turn off the spurious ρ 2 for the initial condition. Unfortunately, the spurious
ρ 2 may reappear at a later stage due to the round-off errors. To overcome
this difficulty, following measures are recommended:
Once the spurious component of the solution has been suppressed, the
leapfrog method works quite nicely. It is specially useful for solving time-
reversible systems such as Hamiltonian systems, Schrödinger’s equation. We
will address some of these systems later in this book.
In addition to the spurious component, the leapfrog method has several
other disadvantages:
Adams-Bashforth method
In this scheme,
x ( n+ 1) = x ( n ) + (Δt ) f ( x ( n ) , t n ) ,
x ( n+ 1) = x ( n ) + ((Δt )/2) [3 f ( x ( n ) , t n ) − f ( x ( n− 1) , t n− 1 )] .
Note that the former is Euler’s forward method.
Adams-Moulton method
This scheme is very similar to Adams-Bashforth except that its RHS has a
term dependent on x ( n+ 1) . In this scheme,
*******************
Conceptual Questions
Exercises
1. Solve the following differential equations using leapfrog and second-
order Adams-Bashforth methods. Compare your results with the
analytical one.
ẋ = 5x with x (0) = 1
ẋ = −10x with x (0) = 1
ẋ = x − 2 with x (0) = 1
ẋ = x 2 − 50 x with x (0) = 10
SOLVING A SYSTEM OF EQUATIONS
The following Python code implements the above scheme. For the
computation of energy, we interpolate the velocity field at the integer grid
points of Figure 80.
def f(x,v):
return -x # force for the oscillator
for k in range(1,n):
y[k][0] = y[k-1][0] + y[k-1][1]*dt
y[k][1] = y[k-1][1] + f(y[k][0],y[k][1])*dt
return t, y
def ydot_osc(y,t):
return ([y[1], -y[0]])
Conceptual questions
Exercises
Solving ẋ = x 2 − 106 x
The eigenvalues of the matrix of the above equation are −a and − 1. Hence,
the timescales of the above equations are 1/a and 1. When a is very large, the
timescales 1/a and 1 are widely separated, thus making the above system
stiff. For Euler forward method, the above equations are stable only for Δt <
1/a that could be very demanding computationally.
For the above system of equations too, we can employ the strategies
employed for ẋ = x 2 − 106 x . A semi-implicit implementation of the stiff
term yields
y ( n +1) = (1−Δt )y ( n )
or
The above equations are stable for Δt < 1 (coming from the solution for ẏ=
−y ), which is a major gain compared to original projection that Δt < 1/a .
We can also employ the exponential trick to the above equations. Using
the change of variable, x’ = x exp(at ), the equation ẋ + ax = y is
transformed to
ẋ' = y exp(at ) ,
whose solution is
x ( n +1) = [x ( n ) +(Δt ) y ( n ) ] exp(α (Δt )) .
The other equation, ẏ = −y, is easily time advanced using y ( n +1) =(1− Δt ) y
( n )
. Clearly, the new set of equations are unconditionally stable.
This is how we solve the stiff equations numerically. With this, we end
this long chapter on ODE solvers.
*******************
Conceptual questions
1. Why is solving an equation with two very different timescales a difficult
problem?
Exercises
Fourier Transform
FOURIER TRANSFORM
We compute the total energy in Fourier space using the formula ∑|^f n | 2 /2
that yields ½, 1/4, 4, and 12 for the four cases. For example, for the last case,
E = ½[42 + 22 + 22 ] = 12 .
The above energy equals the average energy in real space. Verify the same!
Numerical Fourier transform will yield the above result. Analytical form of
the above integral, called error function, is quite complex to be discussed
here.
For analytical calculations, we take L → ∞, for which k n becomes a
continuum, while the sum of Eq. (58) becomes an integral. We perform the
following transformation: k n → k; ∑ → ∫dn ; and L ^f n = ^f (k )/√(2π) that
leads to the following redefinitions of Fourier transforms:
Under these definitions, the Fourier transform of exp(−(x/a )2 /2) is
In mathematics, Eqs. (61,62) are called Fourier transform , while Eq. (58)
is called Fourier series . However, in this book, following the convention of
computational science, we will call Eqs. (#eq(IFT_1d), #eq(FT_1d))
Fourier transforms.
We can compute {^f n } by performing integration of Eq. (59). We could
employ one of the integration schemes of Chapter Eleven - Numerical
Integration. However, optimised techniques, Discrete Fourier Transform
(DFT) and Fast Fourier transform (FFT), are more common for the FT
computations. These methods are topic of the next two sections.
*******************
Conceptual questions
Exercises
Figure 85: For complex f : Storage of {f j } and {^f n } for N = 8, for which f
0 = f 8 . Python stores {^f n } in an array with indices 0:7; the Fourier modes
^f - 3 , ^f - 2 , ^f - 1 are transferred to ^f 5 , ^f 6 , ^f 7 respectively.
The relationship between {f j } and {^f n } is linear, and there is a one-to-one
transformation from one to other. It is customary to choose the wavenumber
of the N Fourier coefficients from −N/ 2+1 to N/ 2. Under this arrangement,
the formulas for the Discrete Fourier Transform (DFT ) are
Using this property, we shift the Fourier coefficients for n = (−N/ 2+1):(− 1)
to (N/ 2+1):(N− 1). Under this arrangement, the Fourier coefficients are
conveniently saved in an array with indices 0 to N− 1. See Figure 85 for N =
8.
For DFT, Parseval’s theorem translates to
Hence, we have (N− 1) complex numbers for {^f n } with n = 1 to (N /2− 1),
and 2 real numbers for ^f 0 and ^f N/2 . Thus, both {^f n } and {f j } consist of N
independent real numbers. See Figure 86 for an illustration.
Figure 86: For real f : Storage of {f j } and {^f n } for N = 8. Python stores ^f
n in an array with indices 0:N /2. Note that ^f -n = ^f\ n *
Hence, the Fourier coefficients for f ’(x ) are ^f ’ n = i k n ^f n . Note that f ’(x
) of Eq. (#eq(deriv_fourier)) is exact as long as all the Fourier modes are
included in the series. Since every signal has a wavenumber cutoff, denote by
k max , the sum of Eq. (#eq(deriv_fourier)) needs to be carried out from −k max
to k max , not −∞ to ∞. Based on these input, we compute accurate f ’(x ) using
the following three steps:
*******************
Conceptual questions
Exercises
For the discrete Fourier transform, we discretize the real space into ∏N j
small blocks, with N 1 sections along x 1 , N 2 sections along x 2 , …, N d
sections along x d . Hence, at the lattice point (j 1 , j 2 , …, j d ),
Figure 89: (a) A discretized 2D complex field {f j }. (b) The Fourier coefficients {^f n } for the
complex field. The number of complex variables in both real and Fourier spaces are Nx *Ny.
where a i are integers. The above properties hold for both real and complex
fields. For real f (x ), the Fourier coefficients satisfy another important
property, called reality condition :
Due to the reality condition, we save only half of the Fourier modes. We
demonstrate this feature in Figure 90 for a 2D real f (x ). In Fourier space,
only {^f n } with positive n y (modes in the shaded region of Figure 90(b))
are stored inside the computer. As shown in the figure, ^f- 2 , 2 = ^f\ 2,2 , hence
^ f-2 ,2 is not stored. However, all the Fourier coefficients are saved for n y
= 0. For this case, we need to manually set ^ f-nx, 0 = ^ f\ nx, 0.
Figure 90: (a) 2D real field {f j }. (b) The Fourier coefficients {^f n } of {f j }. Only {^f n } in the
shaded zone of (b) are stored in a computer due to the reality condition. Note that ^*f- 2 , 2 = ^ f\ 2,2 .
f = np.zeros((Nx, Ny))
for jx in range(0,Nx):
for jy in range(0,Ny):
f[jx,jy] = 8*np.cos(jx*dx)*np.sin(jy*dy)
fk = np.fft.fftn(f)/(Nx*Ny)
fk = np.fft.fftn(f)/(Nx*Ny*Nz)
With this, we end our discussion on Fourier transforms. In the next chapter,
we will solve partial differential equations (PDEs) using Fourier transforms.
*******************
Exercises
1. Consider the velocity field u (x,y ) = 4[^x sin 2x cos 2y −^y cos 2x sin
2y ] that is periodic in a π 2 box. Using Python functions, compute the
Fourier transforms of velocity field. Compute the average energy in real
space and in Fourier space, and verify Parseval’s theorem.
2. Consider a function f (x,y,z ) =16 (sin 2x cos 5y sin 16z + sin x + cos y
) that is periodic in a (2π)3 box. Compute the Fourier transform and
energy of f (x,y,z ).
3. Consider f (x,y,z ) = exp[−12.5 (x 2 + y 2 +z 2 )]. What is the length scale
of this system? Compute the Fourier transform of this function in the
domain [−5:5, −5:5, −5:5].
4. Compute Fourier transform of f (x,y ) = exp[−12.5 ((x −10)2 + (y −5)2 )]
defined in the domain [0:20]x[0:20].
CHAPTER FIFTEEN
Here, the dependent variable φ (x,t ) is a function of x and time t. The RHS
of the equation contains a function dependent on φ and its spatial derivatives,
as well on t. The domain of the space variable x is [0:L ].
In spectral method, we assume that the function φ is periodic with a
period of L . We take Fourier transform of Eq. (71) that yields ODEs for each
Fourier mode of the system:
1. Diffusion equation
2. Wave equation
3. Burgers eqation
4. Navier-Stokes equation
where ∂t φ is a shorthand for ∂φ/ ∂t. The diffusion equation, which is one of
the most important equations of physics, describes diffusion of temperature,
pollution, etc. in a static medium. Note that the diffusion time L 2 /κ , where L
is the system size, is the time scale of the system. In a diffusion time, the field
φ diffuses considerably, or φ max goes down by a significant factor.
In 1807, Fourier devised Fourier series to solve the diffusion equation
for a given initial condition φ (x , t= 0). Fourier’s procedure to solve the
diffusion is as follows. The diffusion equation in Fourier space is
whose solution is
kappa = 1; tf = 1
L = 2*np.pi; N = 64; h = L/N
j = np.arange(0,N); x = j*h
In the above code, the array f contains the initial condition, while fk contains
its Fourier coefficients. fk_t contains the Fourier coefficients at t = tf . The
inverse transform of fk_t yields f_t , which is the final result in real space.
The initial and final real-space fields are shown in Figure 91. The field f
spreads out as time progresses.
where a = ½ . Hence,
where a’ = √ (a 2 +κt ). With time, the above φ (x,t ) spreads with decreasing
amplitude. The exact solution φ (x,t= 2) is shown in Figure 91, and it
matches closely with the numerical solution (except at the ends).
CFL condition
Even though Eq. (#eq(diffusion_eqn_spectral)) has an exact solution, it is
important to understand the evolution of this equation with Euler’s forward
method (from t n to t n +1 ):
Note that the diffusion time scales for different modes are different. Hence
we have a wide range of time scales making the diffusion equation stiff.
However, for time integration, we need to choose a single Δt for all the
modes, and it has to be the smallest Δt of the system. Therefore,
Conceptual Questions
Exercises
Wave Equation
Many PDEs exhibit wave motion. Some of the leading examples are sound
waves and electromagnetic waves. Here we solve the following PDE that
exhibits wave motion:
∂ t φ + c∂ x φ = 0 , …..(!eq(wave_eqn_xt))
where c (a positive number) is the velocity of the wave.
We solve Eq. (#eq(wave_eqn_xt)) for a given initial condition φ (x ,t=
0). Following the same procedure as that for the diffusion equation, we write
Eq. (#eq(wave_eqn_xt)) in Fourier space that yields the following set of
ODEs:
Figure 92: Under wave action, a wave packet moves to the right without
distortion.
Equation (#eq(wave_eqn_fourier_space)) has a simple solution, hence, no
computational method is required to solve this equation. However, it is
important to see how ODE solvers could be used for solving the wave
equation. The lessons learnt here will be useful for solving nonlinear wave
equations.
Let us employ Euler’s forward method to solve Eq.
(#eq(wave_eqn_fourier_space)). Under this scheme,
Burgers Equation
Following the result on the stability of diffusion equation, we deduce that the
Euler’s forward scheme is stable for Burgers equation when
|1 − νk 2 (Δt )| < 1, or Δt < 2/(νk 2 ).
We need to choose the smallest Δt, which is 2/(νk max ^2^) = 2h 2 /(νπ 2 ) with
h as the grid spacing (see Section Solving PDEs Using Spectral Method:
Diffusion Equation for details). Note that Eq. (#eq(Burgers_uk)) haσ a range
of time scales: 2/(νk max ^2^) to 2/(νk min ^2^). In addition, the nonlinear term
has its own time scale, which is of the order of h /u rms (here, u rms is root
mean square velocity). The existence of so many time scales makes the
equations stiff.
As discussed in Section Stiff Equations, we can solve the stiffness
problem by employing a semi-implicit scheme for the linear term or by using
the exponential trick. Under one of the semi-implicit schemes, called Crank-
Nicolson scheme ,
which is stable.
For the exponential trick, we make a change of variable,
that transforms Eq. (#eq(Burgers_uk)) to
def comput_Nk(fk):
f = np.fft.irfft(fk,N)*N
f = f*f
fk_prod = np.fft.rfft(f,N)/N
return (1j*kx*fk_prod)
for i in range(nsteps+2):
Nk = comput_Nk(fk)
fk_mid = (fk -(dt/2)*Nk)*exp_factor_dtby2
Nk_mid = comput_Nk(fk_mid)
fk = (fk -dt*Nk_mid)*exp_factor
The evolution of u (x ) at different times is shown in Figure 94. Clearly, u (x
) evolves from sin(x ) to a well-formed shock by t = 1.
For our computation, we choose Δt = 0.001, which is much smaller than both
the limits.
KdV Equation
where the nonlinear term ^N k is same as that for the Burger equation.
Therefore, KdV equation could be solved using FFT as in Burgers equation.
An implementation of the diffusive term using Euler’s forward method is
unstable, hence either leapfrog method or the exponential trick is employed
for the time integration.
*******************
Conceptual Questions
1. The solution of Burgers equation shows sharp shocks for very small
viscosity. What kind of numerical problems do you anticipate for such
situations?
2. Study the analytical solution of dissipation-less Burgers equation (with
ν = 0). Try to simulate this equation using energy conserving time-
stepping schemes.
3. Study the KdV equation and its soliton solutions. You may refer to
Drazin [1989] on details.
Exercises
where u , p are the velocity and pressure fields respectively, and ν is the
kinematic viscosity. Equation (81) indicates that the fluid density is constant.
Given initial condition, we solve the above equations in a periodic box of
size L x x Ly x L z .
The numerical method for the above equations is very similar to that for
Burgers equation, except two major differences:
1. Equation (#eq(Navier-Stokes)) has more terms than Burgers equation.
2. We need to take into account the pressure term that is solved using the
constraint equation, Eq. (#eq(incompressible)).
We write down the following equation for the Fourier mode ^u k , i , where i
takes values 1,2,3 for the three components of the velocity field:
which is substituted in Eq. (80). We time advance ^u k , i once all the terms of
RHS have been computed. Application of Euler’s forward method yields
The method is stable only if (see Section Solving Wave and Burgers
Equation)
The nonlinear term provides another timescale, τ NL = h /urms . The ratio of the
two time scales is τ ν /τ NL ≈ u rms h/ν ≈ Re/N, where Re is the Reynolds
number. Therefore, for viscous flows (Re ≾ 1), τ ν is smaller of the two time
scales, and hence Δt is choosen as τ ν . However, for turbulent flows (Re ≫
1),
Here, we use the fact that for turbulent flows, N = Re3/4 . The above relation
shows that τ NL < τ ν for Re ≫ 1. Hence, for turbulent flows, Δt is determined
by the nonlinear term, and CFL condition is not relevant in this case.
We urge the students to write a Python code for solving the Navier-Stokes
equations in 2D and 3D. In 1D, ∇·u = 0 implies that u = constant, hence, 1D
incompressible flows are uninteresting.
With this, we end our discussion on spectral solver of the Navier-Stokes
equation.
*******************
Conceptual Questions
Exercises
1. Solve the incompressible Navier-Stokes equations in a 2D periodic box
of dimension (2π)2 . Take Taylor Green vortex (u x = sin(x ) cos(y ), u y
= −cos(x ) sin(y )) as an initial condition, and ν = 0.1. Evolve the
velocity field till 1 unit of time. You may use (32)2 grid.
2. Repeat Exercise 1 in a 3D periodic box of dimension (2π)3 with (32)3
grid.
3. Solve Euler equation, which is Navier-Stokes equation without
viscosity, in 2D and 3D. Employ random velocity field as an initial
condition. Use leapfrog method for time stepping so as to conserve the
total kinetic energy.
SPECTRAL SOLUTION OF
SCHRÖDINGER EQUATION
where ħ is the Planck constant, and m is the mass of the particle that is
moving in a potential V (r ). We nondimensionalize the above equation using
r a as length scale and ħ / E a as time scale, where Ea is the energy scale. We
make a change of variable t = t’ (ħ / E a ) and r = r ’r a , and derive the
following nondimensional Schrödinger equation:
Since
we obtain
whose inverse Fourier transform yields the following wavefunction at time t
:
# initcond
a = 0.1; k0 = 10; k0a = k0*a
f = 1/np.sqrt(np.sqrt(pi))*np.exp(-
x**2/2)*np.exp(1j*k0a*x) # init cond
fk = np.fft.fft(f,N)/N
kx_pos = np.linspace(0, N//2, N//2+1)
kx_neg = np.linspace(-N/2+1,-1,N/2-1)
kx = np.concatenate((kx_pos, kx_neg))*(2*pi/L)
fk_t = fk*np.exp(-1j*kx**2*tf/2)
f_t = np.fft.ifft(fk_t,N)*N. # final solution in real space
See Figure 97 for an illustration of the solution. The numerical result for the
wavefunction at t ’ = 2 is very close to the analytical result.
where ħ is the Planck constant, g s a constant, and m is the mass of the quasi-
particle that is moving in a potential V (r ). In spectral space, the equation for
the Fourier mode ^ψ k is
which can be solved following a similar procedure as employed for Navier-
Stokes equation. We urge the students to solve the above equation for V (r ) =
0.
In summary, the solvers for Schrödinger and GP equations must ensure
the following:
*******************
Conceptual Questions
Exercises
1. Diffusion equation
2. Wave equation
3. Burgers eqation
4. Navier-Stokes equation
5. Schrödinger equation
The basic steps of the FD method for solving the above equation are as
follows. We discretize the domain [0:L ] at N equidistant points and label
them as x 0 , x 1 , …, x N −1 . The separation between two neighbouring points
is h , and the function at x i is denoted by φ i . Note that the end points are
located at x 0 and x N −1 . For vanishing boundary condition, φ 0 = φ N −1 = 0,
but for periodic boundary condition, φ 0 = φ N . See Figure 99 for an
illustration.
At each x i , we compute the RHS function of Eq. (#eq(PDE_FD0)) and
label it as f i . It is best to use accurate and vectorized schemes (to be
illustrated later) for the spatial derivative computation in f . The
discretization process yields the following ODEs for φ i ’s :
Figure 99: (a) Periodic boundary condition. Here, φ 0 = φ Ν , but φ Ν is not stored. (b) Vanishing
boundary condition with φ 0 = φ Ν− 1 = 0.
which can be solved using one of the time-stepping schemes. For example,
with Euler’s forward method, the time stepping from t ( n ) to t ( n +1) is
achieved using
with κ > 0 and real φ . We solve the above equation for a periodic boundary
condition. For the same we discretize the domain at N points. We compute ∂ 2
φ /∂x 2 using the central difference. Consequently, the equation for φ i is
An application of Euler’s forward scheme to the above equation yields
Note that CFL criteria for the RK2 method will yield a different prefactor for
h 2 /κ.
Example 1: We solve the diffusion equation in a 2π box with exp(−2(x −π)2 )
as an initial condition. We take κ = 1, N = 64, Δt = 0.001, t final = 1 units.
Note Δt < h 2 /(4κ ). hence, our method is stable.
The initial and final fields are shown in Figure 101. The solution using
FD method is quite close to the exact result (see Section Solving PDEs Using
Spectral Method: Diffusion Equation), as well as to the spectral solution. At
each x i , the difference between the spectral and FD solutions is of the order
of 10−4 . A code segment for the above computation is given after the figure.
init_temp = np.exp(-2*(x-pi)**2)
f = np.zeros(N+2); f_mid = np.zeros(N+2);
f[1:N+1] = init_temp; f[0] = init_temp[-1];
f[N+1] = init_temp[0]
To speed up the code via vectorization, we save the field in array f [1:N ],
and impose a condition that f[0] = f[N ] and f[N +1] = f[1] due to periodic
boundary condition (see Figure 102). With this arrangement, we can easily
compute the second derivative, f ’’, using vectorized operations. See the
code-segment above. We follow this convention for all our FD
implementations.
Figure 102: The field φ i is saved in a numpy array f [1:N ]. For vectorization, we impose a condition
that f[0] = f[N ] and f[N +1] = f[1].
The stability criteria for an explicit FD method yields very small Δt (~h 2
/(4κ )) for small h (say 10−4 ). Hence, it is better to employ a semi-implicit
scheme that does not put such a strong constraint. One such scheme is Crank
Nickelson method given below:
For periodic boundary condition, we have N equations of type
(#eq(FD_CN)) for φ 0 , φ 1 , … φ N− 1 (see Figure 99). The equations for φ i ’s
can be written in the following matrix form:
where X = (1+κ (Δt )/h 2 ), Y = −κ (Δt )/(2h 2 ), and r i ’ s are the RHS of Eq.
(90). The matrix in the above equation is a variant of a tridiagonal matrix,
which will be solved in Section Solution of Algebraic Equations.
For the vanishing boundary condition, φ 0 = φ N− 1 = 0, the corresponding
matrix equation is (leaving out φ 0 and φ N− 1 , which are zeros)
*******************
Conceptual questions
Exercises
1. Solve Example 1 for different gird sizes and Δt and identify the
optimum Δt and grid size.
2. Solve 1D diffusion equation for κ = 10, 1, 0.1, 0.01 for vanishing
boundary condition. Choose appropriate Δt .
3. Solve the diffusion equation in a 2D box with κ = 0.1. Take exp(−50r 2 )
as an initial condition. Make sure to employ vectorization for the 2D
solution.
4. Solve the diffusion equation in a 3D box with κ = 0.1. Take exp(−50r 2 )
as an initial condition.
5. Consider ∂t φ = − ∂4 x φ, which is a variant of the diffusion equation.
Solve this equation using FD method. Compare the errors for this
equation with that for the original diffusion equation.
SOLVING WAVE EQUATION
IN THIS SECTION we will solve the following wave equation using the FD
method:
∂ t φ + c∂ x φ = 0, …..(93)
where c, a positive number, is the speed of the wave. The above equation
represents a wave travelling rightwards with speed c . Note that the above
equation is also called advection equation since the field φ is advected by
the wave motion.
We employ central-difference scheme for the space-derivative
computation and Euler’s forward method for time stepping that yields
Since |1− ic (Δt ) sin(kh )/h | > 1, the above integration scheme is unstable
for all Δt .
However, upwind scheme given below is stable:
Here, the spatial derivative is computed using Euler’s backward scheme.
Substitution of φ i ( n ) = exp(ik x i )f ( n ) in Eq. (94) yields
for i in range(nsteps+2):
f[1:N+1] = f[1:N+1] - prefactor*(f[1:N+1]-f[0:N])
f[0] = f[N]; f[-1] = f[1]
if (i%500 == 0):
ax2.plot(x, f[1:N+1], lw = i/500*1)
Figure 103: Numerical solution of the wave equation using FD method: (a)
Central difference scheme, which is unstable. (b) Upwind scheme, which is
stable but dissipative. The black-dashed, orange-thin, and green-thick curves
represent the waves at t = 0, 0.5, 1 respectively.
Note that spectral solution discussed in Section Solving Wave and Burgers
Equation has an exact solution. But, the solution using FD method is
approximate. The implementation of spectral method with Euler method is
unconditionally unstable. For the FD implementation, Euler forward time-
advancement with central-difference spatial derivative is unstable. However,
upwind implementation of FD method is conditionally stable.
With this, we end our discussion on the wave equation.
*******************
Conceptual Questions
Exercises
where u is the velocity field, and ν is the kinematic viscosity of the fluid.
Note, however, that the advection velocity u is not constant.
This system has two time scales for stability analysis: τ ν = h 2 /ν from the
diffusion term and τ NL = h/u rms from the advection term, where h is the grid
size. The ratio τ ν /τ NL = u rms h /ν = Re/N , where N is number of grid points.
We choose smaller of the two timescales as Δt.
We employ upwind scheme for the nonlinear term, and central difference
scheme for the diffusion term. Euler’s forward scheme is employed for time-
stepping. Consequently,
Alternatively, we could also apply RK2 for the nonlinear term and central
difference for the diffusion term. These schemes provide convergent
solutions for a reasonable length of time.
Example 1: We solve the Burgers equation for ν= 0.1, N = 128, and Δt =
0.001. We take sin(x ) as the initial condition and carry out our simulation till
t final = 1 unit using RK2 method. We exhibit the evolution of u ( x ) in Figure
104 with the final profile (purple-thick curve) exhibiting a clear shock front.
Further, we assume that the density is constant. The last equation is pressure
Poisson equation . Here we present a simple method, called pressure
correction (Chorin’s projection ), to solve these equations.
The numerical scheme involves the following steps. First, given u ( n ) ,
we compute the intermediate velocity field u * using the following equation:
The pressure, p ( n +1) is determined using the following Poisson equation:
Conceptual Questions
Exercises
# initcond
a = 0.1; k0 = 10; k0a = k0*a
init_f = 1/np.sqrt(np.sqrt(pi))*np.exp(-x**2/2)
*np.exp(1j*k0a*x)
R = np.zeros(N+2);
R[1:N+1] = np.real(init_f); R[0] = np.real(init_f[-1]);
R[N+1] = np.real(init_f[0])
I = np.zeros(N+2);
I[1:N+1] = np.imag(init_f); I[0] = np.imag(init_f[-1]);
I[N+1] = np.imag(init_f[0])
In Figure 106, we plot the |ψ|, real(ψ ), and imag(ψ ) at t’ = 2. The initial
wave packet is shown as black dashed curve. In the figure, we also plot the
exact values, which are |ψ a | and real(ψ a ). The numerical result for the FD
code is very close to the analytical result.
Scalar product of Eq. (98) with <x | yields the following equation
This is a tridiagonal matrix, which can be solved using a matrix solver (see
Section Solution of Algebraic Equations). By time stepping the above
equation for sufficiently large number of steps, we obtain the final ψ (x,t ).
With this, we stop our discussion on FD implementation of solvers for
the Schrodinger equation.
Assessment of FD Method
*******************
Conceptual Questions
Exercises
Bisection Method
1. We plot f (x ) vs. x and identify two points x l and x r that are on the left
and right sides of the root x *. Thus, we bracket the root between x l and
x r . See Figure 108 for an illustration.
2. We compute the mid point x = (x l +x r )/2.
3. If |f (x )|≤ ε, where ε is the tolerance level, then we have found the root.
We stop here.
4. Otherwise, |f (x )|> ε . Now, if x < x *, x replaces x l , otherwise x
replaces x r .
5. We repeat the above process till |f (x )|≤ ε.
Figure 108: Root finder using bisection method. We start with x l and x r ,
and approach the root iteratively.
Now we estimate the error in the bisection method. Let us denote the two
starting points as x 0 and x 1 , and later x’s as x 2 , x 3 , … x n , etc. Note that
x n +1 = (½) (x n +x n −1 ) . …..(!eq(bisection_method))
Hence, the error, ε n = x\ * − x n , evolves as
ε n +1 = (½) (ε n +ε n −1 ) .
Hence, the error, which is average of the two previous steps, converges
slowly.
def f(x):
return np.exp(x)-6
eps = 0.001; N = 10
x = np.zeros(N); x[0] = 3; x[1] = 1
for i in range(2,N):
mid = (x[i-2] + x[i-1])/2
if (abs(mid-x[i-1]) < eps):
x[i] = mid
break
if f(x[i-2])*f(mid) > 0:
x[i] = mid
else:
x[i-1] = x[i-2]; x[i] = mid
print("i, mid = ", i, mid)
Newton-Raphson Method
Figure 113: Finding the root of f (x ) = exp(x)−6. Plots of x n vs. n for (a)
Newton-Raphson method (Example 3); (b) secant method (Example 5).
Secant Method
This method is called secant method due to the secant passing through the
points (x n ,f (x n )) and (x n− 1 ,f (x n− 1 )). Also, the convergence of the secant
method is similar to that of Newton-Raphson method, that is, errors converge
as in Eq. (102).
Relaxation Method
For some fixed points, called stable fixed points , the iteration converges,
that is,|x n +1 −x n | < ε. The fixed points for which the iteration does not
converge are called unstable fixed points . Figure 115 illustrates these
points. The left FP is stable, while the one in the middle is unstable. The
diagram of Figure 115 is also called a web diagram because the arrows form
a web.
Figure 116: The web diagram for finding the fixed points of f (x ) = x using the relaxation method.
x n = f − 1 (x n+ 1 )
will lead us to the unstable FP. Note that reversal of the arrows is equivalent
to going back in time.
Python’s scypy module has several functions for finding the roots of
equations. These functions are brentq, brenth, ridder , and bisect, whose
arguments are the function and the bracketing interval. Here we discuss how
to use the function brentq . The following code segment finds the roots of g
(x ) = x 2 −1 and f (x ) = sin(πx )−x .
In [314]: optimize.brentq(f,0.5,
1)
Out[314]: 0.7364844482410411
Let us review the root finders discussed in this chapter. The bisection method
is quite robust and simple, and it is guaranteed to converge. The convergence
however is slow. On the other hand, Newton-Raphson and secant methods
converge quickly. These two methods however fail if |f ’’(x *)/f ’(x *)|
diverges. We illustrate this issue using f (x ) = x 1/3 as an example. The
bisection method can find the root of f (x ) = x 1/3 , but Newton-Raphson and
secant methods fail to do so.
The relaxation method is an efficient and direct method to find the
solution of f (x ) = x . This method is very useful in dynamical systems.
We will employ root finders in the next chapter where we solve boundary
value problems.
*******************
Conceptual Questions
Exercises
Shooting Method
Figure 119: The solutions of y’’ = −a 2 y using the shooting method for (a) a = 1; y (0) = 0; y (1) =
0.5; (b) a = 5; y (0) = 0; y (1) = 0.1. The first two trial solutions are shown as red and green dashed
curves below and above the real solution (black dashed curve). The final solutions are close to the
respective exact results.
For boundary condition y (0) = 0 and y (1) = A , the equation y’’ = −a 2 y has
an exact solution, y (x ) = A sin(ax )/sin(a ). The slope at x = 0 is Aa/ sin(a );
the shooting method endeavours to reach to this slope. Note that the slope is
negative for a = 5, consistent with the numerical solution.
A code segment for the above problem is given below:
def ydot(y,x):
return ([y[1], -a**2*y[0]])
slope_b = 0.1
yinit = np.array([0, slope_b])
y = odeint(ydot, yinit, x)
ax2.plot(x,y[:,0],'g--', lw=1)
iter = 0
while ((abs(y[-1,0]-end_val) > tollerance) and (iter < 20) ):
slope_mid = (slope_t+slope_b)/2
yinit = np.array([0, slope_mid])
y = odeint(ydot, yinit, x)
ax2.plot(x,y[:,0])
if (y[-1,0]>end_val):
slope_t = slope_mid
else:
slope_b = slope_mid
iter = iter +1
Figure 120: Solution of d 2 φ /dt 2 = −g sinφ using the shooting method. The
first two trial solutions are green and red dashed curves. The final curve is
the black dashed curve.
With this, we end our discussion on shooting method.
*******************
Conceptual Questions
Exercises
1. The shells from the Bofors gun leave the muzzle with an speed of 1
km/sec. The shell is required to hit a target 10 km away. Use shooting
method to compute the angle of launch of the shell. Assume the surface
to be flat. Ignore air friction.
2. Redo Exercise 1 in the presence of air drag. Assume kinematic
viscosity of air to be 0.1 cm2 /sec, and cannon balls to have a diameter
of 20 cms.
3. Solve the equation ẍ = −x − γẋ with γ = 0.1 for the boundary conditions
x (0) = 0 and x (1) = 1.
4. Numerically solve the equation εy’ ’ + y ’ = 0 for ε = 0.001 and the
boundary conditions y (0) = 0 and y (1) = 1. This is a stiff equation.
EIGENVALUE CALCULATION
Solving y’ ’ = λ y
a_prev = a; a = -1
iter = 0
while ((abs(a-a_prev) > tollerance) and (iter < 10)):
y = odeint(ydot, yinit, x)
ax1.plot(x,y[:,0])
For the first eigenvalue, we start the iteration process λ = −2 and −1 and
employ the secant method to reach the desired λ within precision of 0.01. In
4 iterations we converge to −2.47, which is quite close to the exact value −π
2
/4. In Figure 121(a), we exhibit y (x ) computed at each iteration. The initial
y (x ) is shown as a red-dashed curve. The final curve (after 4 iteration) is
quite close to the exact solution, which is exhibited as black-dashed curve.
Equation (106) has infinite many eigenvalues (see Eq. (107)). We can
obtain these eigenvalues by starting the iteration with guessed value of λ. The
second eigenvalue (-9.869606001069714 ≈ π 2 ) can be reached by starting
the iterations with λ = −11 and −8. We illustrate the second eigenfunction in
Figure 121(b).
Next, we compute the eigenvalues and eigenfunctions for a quantum
particle moving in a potential.
Figure 121: First two eigenfunctions of equations y’ ’ = λ y with the
condition that y (1)=0. For the two cases, the final solutions is close to the
exact solutions, which are shown as black dashed curves. The initial y (x ) is
shown as red-dashed curve.
Figure 122: The eigenfunctions of equations −(½)y’ ’ + V (x )y = E y: (a) Ground state, (b) the first
excited state. The normalized potential V (x )/100 is exhibited using the green-chained curve in (a), and
the initial y (x ) using the red dashed curves in both the plots. The final y (x ) are shown using black-
dashed curves.
*******************
Exercises
Figure 123: (a) The grid for Laplace equation. φ i,j is average of φ ’s of the nearest neighbours. (b)
Grid points for the Red-Black method.
We test if
where ε is the tolerance limit. If the error is larger than ε, we continue the
iteration. Otherwise, we stop the iteration and report the final φ as the
desired solution.
y = np.zeros([N+2,M+2])
y[0,:]=V
error = np.max(np.absolute(yp[1:N+1,1:M+1]-
y[1:N+1,1:M+1]))
y = yp.copy()
Figure 124: (a) The initial potential profile in which the left wall has φ = 1, while φ = 0 elsewhere. (b)
The final potential φ computed using Jacobi method.
Jacobi method employs φ of the previous step to update φ i,j (see Eq. (111).
Note however that when we reach φ i,j in the loop, we have already updated
all the points up to (i,j ). In Gauss-Seidel scheme, we use these already
updated values for computing new φ i,j , that is,
error = np.max(np.absolute(yp[1:N+1,1:M+1] \
- y[1:N+1,1:M+1]))
For Example 1, the Gauss-Seidel method takes 2918 iterations (less than
Jacobi method) to converge to the desired accuracy, but the time taken is
approximately 15 seconds, which is much larger than that for the Jacobi
method. This is because the Gauss-Seidel loop cannot be vectorized. Note
however that the convergence of Gauss-Seidel method is faster than that for
the Jacobi scheme (see Figure 125).
Next, we present Red-Black method, which is a vectorized
implementation of Gauss-Seidel scheme.
Red-Black Method
In this scheme, we classify the grid points as red and black, as shown in
Figure 123(b). The red grid-points are updated first, and then the black grid-
points are updated. Fortunately, this process can be vectorized, as shown in
the following code segment.
y[2:N+1:2,2:M+1:2] = (y[1:N:2,2:M+1:2]
+y[3:N+2:2,2:M+1:2] \
+ y[2:N+1:2,1:M:2] \
+y[2:N+1:2,3:M+2:2])/4
y[1:N:2,2:M+1:2] = (y[0:N-1:2,2:M+1:2]
+ y[2:N+1:2,2:M+1:2] \
+ y[1:N:2,1:M:2] \
+ y[1:N:2,3:M+2:2])/4
y[2:N+1:2,1:M:2] = (y[1:N:2,1:M:2]
+ y[3:N+2:2,1:M:2] \
+ y[2:N+1:2,0:M-1:2] \
+ y[2:N+1:2,2:M+1:2])/4
error = np.max(np.absolute(tmp[1:N+1,1:M+1] \
-y[1:N+1,1:M+1]))
sor = 1.5
for i in range(1,N+1):
for j in range(1,M+1):
y[i,j] = sor*(y[i-1,j] +y[i+1,j] \
+ y[i,j-1] +y[i,j+1])/4 + (1-sor)*y[i, j]
*******************
Conceptual Questions
Exercises
1. Solve Example 1 for various grids, e.g., 130x130, and examine the
dependence of the convergence on the grid size.
2. Example 1 employs second order scheme for the computation of ∇2 φ .
Redo the exercise using fourth order scheme for the derivative
computation. Compare the convergence.
3. Consider a box of unit dimension. Given that the left, right, bottom, and
top walls are kept at a electric potential of 1, 2, 3, 4 Volts respectively,
compute the potential inside the box using Jacobi, Gauss-Seidel, Red-
Black, and SOR methods. Compare the time taken by these methods.
4. Solve for the electric potential inside a box of dimension 2x1x1. The
left wall of the box is at kept at a potential of 1 Volt, while the other
walls are at zero potential.
SOLVING POISSON EQUATION
∇2 φ = f .
The above equation is solved for given f and boundary condition. The
computation methods for Poisson equation are very similar to those for
Laplace equation.
The discretized version of the Poisson equation at the grid point (i,j ) is
where h is the grid spacing. We can write the equations as the following
matrix equation:
where A is a banded matrix. This equation is solved using matrix methods,
which will be described in Section Solution of Algebraic Equations.
However, the iterative methods are simpler to implement. In Jacobi
method, φ is updated using the following equation:
The implementation of Jacobi method for Poisson equation is same is that for
Laplace equation.
Example 1: We solve the equation ∇2 φ = exp(−20 r 2 ) in a 2D box
[−1:1,−1:1] with The potentials φ at all the side walls are kept at zero. We
employ a grid of 64x64 inside the box, hence, h = 2/64. We use the Jacobi
scheme for iteration, and obtain the final solution for tolerance of 10−6 .
Figure 126(a) illustrates the source term exp(−20 r 2 ), while Figure 126(b)
shows the final solution. The following Python code implements the above
procedure.
y = np.zeros([N+2,M+2])
# set BC
y[0,:]= 0; y[N+1,:]=0
y[:,0]=0; y[:,M+1]=0
yp = y.copy()
# Jacobi method
while (error > eps):
yp[1:N+1,1:M+1] = (y[0:N,1:M+1] +y[2:N+2,1:M+1]
+ y[1:N+1,0:M] +y[1:N+1,2:M+2])/4
yp[1:N+1,1:M+1] = yp[1:N+1,1:M+1]
+ h**2*f[1:N+1,1:M+1]
error = np.max(np.absolute(yp[1:N+1,1:M+1]
-y[1:N+1,1:M+1]))
y = yp.copy()
Figure 126: (a) The source term exp(−20r 2 ). (b) The potential φ generated by the source term.
*******************
Conceptual Questions
Exercises
1. Splines,
2. Solving equations for implicit ODE solvers
3. Solution of Laplace and Poisson’s equation.
Here, adj(A) is the adjoint of A , and det(A) is the determinant of the matrix,
which is defined as the following for a n x n matrix:
where sign = +1 for even permutations of j i ’s, and sign = −1 for odd
permutations. Note that det(A ) has n ! terms, hence the number of
computations required to compute the determinant is O (n n ), which is
enormous for large n . Therefore, the inverse of a matrix and the solution of A
x = b are not computed using Eq. (114). Gauss elimination, to be discussed
below, is one of the popular methods for solving linear equations.
Gauss Elimination Method
We write equation A x = b as
and solve for x in three steps:
The element a 00 , which is called pivot, plays a critical role here. After this
step, we eliminate a i 1 from rows i = 2:(n −1), and so on. This process
finally leads to
Here, a ij ^(m ) represents the the matrix element after m th level of
elimination.
(b): Solution of the last equation yields
(c) Back substitution : We solve for xn −2 using the second last equation,
which is
The final matrix is of the upper triangular form. We solve for x 2 using the
equation of the third row, 4 x 2 = 12, which yields x 2 = 3. After this, we
compute x 0 and x 1 using back substitution. The equation of the second row is
−x 1 + x 2 = 1 that yields x 1 = 2. Substitution of x 1 in the equation of the first
row, x 0 + x 1 = 3, yields x 0 = 1. Thus, the solution of the three linear
equations is x = [1,2,3].
Gauss elimination method has several problematic issues:
LU Decomposition
The vector in the RHS has 1 in the i th row and zeros everywhere else. This
process is carried out to compute x i ’s for i = (0:n −1). It is easy to show
that
A −1 = [x 0 , x 1 , …, x i, …, x n −1] .
Python’s scipy.linalg module has functions to solve linear equations, as well
as for LU decomposition. We illustrate these functions using the following
code segment:
In [179]:
x
In the matrix ab , the top row is the upper diagonal, the middle row is the
main diagonal, and the bottom row is the lower diagonal. We can put any
number for – in the matrix ab . However, it cannot be left blank.
We illustrate the above functions as follows:
In [192]: ab = np.array([[0,1,1],[1,2,3],
[3,1,0]])
In [193]: b =
np.array([3,10,11])
Conceptual Questions
Exercises
Figure 127: (a) For a typical vector x , A x is not in the same direction of x .
(b) The eigenvector x 0 and A x 0 are in the same direction, so are the second
eigenvector x 1 and A x 1.
Eigenvector x i satisfies the following equation:
A x i = λi x i ,
where λ i is the eigenvalue corresponding to x i . Note that λi could be real or
complex. For a n x n matrix, there are n eigenvalues, but the number of
eigenvectors are n or less. Note that the eigenvalues need not be distinct
(Mathews and Walker [1970]).
The eigenvalues and eigenvectors have many interesting properties, but
we will not discuss them here. In addition, we will not discuss the QR
algorithm which is often used for the computation of eigenvalues and
eigenvectors.
Numpy.linalg has several functions for computing eigenvalues and
eigenvectors of a matrix. They are
Using the function eigh (A ), we find the eigenvalues to be 1 and 3, and the
corresponding eigenvectors to be x 0 = [−1/√2, 1/√2] and x 1 = [1/√2, 1/√2]
respectively. The eigenvectors, illustrated in Figure 128, are perpendicular
to each other. See below for the Python code:
In [83]:
eigh(A)
Out[83]:
(array([1., 3.]), array([[-0.70710678, 0.70710678],
[ 0.70710678, 0.70710678]]))
In [214]:
vals
In [215]:
vecs
Out[215]:
array([[ 0.56151667, 0.33655677, 0.22941573],
[-0.79410449, 0.47596315, 0.6882472 ],
[ 0.23258782, -0.81251992, 0.6882472 ]])
In [218]:
vals
In [219]:
vecs
Out[219]:
array([[ 0. , 0.81649658, -0.57735027],
[-0.70710678, -0.40824829, -0.57735027],
[ 0.70710678, -0.40824829, -0.57735027]])
In [26]: x =np.array([1, 1,
1])
In [27]: x =
x/norm(x)
In [32]: x # (vector
**S**)
In [34]:
norm(dot(A,x))/norm(x)
Out[34]: 4.0000000001217835
Conceptual Questions
Exercises
Random Numbers
where μ is the mean of the variables, and σ is the standard deviation (std in
short). Νote that at x = μ ± σ, P (x ) falls by a factor of 1/√e . See Figure
129(b) for an illustration; here μ = 5 and σ = 5.
Figure 129: (a) Uniform probability distribution P (x ) = 1/40 in the interval [10,50]. (b) Normal
probability distribution with mean = 5 and std = 5.
Python offers several functions to generate random numbers. They are given
below (reproduced from Numpy Arrays):
random.rand (shape ): Returns a float array of given shape with random
numbers sampled from a uniform distribution in [0,1].
random.randn (shape ): Returns a float array of given shape with random
numbers sampled from a Gaussian (normal) distribution.
random.randint(l ow , high=None, size=None, dtype=int ): Returns an
integer array of given size with random numbers sampled from a uniform
distribution in [low, high ). If high is None, the random numbers are sampled
from [0, low ).
We illustrate the usage of the above functions using several examples.
In [35]: x = np.random.rand(500)
We can change the range of the random numbers to [a,b ] using the following
trick. We multiply x with (b−a ) and then add a to the same:
You can verify the minimum and maximum elements of y. The probability
distribution P (y ) = 1/(b−a ) in the band [a,b ]. See Figure 129(a) for an
illustration.
In [40]: x = np.random.randn(500)
In [42]: x = mu + sig*np.random.randn(500)
In [56]: x = np.random.randint(0,11,1000)
import pandas as pd
dat = pd.read_csv("weight-height.csv")
h_male = np.array(dat['Height'][0:5000])
h_female = np.array(dat['Height'][5000:10000])
w_male = np.array(dat['Weight'][0:5000])
w_female = np.array(dat['Weight'][5000:10000])
num_bins = 50
n, bins, patches = ax1.hist(h_male, num_bins)
ax2 = fig.add_subplot(1,2,2)
mu = np.mean(h_male)
st = np.std(h_male)
my_bins = (bins[1:num_bins+1] + bins[0:num_bins])/2
h = bins[1]-bins[0]
P2 = h*np.exp(-0.5*((my_bins-mu)/st)**2)
/(st*np.sqrt(2*np.pi))
Figure 130: (a) Histogram of heights of males (height in inches). (b) PDF of height. The blue curve
represents the PDF of the data, while red-dashed curve is the associated Gaussian PDF.
We exhibit the histogram and associated PDF in Figure 130. The average and
std of male height are 69 inches and 2.86 inches respectively. We compute a
Gaussian PDF using the mean and std, and exhibit it as red-dashed curve in
Figure 130(b); this PDF matches well with the PDF of the data. Thus, we
provide a numerical demonstration that the heights of human males follow a
Gaussian distribution.
With this, we end our discussion on random numbers.
*******************
Conceptual questions
Exercises
Figure 131: (a) Plot of y = x 2 . (b) Plot of f (x ) = cos2 (1/(x (1-x )). (c) Plot of a circle whose
equation is x 2 +y 2 = 1
def f(x):
return x**2
N = 1000000
count = 0
for i in range(N):
x = np.random.rand()
y = np.random.rand()
if y<f(x):
count += 1
We observe that the the integral fluctuates around a mean. For N = 106 , one
of the outcomes is 0.333967, which is close to the actual answer of 1/3. The
error for the integral is approximately 0.00063367, which is O (√N ) or 10−3
.
d=2
N = 10000
count = 0
for i in range(N):
x = 2*np.random.rand(d)-1
if sum(x**2) < 1:
count += 1
The above examples illustrate that Monte Carlo method is a handy tool for
integration, but the results have significant errors, which are of the order of O
(√N ), where N is the number of points used.
*******************
Conceptual questions
Exercises
Linear Regression
Many systems exhibit linear behaviour. For example, the voltage across a
resistor is proportional to the current passing through it. The force on a
spring is proportional to the displacement of the spring. In the following
discussion, we will take a demographic example.
Figure 132: Plots of height (h ) vs. weight (w ) for 5000 men (a) and 5000
women (b). The dots represent the sample data, and the straight line in the
middle is the best-fit curve obtained using linear regression.
It has been observed that the weight of a healthy individual is proportional to
his/her height. We download the height and weight data of 5000 men and
women from the website https://www.kaggle.com/mustafaali96/weight-
height . Here, the height is in inches, and weight is in pounds. First we plot
the height vs. weight data for men and women separately. The data plotted in
Figure 132 exhibits linear relations between the weights and heights, both for
men and women. In the following, we perform linear regression on this data
set.
The process of linear regression is as follows. We postulate a linear
relation between the height h and weight w of a population:
w=ah+b,
with a and b are unknown constants. Most data points do not lie on the linear
curve, hence the function w = a h + b is an approximate or guessed
relationship, not an exact one.
We derive the constants a and b by minimising the squared error E :
E = ∑i (wi − a h i −b )2 , …(!eq(regression_err_w))
where wi and h i are the weight and height of i th individual. The
minimization criteria,
∂a E = 0 and ∂b E = 0 ,
Example 1: For the data of Figure 132, we compute a and b using the
formulas of Eqs. (#eq(regression_a)) and (#eq(regression_b)). The statistics
for both men and women are listed in Table 31. We verify that the output of
polyfit provides the same a and b as Eqs. (#eq(regression_a)) and
(#eq(regression_b)).
Note that Python lists the above numbers with 16 significant digits.
However, they are not as accurate, as is evident from the standard deviations
listed in the table. For example, σ h = 2.9 inches for the males, hence, it is not
prudent to report ⟨h ⟩ with more than 1 significant digit after the decimal
point.
Nonlinear Regression
n = 21
x = np.arange(n)
y = 0.2*x + 0.3*x**2 -1 + np.random.rand(n)
[Figure 133: Plot of y = 0.2 x + 0.3 x 2 − 1 + η (dots) along with best fit
curve (solid curve)]
The function np.polyfit(x, y,3, full=True) produces the following
*******************
Conceptual Questions
Exercises
Ising model is often used to study phase transition. In this model, the spins
that take values +1 or −1 are placed on a d- dimensional grid. The energy of
the Ising system is
E=−J∑SiSj,
where J is the coupling constant, and the sum is performed over nearest
neighbours. In Figure 134(a), we illustrate the nearest neighbours of Ising
spin X .
Figure 134: Illustrations of the Ising spins in (a) a disordered phase, and in (b) an ordered phase. In
(a), the corners of the square represent the nearest neighbours of the spin X .
def energy(A):
return (-np.sum(A[1:N+1,1:M+1]*A[0:N,1:M+1] \
+ A[1:N+1,1:M+1]*A[2:N+2,1:M+1] \
+ A[1:N+1,1:M+1]*A[1:N+1,0:M] \
+ A[1:N+1,1:M+1]*A[0:N,2:M+2]))
Enow = energy(A)
E[0] = Enow
m[0] = np.sum(A)/(N*M) # magnetization
It has been observed that the correlation function of spins exhibits a power
law near the phase transition (Bak et al. [1986]). This is termed as critical
behaviour. Note, however, that phase transition in Ising system is achieved
by tuning the temperature. Interestingly, there are systems that approach
criticality without external tuning. This phenomena is called self-organised
criticality , a topic of the present discussion.
In 1986, Bak et al. considered a 2D grid with open boundaries. Each grid
point contains sands ranging from 0 to 3, hence it is called a sandpile . In
Figure 136, we illustrate different snapshots of a sandpile on a 4x4 grid.
We initialise the sandpile with random numbers in the range 0 to 3. After
initialization, we update the sandpile as follows:
Figure 136: Four snapshots of a sandpile. A new sand falls at site whose occupation number is in bold.
This event affects sites coloured in red. In frame (a), a sand is dropped at (2,1) making the count of
sands at that site to go to 4. The 4 sands at (2,1) are distributed to the neighbours. In frame (b), a new
sand arrives at (3,3). The critical site (3,3) distributes its sands to two of its neighbours, while the other
two sands are lost at the boundary. In frame (c), a new sand arrives at (2,2). The critical site (2,2)
distributes its sands to its neighbours. However, the distribution makes the site (2,3) critical. The sands
from this site are sent to its neighbours, but one sand is lost at the boundary. The frame (d) is the final
state.
We illustrate the above processes in Figure 136. The new sand arrives at a
site whose occupation number is in bold, while the affected sites are
coloured in red. The figure contains four consecutive snapshots, while the
figure caption explains the transitions.
A Python implementation of the above algorithm is given below. Note
that the function update () is recursive.
N = 4; M = 4
nsteps = 40
cnt = np.zeros(nsteps, dtype = int)
A = np.random.randint(0,4,(N,M))
Conceptual Questions
Exercises
1. Think of algorithm first, then write pseudocode, and then write your
program. Write your algorithm in a piece of paper. Do not start typing
your code right away.
2. Think of extreme cases! Most codes break for these cases.
3. Code slowly!
4. Code in parts, and test each part. Python gives you this flexibility.
5. Code with confidence!
6. First write a code that works, and then make it efficient
7. Comment your code!
What Next?
The tools discussed in this book will enable you to solve moderate-size
problems, but not large ones, e.g., weather prediction codes and Monte Carlo
simulations of a billion particles. We need the following advanced tools for
solving large-scale problem:
1. Turbulence
2. Human brain
3. Universe, stars, and planets
4. Oceans and Atmospheres
5. Human body
6. Complex materials and Drugs
7. Human behaviour and Artificial Intelligence (AI)
Future hardware and computing tools will be even more powerful, and they
will accelerate the pace of research in complex systems. I encourage young
students to learn advanced computing so that they can venture into these
challenging areas of science and engineering.
Limits of Computation
Preliminaries
where x i are the given data points, and P(t ) is the extrapolating n -th order
polynomial. It is easy to see that g (t =x i ) = 0 and g (t =x ) = 0. Hence g (t
)=0 at n +1 points. Therefore, according to Rolle’s theorem, g’ (t )=0 at n
points. Continuing this argument, we conclude that the second derivative of g
, g (2) (t )=0 at n -1 points, ..., and g ( n ) (t )=0 at one point. We denote this
point of vanishing g ( n ) (t ) by ζ. Setting g ( n ) (ζ ) = 0, we obtain
Q.E.D.
APPENDIX B: IMPROVING
ACCURACY USING RICHARDSON
METHOD
where H.O.T. stands for the higher order terms. The exact result is I , but
numerical value differs from it due to the errors arising because of finite h .
With h /2, the numerical value is improved, but the accuracy is still O (h ):
Interestingly, using the above two equations, we can improve the accuracy of
the numerical value to O (h 2 ) using the following trick:
Note that the above scheme is quite general, and it can be applied to all kinds
of numerical methods—quadrature, derivatives, ODE solvers, etc.
REFERENCES
BOOKS
Journal Papers