Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
188 views

Notes Python

NumPy is a Python library that allows for efficient manipulation of large multi-dimensional arrays and matrices. It provides support for vectorized mathematical and other operations on these arrays. NumPy arrays can be created from Python lists and support fast element-wise arithmetic operations. NumPy random functions can generate random integers and floating point numbers to populate arrays for testing purposes.

Uploaded by

Robert Cullen
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
188 views

Notes Python

NumPy is a Python library that allows for efficient manipulation of large multi-dimensional arrays and matrices. It provides support for vectorized mathematical and other operations on these arrays. NumPy arrays can be created from Python lists and support fast element-wise arithmetic operations. NumPy random functions can generate random integers and floating point numbers to populate arrays for testing purposes.

Uploaded by

Robert Cullen
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 93

1.

Working with NumPy


NumPy (pronounced as "num-pie" or "num-pee")is a Python library that can be
imported to perform basic matrix algebra. In principle, it provides support for large multi-
dimensional arrays and mathematical functions that can act on these arrays.
Please read more at: https://numpy.org/doc/stable/user/index.html
Arrays in NumPy -- Lists with more power
In [8]: import numpy as np

a = [i for i in range(20)] # creating a list in the usual sense


print (a)

b = np.array(a) # creates a numpy array of the list


b

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
Out[8]:
17, 18, 19])

In [9]: print (a + a) # creates a copy of the original list


b + b # Performs vector or matrix addition

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32,
Out[9]:
34, 36, 38])

In [10]: print (a * 3) # creates a copy of the original list 3 times


b * 3 # Element wise multiplication by an integer/scalar

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
array([ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48,
Out[10]:
51, 54, 57])

In [11]: b * 3.7 # Element wise multiplication by a float/scalar

array([ 0. , 3.7, 7.4, 11.1, 14.8, 18.5, 22.2, 25.9, 29.6, 33.3, 37. ,
Out[11]:
40.7, 44.4, 48.1, 51.8, 55.5, 59.2, 62.9, 66.6, 70.3])

In [12]: type(a)

list
Out[12]:

In [13]: type(b)

numpy.ndarray
Out[13]:

How fast is np.array compared to list?


Let us square every element in a list or array.
In [14]: print ([i for i in range(10)])
print (np.arange(10)) # This is the array version of range() in lists
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0 1 2 3 4 5 6 7 8 9]

In [15]: %timeit -r1 [i**2 for i in range(1000)]

149 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 10,000 loops each)

In [16]: arr = np.arange(1000)


%timeit -r1 arr**2 # This squares every elemnt in the list and runs very

779 ns ± 0 ns per loop (mean ± std. dev. of 1 run, 1,000,000 loops each)

Pre-defining an array for use in loops and matrix algebra


In [17]: σ_x = np.empty([2,2]) # All elements are empty with any random values.
σ_x[0,0] = 0
σ_x[0,1] = 1 # Type the elements individually or using a loop.
σ_x[1,0] = 1
σ_x[1,1] = 0

print (σ_x)

[[0. 1.]
[1. 0.]]

In [18]: mat_0 = np.zeros([3,3]) # All elements are initialised as float 0.


mat_0

array([[0., 0., 0.],


Out[18]:
[0., 0., 0.],
[0., 0., 0.]])

In [19]: mat_1 = np.ones([2,2,2]) # Creates a 3D array (tensor), with all elements i


mat_1

array([[[1., 1.],
Out[19]:
[1., 1.]],

[[1., 1.],
[1., 1.]]])

In [20]: mat_identity = np.eye(4) # Creates a 2D array with all diagonals as 1 (iden


mat_identity

array([[1., 0., 0., 0.],


Out[20]:
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])

NOTE 1: One can use np.ndarray to create an array. Here, all the elements are initialised
with some random number. However, this is not recommended.
NOTE 2: The use of the np.matrix subclass is not recommended as it may get
deprecated in the future. NumPy forums recommend only the use of np.ndarray class
for all matrices.
Shape and dimension of an array
In [21]: mat_1.ndim # returns the dimension of the array

3
Out[21]:

In [22]: mat_1.shape # returns a tuple with the shape or all the dimensions of the ar
(2, 2, 2)
Out[22]:

In [23]: num_list = np.arange(10) # creates a 1D array with shape 10


print (num_list)
num_list.shape

[0 1 2 3 4 5 6 7 8 9]
(10,)
Out[23]:

In [24]: print(num_list.reshape(2,5)) # one can reshape it into a 2D array with shape


num_list.reshape(2,5).shape

[[0 1 2 3 4]
[5 6 7 8 9]]
(2, 5)
Out[24]:

In [25]: print(num_list.reshape(4,2)) # cannot reshape it (4,2) as no. of elements mu

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Input In [25], in <cell line: 1>()
----> 1 print(num_list.reshape(4,2))

ValueError: cannot reshape array of size 10 into shape (4,2)

Accessing elements of an array


In [26]: num_list = np.arange(16)
mat = num_list.reshape(4,4)
print (mat) # Elements are arranged by rows and columns while reshaping

[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]

In [27]: mat[0,2] # the containers for accessing element 2D arrays (row index, column

2
Out[27]:

In [28]: tens = num_list.reshape(2,2,4)


print (tens) # Elements are arranged by dim 1, dim 2,...,dim n while reshap

[[[ 0 1 2 3]
[ 4 5 6 7]]

[[ 8 9 10 11]
[12 13 14 15]]]

In [29]: tens[0,1,3] # accesses 1st, 2nd, 4th element from the tensor

7
Out[29]:

In [30]: tens[0,2,3] # calls 1st, 3rd, 4th element from the tensor, but 3rd element

---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Input In [30], in <cell line: 1>()
----> 1 tens[0,2,3]

IndexError: index 2 is out of bounds for axis 1 with size 2


2. Using random numbers
Working with random integers
In [31]: A = np.random.randint(5,size=(3, 4)) # Random array with shape (3,4) filled
print ('Matrix A is:\n', A)

Matrix A is:
[[4 3 0 1]
[4 3 4 2]
[4 0 2 4]]

In [32]: A = np.random.randint(1,[3,5,10]) # Random array with shape (1,3)


# Integers with lower bound 1 and upperbou
print ('Matrix A is:\n', A)

Matrix A is:
[1 4 1]

In [33]: A = np.random.randint([1, 3, 5, 7], [[10], [20]]) # Random array with shape

print ('Matrix A is:\n', A)

Matrix A is:
[[ 1 7 8 8]
[17 13 14 12]]

Working with general random numbers (floats)


In [34]: A = np.random.rand(3,2) # Random array with shape (3,2) with uniform random
print ('Matrix A is:\n', A)

Matrix A is:
[[0.960704 0.31006085]
[0.00543051 0.33408133]
[0.26585241 0.53001972]]

In [35]: A = np.random.randn(3,3) # Random array of shape (3,3) from a Gaussian dist


print ('Matrix A is:\n', A)

Matrix A is:
[[-0.25045079 -2.08384439 -1.07010669]
[-0.51716863 0.69883823 -1.46131635]
[ 0.47136285 -1.33294674 1.46969341]]

3. Arithmetic on arrays and matrices


Different ways to add and multiplying
In [36]: A = np.random.randint(5,size=(3, 4)) # Random array with shape (3,4) filled
B = np.random.randint(5,size=(3, 4))

print ('Matrix A is:\n', A)


print ()
print ('Matrix B is:\n', B)
print ()
print ('The sum is:\n', A + B) # Both A and B must have same shape
Matrix A is:
[[0 4 3 0]
[3 4 3 4]
[3 0 4 4]]

Matrix B is:
[[3 1 1 2]
[1 0 1 4]
[4 3 4 0]]

The sum is:


[[3 5 4 2]
[4 4 4 8]
[7 3 8 4]]

In [37]: A = np.random.randint(5,size=(3, 4)) # Random array with shape (3,4) filled


B = np.random.randint(5,size=(3, 4))

print ('Element-wise multiplication:\n', A * B) # Element wise multiplicati


print ()
print ('Element-wise exponentiation:\n',A ** 2) # Element wise exponentiatio

Element-wise multiplication:
[[ 0 6 0 2]
[12 3 2 4]
[ 2 12 0 4]]

Element-wise exponentiation:
[[ 0 4 0 1]
[ 9 1 1 1]
[ 1 16 0 4]]

In [38]: A = np.random.randint(5,size=(3, 4)) # Random array with shape (3,4) filled


B = np.random.randint(5,size=(3, 4))

print ('Matrix multiplication:\n') #


print ()
A @ B # Matrix multiplication; does not work due to mismatch of shape betwe

Matrix multiplication:

---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Input In [38], in <cell line: 6>()
4 print ('Matrix multiplication:\n') #
5 print ()
----> 6 A @ B

ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0,


with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 3 is different from 4)

In [39]: A = np.random.randint(2,size=(4, 4))


B = np.random.randint(2,size=(4, 4))
print ('Matrix A is:\n', A)
print ()
print ('Matrix B is:\n', B)
print ()
print ('Matrix multiplication:\n', A @ B) # Matrix multiplication
Matrix A is:
[[0 1 0 1]
[0 1 0 0]
[0 1 0 0]
[1 0 1 1]]

Matrix B is:
[[1 1 1 1]
[1 0 0 0]
[1 1 1 1]
[0 1 1 1]]

Matrix multiplication:
[[1 1 1 1]
[1 0 0 0]
[1 0 0 0]
[2 3 3 3]]

In [40]: A = np.random.randint(2,size=(4, 4))


B = np.random.randint(2,size=(4, 1))
print ('Matrix A is:\n', A)
print ()
print ('Matrix B is:\n', B)
print ()
print ('Matrix multiplication:\n', A @ B) # Matrix multiplication

Matrix A is:
[[0 0 0 1]
[0 1 1 1]
[1 1 0 0]
[1 0 0 0]]

Matrix B is:
[[0]
[0]
[1]
[0]]

Matrix multiplication:
[[0]
[1]
[0]
[0]]

In [41]: A = np.random.randint(3,size=(4))
B = np.random.randint(3,size=(4))
print ('Matrix A is:\n', A)
print ()
print ('Matrix B is:\n', B)
print ()
print ('Inner product:\n', np.dot(A,B)) # Inner product of two 1D arrays.
# For 2D this gives matrix multipli

Matrix A is:
[0 0 2 0]

Matrix B is:
[2 0 1 2]

Inner product:
2

For more options see https://numpy.org/doc/stable/reference/generated/numpy.dot.html


4.SepTasks for
4, 2022)today (you need to submit these tasks by
Solve the following problems.
1. Consider the code for the Fibonacci series done in the last class using recursive
functions and the for...in loop. Check the time taken in both the methods by
importing the time module. See, how this changes if N in the series is increased
from 10 to 100, in steps of 10.
1. Create two random 2D arrays/matrices A and B of size , that take random real
3 × 6

numbers between [0,10). Reshape them to 1D vectors and add them. Now reshape
them back to 3 × 6 and compare the sum with the direct sum A + B.
1. Consider the set of equations:

x = 15x + 3y + 5z


y = −5x + 2z


z = 8x + 13y + 5z

Solve, the above for ′ ′


x ,y ,z , using 2D arrays.

1. Using NumPy, show that Pauli matrices are indeed unitary. Also, check if sum of two
Pauli matrices are indeed unitary.
1. Check the np.kron(A,B) function. This creates a tensor (Kronecker) product of two
matrices.
https://en.wikipedia.org/wiki/Kronecker_product
Use NumPy matrix calculations to verify the np.kron(A,B) command.

5. Difficult task of the day (you can add this to your


submission)
Write a function that can find all even, odd and prime numbers between 1 and 1000.
~
6. How fast can you run?
We begin today's class with a test of how fast can a block of code run in Python. This
allows you to check which of the several loops you can implement will give you a faster
output.
This allows you to optimize your code at a very elementary level.
timeit command
In [1]: %timeit [i for i in range(10000)] #-rx runs the timer x times, and -ny means
# If you do not specify, then r is

158 µs ± 2.85 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

Let us compare while with for...in loops


In [2]: %%timeit -r1 -n100 # -rx and -ny mean same as before. Double %% means we che

i = 0
while i < 10000:
i += 1

297 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 100 loops each)

In [3]: %%timeit -r1 -n100 # Again, -rx mean run x times, and -ny means y loops for
# If you do not specify, then r is 7 and y is 1000000 by
k = 0
for i in range(10000):
k += 1

636 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 100 loops each)

Let us compare a recursive function with a loop


In [4]: def factorial_rec(n): # A recursive funstion for factorial
if n == 1:
return 1
else:
return n*factorial_rec(n-1)

def factorial_loop(n): # A loop function for factorial


prod = 1
for i in range(n):
prod *= i+1
return prod

In [5]: %%timeit
factorial_loop(5)

253 ns ± 10.5 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [6]: %%timeit
factorial_rec(5)

288 ns ± 8.84 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Importing the time module


Simple loops are usually faster than recursive functions
In [7]: import time

start = time.time() # gives the wall-time (total time taken by I/O and proce
# start = time.process_time() # gives the process-time (total time taken by

# Run the main block inside start and end

factorial_loop(1500)

end = time.time()
# end = time.process_time()
print ("Time taken to run the block in seconds (default):",end-start)

start = time.time()
# start = time.process_time()
factorial_rec(1500)

end = time.time()
# end = time.process_time()

print ("Time taken to run the block in seconds (default):",end-start)

Time taken to run the block in seconds (default): 0.00033974647521972656


Time taken to run the block in seconds (default): 0.0006968975067138672
More of NumPy, simple plotting and then practice
We begin today's class with more examples of NumPy

1. Copying an array the notion of assignment, shallow copy and deep copy

We begin with lists


In [1]: import numpy as np

simple_list = list([1,2,10,34,'man','cat',[9,1,2],['women','baby','dog']])
print (simple_list)

[1, 2, 10, 34, 'man', 'cat', [9, 1, 2], ['women', 'baby', 'dog']]

In [2]: dup_list = simple_list # This is an assignment '='


dup_list[4] = 'human'

print (dup_list)
print (simple_list,'\n')

print (id(dup_list)) # id gives you an integer associated with an object -- typically a memory location
print (id(simple_list)) # both lists have the same id

[1, 2, 10, 34, 'human', 'cat', [9, 1, 2], ['women', 'baby', 'dog']]
[1, 2, 10, 34, 'human', 'cat', [9, 1, 2], ['women', 'baby', 'dog']]

4381379264
4381379264

In [3]: slice_list = dup_list[2:] # Creating a slice of the original list


print (slice_list,'\n')
slice_list[2] = 'man'

print (slice_list)
print (simple_list,'\n')

print (id(slice_list),'\n') # So the slice now is a different list in some sense


slice_list[5][1] = 'teen'

print (slice_list)
print (dup_list)
print (simple_list,'\n') # So changes in first index is independent -- but second index remains same
# This is called a shallow copy

[10, 34, 'human', 'cat', [9, 1, 2], ['women', 'baby', 'dog']]

[10, 34, 'man', 'cat', [9, 1, 2], ['women', 'baby', 'dog']]


[1, 2, 10, 34, 'human', 'cat', [9, 1, 2], ['women', 'baby', 'dog']]

4381774016

[10, 34, 'man', 'cat', [9, 1, 2], ['women', 'teen', 'dog']]


[1, 2, 10, 34, 'human', 'cat', [9, 1, 2], ['women', 'teen', 'dog']]
[1, 2, 10, 34, 'human', 'cat', [9, 1, 2], ['women', 'teen', 'dog']]

In [4]: import copy as cp

simple_list = list([1,2,10,34,'man','cat',[9,1,2],['women','baby','dog']]) # Let us repeat

slice_copy = cp.copy(simple_list[2:]) # This slice will again create a shallow copy


slice_deepcopy = cp.deepcopy(simple_list[2:]) # This slice will again create a deep copy

slice_copy[2] = 102
slice_copy[5][1] = 'teen'
slice_deepcopy[5][1] = 'adult'

print (simple_list)
print (slice_copy)
print (slice_deepcopy) # So a deep copy creates an independent copy at all levels of a list or an array

[1, 2, 10, 34, 'man', 'cat', [9, 1, 2], ['women', 'teen', 'dog']]
[10, 34, 102, 'cat', [9, 1, 2], ['women', 'teen', 'dog']]
[10, 34, 'man', 'cat', [9, 1, 2], ['women', 'adult', 'dog']]

The same thing holds for NumPy arrays, albeit with some subtle differences
One can use the np.copy function
In [5]: simple_array = np.array([1,2,10,34,'man','cat',[9,1,2],['women','baby','dog']],dtype=object)
# create an array from the list; we use dtype = object
# this ensures different objects are listed in an array including a nested list

print (simple_array)

slice_array = simple_array[3:]
print (slice_array,'\n')

slice_array[1] = 'human'
print (slice_array)
print (simple_array,'\n') # Slice here is an assignment and not a shallow copy, in contrast to lists

slice_array_copy = cp.copy(simple_array[3:]) # You can also use np.copy (in NumPy) for shallow copy
slice_array_copy[1] = 'monkey'
print (slice_array_copy)
print (simple_array,'\n') # Slice here is an assignment and not a shallow copy

slice_array_copy[4][0] = 'queen'
print (slice_array_copy)
print (simple_array,'\n') # This is indeed a shallow copy

slice_array_deepcopy = cp.deepcopy(simple_array[3:]) # Now we have a deep copy


slice_array_deepcopy[4][1] = 'rascal'
print (slice_array_deepcopy)
print (simple_array,'\n')

slice_array_deepcopy = np.copy(simple_array[3:]) # np.copy is a shallow copy, but works for simple arrays
slice_array_deepcopy[4][1] = 'naughty'
print (slice_array_deepcopy)
print (simple_array,'\n')
[1 2 10 34 'man' 'cat' list([9, 1, 2]) list(['women', 'baby', 'dog'])]
[34 'man' 'cat' list([9, 1, 2]) list(['women', 'baby', 'dog'])]

[34 'human' 'cat' list([9, 1, 2]) list(['women', 'baby', 'dog'])]


[1 2 10 34 'human' 'cat' list([9, 1, 2]) list(['women', 'baby', 'dog'])]

[34 'monkey' 'cat' list([9, 1, 2]) list(['women', 'baby', 'dog'])]


[1 2 10 34 'human' 'cat' list([9, 1, 2]) list(['women', 'baby', 'dog'])]

[34 'monkey' 'cat' list([9, 1, 2]) list(['queen', 'baby', 'dog'])]


[1 2 10 34 'human' 'cat' list([9, 1, 2]) list(['queen', 'baby', 'dog'])]

[34 'human' 'cat' list([9, 1, 2]) list(['queen', 'rascal', 'dog'])]


[1 2 10 34 'human' 'cat' list([9, 1, 2]) list(['queen', 'baby', 'dog'])]

[34 'human' 'cat' list([9, 1, 2]) list(['queen', 'naughty', 'dog'])]


[1 2 10 34 'human' 'cat' list([9, 1, 2]) list(['queen', 'naughty', 'dog'])]

2. Accessing, slicing and creating a NumPy array


Let us now consider how we can access subarrays from a NumPy array
In [6]: num_list = np.arange(1,37) # This ensures that numbers start at 1 and end at 144 (rather than 0 and 143)
print (num_list,'\n')

num_array = num_list.reshape(4,3,3) # This is just an assignment


# print (num_array,'\n') # so we now have 4, 3 x 3 matrices in the tensor or a 3D array

num_array[0,2,1] = num_array[0,2,1]*100 # This will also change the original 1D array


print (num_array,'\n')
print (num_list)
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
25 26 27 28 29 30 31 32 33 34 35 36]

[[[ 1 2 3]
[ 4 5 6]
[ 7 800 9]]

[[ 10 11 12]
[ 13 14 15]
[ 16 17 18]]

[[ 19 20 21]
[ 22 23 24]
[ 25 26 27]]

[[ 28 29 30]
[ 31 32 33]
[ 34 35 36]]]

[ 1 2 3 4 5 6 7 800 9 10 11 12 13 14 15 16 17 18
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36]

In [7]: num_list = np.arange(1,37)


num_array = num_list.reshape(4,3,3)

copy_array = cp.copy(num_array) # for a N-dim array, a shallow copy is same as deepcopy


copy_array[0,2,1] = copy_array[0,2,1]*100 # This will now not change the original 1D array

print (copy_array,'\n')
print (num_array,'\n')
[[[ 1 2 3]
[ 4 5 6]
[ 7 800 9]]

[[ 10 11 12]
[ 13 14 15]
[ 16 17 18]]

[[ 19 20 21]
[ 22 23 24]
[ 25 26 27]]

[[ 28 29 30]
[ 31 32 33]
[ 34 35 36]]]

[[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]]

[[10 11 12]
[13 14 15]
[16 17 18]]

[[19 20 21]
[22 23 24]
[25 26 27]]

[[28 29 30]
[31 32 33]
[34 35 36]]]

In [8]: num_list = np.arange(1,37)


copy_array = cp.copy(num_list.reshape(4,3,3)) # so we now have 4, 3 x 3 matrices in the tensor or a 3D array
copy_array
array([[[ 1, 2, 3],
Out[8]:
[ 4, 5, 6],
[ 7, 8, 9]],

[[10, 11, 12],


[13, 14, 15],
[16, 17, 18]],

[[19, 20, 21],


[22, 23, 24],
[25, 26, 27]],

[[28, 29, 30],


[31, 32, 33],
[34, 35, 36]]])

In [9]: print (copy_array[1,:,:],'\n') # Gives you the second 3 x 3 array

print (copy_array[:,1,:],'\n') # Gives you the cross 4 x 3 matrix

print (copy_array[:,:,1],'\n') # Gives you the cross 4 x 3 matrix

print (copy_array[0:2,1,:],'\n') # Gives you the cross 2 x 3 matrix

print (copy_array[:,1,0:2],'\n') # Gives you the cross 4 x 2 matrix


[[10 11 12]
[13 14 15]
[16 17 18]]

[[ 4 5 6]
[13 14 15]
[22 23 24]
[31 32 33]]

[[ 2 5 8]
[11 14 17]
[20 23 26]
[29 32 35]]

[[ 4 5 6]
[13 14 15]]

[[ 4 5]
[13 14]
[22 23]
[31 32]]

In the last notes, we looked at how general NumPy arrays can be created. Here we discuss a few more options to create, split and
stack arrays.
For more read: https://numpy.org/doc/stable/user/basics.creation.html
In [10]: Mat = np.empty([3,3])
for i in range(3):
for j in range(3):
Mat[i,j] = i + j

print (Mat)

[[0. 1. 2.]
[1. 2. 3.]
[2. 3. 4.]]

In [11]: Mat = np.empty([3,3])


i,j = 0,0

while i < 3:
while j < 3:
Mat[i,j] = i + j
j += 1
i += 1

print (Mat)

[[0. 1. 2.]
[1. 2. 3.]
[2. 3. 4.]]

In [12]: Mat = np.array([[i+j for i in range(3)] for j in range (3)],dtype=float) # np.empty by default is float
print (Mat)

[[0. 1. 2.]
[1. 2. 3.]
[2. 3. 4.]]

Combining arrays and matrices


In [13]: Mat = np.arange(20)
print (Mat,'\n')

Mat_split = np.split(Mat,5) # list of split array


print (Mat_split[0],'\n') # 1D array from the split
print (Mat_split[-1],'\n')

print (np.concatenate((Mat_split[0],Mat_split[-1])),'\n')

print (np.stack((Mat_split[0],Mat_split[-1]),axis=0),'\n')
print (np.row_stack((Mat_split[0],Mat_split[-1])),'\n')
print (np.vstack((Mat_split[0],Mat_split[-1])),'\n')

print (np.stack((Mat_split[0],Mat_split[-1]),axis=1),'\n')
print (np.column_stack((Mat_split[0],Mat_split[-1])),'\n')

print (np.hstack((Mat_split[0],Mat_split[-1])),'\n') # Simply concatenates in 1D, will work the same in 2D


[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19]

[0 1 2 3]

[16 17 18 19]

[ 0 1 2 3 16 17 18 19]

[[ 0 1 2 3]
[16 17 18 19]]

[[ 0 1 2 3]
[16 17 18 19]]

[[ 0 1 2 3]
[16 17 18 19]]

[[ 0 16]
[ 1 17]
[ 2 18]
[ 3 19]]

[[ 0 16]
[ 1 17]
[ 2 18]
[ 3 19]]

[ 0 1 2 3 16 17 18 19]

3. Important NumPy functions


∙ np.linspace(start,end,num_elements)
https://numpy.org/doc/stable/reference/generated/numpy.linspace.html
Also see np.logspace( )
In [14]: np.linspace(0,10,11) # By default end point is included unlike np.arange();
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])
Out[14]:

In [15]: x_space = np.linspace(0,10,101) # can be used to create fractional steps; such as an x-axis in a plot
x_space

array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ,
Out[15]:
1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. , 2.1,
2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3. , 3.1, 3.2,
3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4. , 4.1, 4.2, 4.3,
4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5. , 5.1, 5.2, 5.3, 5.4,
5.5, 5.6, 5.7, 5.8, 5.9, 6. , 6.1, 6.2, 6.3, 6.4, 6.5,
6.6, 6.7, 6.8, 6.9, 7. , 7.1, 7.2, 7.3, 7.4, 7.5, 7.6,
7.7, 7.8, 7.9, 8. , 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7,
8.8, 8.9, 9. , 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8,
9.9, 10. ])

∙ Trigonometric and exponential functions


In [16]: np.sin(x_space[:15]) # sin(x) of the first 15 elements from the above linspace

array([0. , 0.09983342, 0.19866933, 0.29552021, 0.38941834,


Out[16]:
0.47942554, 0.56464247, 0.64421769, 0.71735609, 0.78332691,
0.84147098, 0.89120736, 0.93203909, 0.96355819, 0.98544973])

In [17]: print(np.cos(x_space[:15]),'\n')

print(np.exp(x_space[:15]),'\n')

print(np.log(x_space[1:15]),'\n') # Avoiding log_e(0)

print(np.log2(x_space[1:15]),'\n') # log_2(x) (base 2)

print(np.pi) # Defining pi
[1. 0.99500417 0.98006658 0.95533649 0.92106099 0.87758256
0.82533561 0.76484219 0.69670671 0.62160997 0.54030231 0.45359612
0.36235775 0.26749883 0.16996714]

[1. 1.10517092 1.22140276 1.34985881 1.4918247 1.64872127


1.8221188 2.01375271 2.22554093 2.45960311 2.71828183 3.00416602
3.32011692 3.66929667 4.05519997]

[-2.30258509 -1.60943791 -1.2039728 -0.91629073 -0.69314718 -0.51082562


-0.35667494 -0.22314355 -0.10536052 0. 0.09531018 0.18232156
0.26236426 0.33647224]

[-3.32192809 -2.32192809 -1.73696559 -1.32192809 -1. -0.73696559


-0.51457317 -0.32192809 -0.15200309 0. 0.13750352 0.26303441
0.37851162 0.48542683]

3.141592653589793

∙ Transpose of a matrix
In [18]: mat = copy_array[1,:,:2] # The slice gives you a 3 x 2 array
print (mat,'\n')
print (np.transpose(mat))

[[10 11]
[13 14]
[16 17]]

[[10 13 16]
[11 14 17]]

∙ Flatten, ravel
In [19]: print (mat) # Is a 3 x 2 complex matrix
print ()
print (mat.flatten()) # Flattens it to a 1D array
print ()
print (np.ravel(mat)) # Unravels it to a 1D array; same as flatten
[[10 11]
[13 14]
[16 17]]

[10 11 13 14 16 17]

[10 11 13 14 16 17]

∙ Trace of a matrix
In [20]: mat = copy_array[1,:,:] # The slice gives you a 3 x 2 array
print (mat)
print ()
np.trace(mat)

[[10 11 12]
[13 14 15]
[16 17 18]]

42
Out[20]:

∙ Diagonal of a matrix
In [21]: np.diagonal(mat) # Returns a 1D array of the diagonal elements

array([10, 14, 18])


Out[21]:

∙ Conjugate of a matrix
In [22]: comp_mat = np.random.rand(3,3) + 1j*np.random.rand(3,3) # A random complex matrix
print (comp_mat,'\n')
print (np.conjugate(comp_mat)) # Conjugate of a matrix

[[0.08292567+0.46838763j 0.92156076+0.90785993j 0.13198283+0.17438904j]


[0.35044334+0.12414423j 0.66647702+0.08269438j 0.25849498+0.4500927j ]
[0.637308 +0.36137959j 0.5591691 +0.74298545j 0.59263628+0.03882147j]]

[[0.08292567-0.46838763j 0.92156076-0.90785993j 0.13198283-0.17438904j]


[0.35044334-0.12414423j 0.66647702-0.08269438j 0.25849498-0.4500927j ]
[0.637308 -0.36137959j 0.5591691 -0.74298545j 0.59263628-0.03882147j]]

∙ np.floor
In [23]: comp_mat = np.random.randint(5,size=[3,3]) + np.random.rand(3,3)
print (comp_mat,'\n')

print (np.floor(comp_mat)) # The floor of the scalar x is the largest integer i, such that i <= x

[[1.3596184 2.71665045 2.13708888]


[0.8208742 3.69273165 2.54455328]
[1.0869674 0.14600886 4.95004334]]

[[1. 2. 2.]
[0. 3. 2.]
[1. 0. 4.]]

4. Linear algebra package using NumPy


We use the numpy.linalg functions for simple linear algebra algorithms. In later classes, we will use the scipy.linalg library to perform
similar functions.
See this page for more functions: https://numpy.org/doc/stable/reference/routines.linalg.html?highlight=np%20linalg
Determinant of a matrix
In [24]: from numpy import linalg as la

A = np.random.randint(5,size=(3, 3)) # Random array with shape (3,3) filled with integer elements from 0 to 5
print ('Matrix A is:\n', A,'\n')

la.det(A) # Compute the determinant of a square matrix

Matrix A is:
[[0 0 2]
[1 3 0]
[4 3 0]]

-17.999999999999996
Out[24]:

Eigenvalue and eigenvector of a matrix


In [25]: B = np.random.randint(5,size=(3, 3)) # Random array with shape (3,3) filled with integer elements from 0 to 5
print ('Matrix A is:\n', B,'\n')

e_val, e_vec = la.eig(B) # Compute the eigenvalues and right eigenvectors of a square array

print (e_val,'\n')
print (e_vec)

Matrix A is:
[[3 1 4]
[3 1 1]
[1 2 3]]

[6.42966491+0.j 0.28516754+1.50053856j 0.28516754-1.50053856j]

[[ 0.7190541 +0.j -0.06826679+0.4837944j -0.06826679-0.4837944j ]


[ 0.48835468+0.j 0.73134993+0.j 0.73134993-0.j ]
[ 0.49443999+0.j -0.31799229-0.35396442j -0.31799229+0.35396442j]]

Multiplicative inverse of a matrix


In [26]: A = np.random.randint(5,size=(3, 3)) # Random array with shape (3,3) filled with integer elements from 0 to 5
print ('Matrix A is:\n', A,'\n')

A_inv = la.inv(A) # Compute the eigenvalues and right eigenvectors of a square array
print (A_inv) # Compute the eigenvalues and right eigenvectors of a square array

A @ A_inv # Gives a numerically accurate identity matrix

Matrix A is:
[[0 1 3]
[3 4 3]
[1 0 1]]

[[-0.33333333 0.08333333 0.75 ]


[-0. 0.25 -0.75 ]
[ 0.33333333 -0.08333333 0.25 ]]
array([[ 1.00000000e+00, 1.38777878e-17, 0.00000000e+00],
Out[26]:
[-5.55111512e-17, 1.00000000e+00, 0.00000000e+00],
[ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])

Matrix exponentiation − A
n
In [27]: A = np.random.randint(5,size=(3, 3)) # Random array with shape (3,3) filled with integer elements from 0 to 5
print ('Matrix A is:\n', A,'\n')

A_pow3 = la.matrix_power(A,3) # Matrix A raised to the power 3

A_pow3 - A @ A @ A

Matrix A is:
[[3 0 2]
[1 4 1]
[2 2 4]]

array([[0, 0, 0],
Out[27]:
[0, 0, 0],
[0, 0, 0]])

5. Elementary plotting with Python


We use the library matplotlib to produce data plots. The library allows for simple 2D lineplots to complex 3D colormaps. Here, we take a
look at how one can plot simple XY plots using this library. We will come back to more sophisticated plots at a later part on data
visualization using Python.
Learn more at https://matplotlib.org/
Using matplotlib.pyplot for simple XY plots
In [28]: import matplotlib.pyplot as plt

x = np.arange(100)

plt.plot(x,x**2,'r--') # Plotting the quadratic x^2 with x; r-- is for red with dashed lines
plt.plot(x,100*x,'k-') # Plotting the linear 100x with x; k- is for black with straight lines
plt.show()
In [29]: plt.semilogy(x,x**2,'r--') # Plotting the quadratic x^2 with x plotted in the logscale

[<matplotlib.lines.Line2D at 0x10a23d2d0>]
Out[29]:
In [30]: x = np.linspace(0.0,100,1000)

def func(x):
return 5*np.sin(x) + x

plt.plot(x,func(x),'g:')

[<matplotlib.lines.Line2D at 0x10a0198d0>]
Out[30]:
In [31]: A = np.random.randint(5,size=(2, 20)) # Random array filled with integer elements between [0,5)
plt.scatter(A[0,:],A[1,:])

<matplotlib.collections.PathCollection at 0x10a36eb10>
Out[31]:
In [32]: A = np.random.rand(2,100) # Random array with uniform random numbers between [0,1)

plt.plot(A[0,:],A[1,:],'ro',linestyle='None') # the last comment removes the lines

[<matplotlib.lines.Line2D at 0x10a441650>]
Out[32]:
In [33]: A = np.random.rand(50000,1) # Random 1D array with uniform random numbers between [0,1)

n, bins, patches = plt.hist(A, bins = 1000)


In [34]: A = np.random.randn(50000,1) # Random 1D array from a Gaussian distribution mean = 0 and variance = 1

n, bins, patches = plt.hist(A, bins = 1000)


6. Tasks for today
Solve the following problems.
Write a code to create an array for a tensor , with elements
M , where
mijk = i + j + k , using a for...in or while loop.
i, j, k ∈ [0, 1, 2]

What is the dimension and shape of ? M

Find the matrix which corresponds to a) , b) , and c)


i = 1 j = 1 .
k = 1

</br>
Create the quantum operator: H = σy ⊗ I + I ⊗ σy , where is the 2D identity matrix. Find the eigenvalues and eigenvectors of .
I H
Show that D = P
−1
, where is the diagonal matrix containing the eigenvalues of and is an invertible matrix formed by the
HP D H P

eigenvectors. </br>
Newton-Raphson method to find the root of a function
The root of a function is defined as
f (x) , such that
x = α . The Newton-Raphson method helps find the root of a continuous
f (α) = 0

function in an iterative manner. Starting with a guess for the root, it predicts a better guess , where
xn xn+1

f (xn )
xn+1 = xn − .

f (xn )

Here, ′
f (x) is the numerical derivate of the function f (x) , given by
df (x) f (x + h) − f (x)

f (x) ≡ = lim .
dx h→0 h

Find a function, that can be iteratively find the root of the function, , using the Newton-Raphson method
f (x) = sin(x) + 3 cos(x)

upto an accuracy of , starting from an initial random guess


10
−6
. The function takes , and , as its arguments. The
x0 ∈ [0, π] f (x) x0 h

output of the function should be a tuple containing the approximate root of and the number of iterations needed to find it.
f (x) n

Hint: If the function is unable to find the root after a fixed number of iterations than you should restart it with a different initial
guess.
</br>
Using matplotlib, plot the function f (x) = sin(x) + 3 cos(x), in the range . Use atleast 1000 plot points. Compare this
x ∈ [0, 5π]

with the plot f (x) = cos(x).


In [ ]:
First, let us introduce SciPy
SciPy is a Python library that provides algorithms for a vast array of scientific computation. This primarily includes tools for integration,
optimization, and solvers for eigenvalue problems, differential equations and several other problems in numerical computing.
See the tutorial below to learn more about SciPy:
https://docs.scipy.org/doc/scipy/tutorial/index.html
What is the difference with NumPy?
NumPy: Performs basic operations such as sorting, indexing, etc. Contains a variety of functions but these are not defined in depth. Arrays
are multi-dimensional arrays of objects which are of the same type. Written in C. Fast but not flexible.
SciPy: Used for complex operations such as algebraic functions, various numerical algorithms, etc. Contains detailed versions of the
functions like linear algebra. Does not have any concept of array or data structures. Written in Python. Slow and versatile.

1. The linear algebra package in SciPy


Eigenvalues and eigenvectors
In [2]: import numpy as np
from scipy import linalg

A = np.random.randint(5,size=(3, 3)) # Random array with shape (3,3) filled with integer elements from 0 to 5
print ('Matrix A is:\n', A,'\n')

e_val, e_vec = linalg.eig(A) # Compute the eigenvalues and right eigenvectors of a square array
print (e_val)
print ()
print (e_vec)
Matrix A is:
[[4 0 3]
[1 0 0]
[3 4 1]]

[ 6.13404654+0.j -0.56702327+1.27858471j -0.56702327-1.27858471j]

[[ 0.80776757+0.j 0.48086617+0.13462338j 0.48086617-0.13462338j]


[ 0.13168592+0.j -0.05139048-0.35330202j -0.05139048+0.35330202j]
[ 0.57460453+0.j -0.78941812+0.j -0.78941812-0.j ]]

Matrix exponentiation
e , where is a square matrix
A
A

In [3]: exp_A = linalg.expm(A)


print (exp_A,'\n')

B = np.zeros((2, 2))
print (linalg.expm(B))

[[312.22235627 118.16870491 182.48754016]


[ 50.98178798 19.66867568 29.54217623]
[221.87710846 85.75844699 129.73481612]]

[[1. 0.]
[0. 1.]]

Several other intersting operations in the linear algebra package


Please go through each of them and practice:
https://docs.scipy.org/doc/scipy/reference/linalg.html
Note that you are expected to study these for future tests.

2. Numerical integration
Using the integrate package in SciPy
x=1
2
I = ∫ ax + bx dx
x=0

scipy.integrate.quad(func, a, b)
In [4]: from scipy import integrate

def I(x, a, b):


return a*x**2 + b

a,b = 1,2

integrate.quad(I,0,1,args=(a,b)) # Returns the integration with possible upper bound on the error

(2.333333333333333, 2.5905203907920317e-14)
Out[4]:

One can also double integrate


x=2 y=1
2
I = ∫ ∫ xy dy dx
x=0 y=0

scipy.integrate.dblquad(func, a0, b0, a1, b1)


In [5]: from scipy import integrate

def f(y,x): # Note the order of x and y, while defining the function
return x*y**2

integrate.dblquad(f, 0, 2, 0, 1) # The integration limits of x are given first, followed by limits of y

(0.6666666666666667, 7.401486830834377e-15)
Out[5]:

x=2 y=cos(x)
I = ∫ ∫ dy dx
x=0 y=sin(x)

In [6]: def f(y, x):


return 1

integrate.dblquad(f, 0, np.pi/4, np.sin, np.cos) # Note that limits of y can be two functions of x

(0.41421356237309503, 1.1083280054755938e-14)
Out[6]:
Trapezium or trapezoidal rule
https://en.wikipedia.org/wiki/Trapezoidal_rule
To integrate I = ∫
x1

x0
, the idea is to slice the function into several trapeziums and calculate the area.
f (x) dx

For example, using a single trapezium:


x1 1
∫ f (x) ≈ (x1 − x0 ) × (f (x1 ) + f (x0 ))
x0 2

Using several trapeziums


xN N
f (xi−1 ) + f (xi )
∫ f (x) ≈ ∑ Δxi
x0 i=1
2

In [25]: def f(x):


return x**2
N = 1000
x_val = np.linspace(0,10,N)
dx = x_val[1]-x_val[0]

int_sum = sum([dx/2*(f(x_val[i])+f(x_val[i+1])) for i in range(len(x_val)-1)])

print (int_sum)

333.33350033383414

Monte Carlo intergration


https://en.wikipedia.org/wiki/Monte_Carlo_integration
Let us consider the integration:
I = ∫
Ω
, where the volume,
f (x)dx . V = ∫
Ω
dx

In MC integration, we sample uniformly random points


N . In the limit of large , we get
{xi } ∈ Ω N

V N
I ≈ ∑ f (xi ) ≡ V × ⟨f (x)⟩.
i=1
N
Let us consider, f (x) = x
2
between x = 0 and . Here,
x = 10 .
v = 10 − 0 = 10

In [27]: V = 10 - 0
x = np.random.uniform(0,10,N) # we choose N points between 0 and 10

I = V/N*sum(f(x))

print (I) # Check if this is correct

print (integrate.quad(f,0,10))

335.5959563911658
(333.33333333333337, 3.700743415417189e-12)

3. Tasks for today


Solve the following problems.
Integrate the function , using the trapezium and Monte-Carlo rule, between
f (x) = x
2
x = 2 and . Also, compare this with the
x = 100

integrate.quad function from SciPy. </br>


Using both trapezium and Monte-Carlo methods, integrate , between
x log x and x = 0 x = 1 . Compare with exact and integrate.quad.
Solving ordinary differential equations
Using the solve_ivp function under scipy.integrate in the SciPy library.
https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html
An ordinary differential equation of order is: n

0 1 n
F (x, y, y , y , … , y ) = 0,

where . Alternatively, it can be written as: .


k
d y
k n 0 1 n−1
y = k
y = g(x, y, y , y , … , y )
dx

Now a system of differential equations of order can be written as:


m n

n 0 1 n−1
y = g(x, y, y , y , … , y ),

where y = [y0 , y1 , y2 , … , ym ] .
Consider the following:
dy

dt
= f (t, y) , given y(t0 ) = y0 , where y is a function, either a scalar or a vector.
1) dy

dt
= y

In [1]: import numpy as np


from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

def func(t,y):
dydt = y
return dydt

t = np.linspace(0,5,10)
y_out = solve_ivp(func, t_span = [0,5], y0 = np.array([1.0]), method='RK45', t_eval=t)
print (y_out.y[0]) # So y_out.t contains t, and y_out.y contains y(t). Careful of the containers.

[ 1. 1.74287174 3.03783363 5.29347269 9.22755581


16.08249871 28.02011347 48.86252374 85.10100159 148.39440874]

In [2]: y_out.y

array([[ 1. , 1.74287174, 3.03783363, 5.29347269,


Out[2]:
9.22755581, 16.08249871, 28.02011347, 48.86252374,
85.10100159, 148.39440874]])

In [4]: plt.plot(y_out.t,y_out.y[0])

[<matplotlib.lines.Line2D at 0x298d7d950>]
Out[4]:
2) dy

dt
= sin(5t) + 0.1t

In [6]: def func(t,y):


dydt = np.sin(5*t)+0.1*y
return dydt

t = np.linspace(0,5,100)
y_out = solve_ivp(func, t_span = [0,5], y0 = np.array([1.0]), method='LSODA', t_eval=t)
y_out

message: The solver successfully reached the end of the integration interval.
Out[6]:
success: True
status: 0
t: [ 0.000e+00 5.051e-02 ... 4.949e+00 5.000e+00]
y: [[ 1.000e+00 1.013e+00 ... 1.798e+00 1.794e+00]]
sol: None
t_events: None
y_events: None
nfev: 115
njev: 0
nlu: 0

In [2]: plt.plot(y_out.t,y_out.y[0])

[<matplotlib.lines.Line2D at 0x7f1b019773d0>]
Out[2]:
3) Solve the following ODE:
dx
2 = −x(t) + u(t)
dt

dy
5 = −y(t) + x(t)
dt

where, u(t) = 2S(t − 5) , and x(0) = y(0) = 0 .


S(t − 5) is a step function,S = 0 for , and
t < 5 S = 1 for .
t ≥ 5

In [2]: def func(t,x):


u = 1 if t >= 5 else 0
y = np.zeros(len(x))
y[0] = (1/2.)*(-x[0] + u)
y[1] = (1/5.)*(-x[1] + x[0])
return y

In [3]: t = np.linspace(0,40,400)
x0 = np.array([0.0,0.0])
y_out = solve_ivp(func, t_span = [0,40], y0 = x0, method='BDF', t_eval=t)

def u(t): # This bit is needed just for the plot


if t < 5:
return 0
else:
return 1

In [9]: x=y_out.y[0]
y=y_out.y[1]

In [3]: plt.plot(y_out.t,y_out.y[0],'r')
plt.plot(y_out.t,y_out.y[1],'b')
plt.plot(t,[u(i) for i in t],'k--')

[<matplotlib.lines.Line2D at 0x7f1b018f3910>]
Out[3]:
4) Solve the following ODE:
dx1
= x2 (t)
dt

dx2
= 2x1 (t) − x2 (t)
dt

x1 (0) = 1; x2 (0) = 2.

which can be written as matrix multiplication:


dx

dt
= A × x, where A = (
0

2
1

−1
) , and x = (
x1

x2
). The initial condition is x(0) = (
1

2
).

In [ ]: A = np.array([[0,1],[2,-1]])

def func(t,x):
x1 = A@x
return x1

t = np.linspace(0,5,100)
x0 = np.array([1.0,2.0])
y_out = solve_ivp(func, t_span = [0,5], y0 = x0, method='LSODA', t_eval=t)
In [ ]: plt.plot(y_out.t,y_out.y[0])

Tasks for today


Solve the following problems.
Solve the differential equation:
dx
= σ(y − x)
dt

dy
= x(ρ − z) − y
dt

dz
= xy − βz
dt

where , respectively. You can take the time,


σ, β, ρ = 10, 4, 25 . Plot vs , vs , and vs .
t ∈ [0, 40] x y x y z y

Convert the second order differential equation to a set of ODEs and solve: ,
2
d y dy
+ 3 + ty = cos(t)
2
dt dt

where dy

dt

= y (0) = −2 and . Plot the solutions of the ODEs for some time range.
y(0) = 2

Consider the matrix equation:


0 1 −2
⎛ ⎞
dx
= Ax, where A = ⎜ 1 3 −1 ⎟
dt
⎝ ⎠
−2 −1 −2

Solve the above equation using solve_ivp, and compare the solutions with , where
⎛ ⎞
At
x(t) = e x(0) x(0) = ⎜ −1 ⎟ .
⎝ ⎠
0
Solving ordinary differential equations
In this class we look at the Runge-Kutta method and some problems involving ODEs.
Runge-Kutta (RK) methods
RK methods are a family of iterative method for solving ordinary differential equations. It's simplest form, the first order RK, is nothing but
the Euler method, which we have encountered earlier. In general, it is the fourth-order Runge Kutta method (aka RK4 ), which is widely
used and is often eponymous while discussing these methods.
The RK4 can be described as: dy
, where is some unknown function (scalar or vector) that we want to find, with some initial
= f (x, y) y

values, and .
dx

x = x0 y(x0 ) = y0

Now, for some , the function at different values of can be iteratively estimated using,
h > 0 y x

1
yn+1 = yn + (k1 + 2k2 + 2k3 + k4 ) h,
6

xn+1 = xn + h,

where n = 0, 1, 2, 3, … , and
k1 = f (xn , yn ),

k1 h
k2 = f (xn + h , yn + ),
2 2

k2 h
k3 = f (xn + h , yn + ), and
2 2

k4 = f (xn + hk3 , yn + h)

Note: If ki = k1 = f (xn , yn ), ∀i, we get back the first-order RK or the Euler method. Use the RK4 method above to solve the following
problem:
dy
= y sin(t), where 2
. Plot the function for different values of and also the exact solution for
y(0) = 1 y h . Compare with
t ∈ [0, 5]

"solve_ivp" using the RK45 solver. </br>


dt

dx

dy
= x
2
+ y
2
, where x(1) = 1.2 . Find, for x y ∈ [1.0, 10.0] with h = 0.01 . Compare with "solve_ivp" using the LSODA solver.

Quantum dynamics
A two-level state (say a spin) in quantum mechanics can be written as:
α
|ψ⟩ = α|1⟩ + β|0⟩ = [ ] , where α, β ∈ C.
β

Here, and , are the basis states and can refer to the energy level or eigenstates of any observable in the two-dimensional Hilbert
|0⟩ |1⟩

space.
Moreover, operators acting on the systems are given by matrices such as the Pauli matrices,
0 1 0 −i 1 0
σx = ( ) , σy = ( ) , and σz = ( ).
1 0 i 0 0 −1

Consider the Hamiltonian for a system of a quantum spin and a photon, given as:
ωs 0 0 σx ± iσy
† + † − ±
^ a +
H = ωc a ^σ
σz + g(a ^ σ
+ a ^ = (
), where a ) , and σ = .
2 1 0 2

Here, ^ (a
a ^ )

is the photon annihilation (creation) operator, whereas act on the spin. σ
±

Now, for any Hamiltonian, , the time evolution of a quantum state is given by the equation:
H
d|ψ⟩
= H|ψ⟩ . If the spin-photon system is
initially in the state
dt

1 0
|ψ(0)⟩ = [ ] ⊗ [ ] , (1)
0 1
spin photon
where spin state is in the up/excited and photon state is in vacuum.
The Hamiltonian parameters are given by , and
ωc = ωs = 1 g/ωc = 10, solve the dynamics of the system and plot the expectation value
−3

of , which gives us the average number of photons in the state, for increasing time .

^ a⟩
⟨a t

Hint: If operator only acts on the spin, then the operator


A acts on the joint spin-photon system. Similarly, if acts only on the
(A ⊗ I) B

photon, then (I ⊗ B) acts on the joint system. Expectation value of is given by


B and is the dot-product between the
⟨ψ(t)|I ⊗ B|ψ(t)⟩

vectors ∗
|ψ (t)⟩ and .
(I ⊗ B)|ψ(t)⟩

Basic epidemiology
Let us consider a virus infection that spreads and can develop into a deadly disease X. Without treatment with antiviral drugs, survival time
after infection and onset of disease X is about 9 to 11 month, depending on a number of factors.
The spread of the viral infection in a body with an initial infection is approximated with balance equations on the number of healthy cells ( H

), infected cells ( ), and virus count ( ), which are governed by:


I V

dH
= r1 − r2 H − r3 H V
dt

dI
= r3 H V − r4 I
dt

dV
= −r3 H V − r5 V + r6 I ,
dt

where, r1 = 10 is the growth rate of healthy cells,


5
r2 = 0.1 is the death rate, r3 = 2 ∗ 10 is the rate of conversion of healthy cells into
−7

infected cells, r4 = 0.5 is the death rate of infected cells, is the death rate of virus, and
r5 = 5 r6 = 100, is the production of virus by
infected cells. All rates are per month.
Plot the healthy cell, infected cell, and the virus count over the course of 15 months, if the initial counts are: H (0) = 10
6
, , and
I (0) = 0

V (0) = 100 . Please use the RK4 method derived in the first section.
In [ ]:
Python Dictionaries
So, far we have seen three in-built data-types in Python that can be used to store data, viz lists, tuples and sets. Each have their own
characteristics.
Today, we look at the fourth type. A Python dictionary. A dictionary stores data values in key-data pairs. In latest Python version,
dictionaries are ordered, changeable and cannot have duplicate keys.
In [1]: import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

In [2]: new_dict = {
"Name": ['Rahul','Sourav','Sachin'], # You can have any data type as values, i.e., np.array, string
"Debut": (1996,1992,1989),
"Century": np.array([48,38,100])
}

print(new_dict)

{'Name': ['Rahul', 'Sourav', 'Sachin'], 'Debut': (1996, 1992, 1989), 'Century': array([ 48, 38, 100])}

In [3]: print (len(new_dict)) # the length of the dictionary or number of keys


print (type(new_dict)) # the type of the variable -- is a dictionary
print (type(new_dict["Debut"])) # the type of the variable -- is a tuple
print (type(new_dict["Century"])) # the type of the variable -- is a numpy array

new_dict.keys() # gives the keys of the dictionary

3
<class 'dict'>
<class 'tuple'>
<class 'numpy.ndarray'>
dict_keys(['Name', 'Debut', 'Century'])
Out[3]:

Accessing and adding elements and keys in a dictionary


In [25]: print (new_dict.get("Name"))
print (new_dict.get("Name")[1])
print ()

new_dict["Name"][1] = 'Ganguly' # Edit a specific value in a specific key


print(new_dict["Name"])

['Rahul', 'Sourav', 'Sachin']


Sourav

['Rahul', 'Ganguly', 'Sachin']

In [26]: new_dict["Wicket"] = [5,132,200] # Add a new key


print(new_dict)

{'Name': ['Rahul', 'Ganguly', 'Sachin'], 'Debut': (1996, 1992, 1989), 'Century': array([ 48, 38, 100]), 'Wicket':
[5, 132, 200]}

In [4]: # Adding new elements to each list


new_dict["Debut"].append(1996)
# new_dict["Name"].append('Laxman')
# new_dict["Century"].append(23)
# new_dict["Wicket"].append(2)

---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[4], line 2
1 # Adding new elements to each list
----> 2 new_dict["Debut"].append(1996)

AttributeError: 'tuple' object has no attribute 'append'

In [28]: new_dict

{'Name': ['Rahul', 'Ganguly', 'Sachin'],


Out[28]:
'Debut': (1996, 1992, 1989),
'Century': array([ 48, 38, 100]),
'Wicket': [5, 132, 200]}

In [29]: new_dict.items() # Gives you the key and value pair as a tuple

dict_items([('Name', ['Rahul', 'Ganguly', 'Sachin']), ('Debut', (1996, 1992, 1989)), ('Century', array([ 48, 38, 10
Out[29]:
0])), ('Wicket', [5, 132, 200])])
Saving data in Python
We look at ways in which data can be saved in Python. There are 3 types we will discuss 1) simple .txt for simple text format, 2) csv file for
data that can be written as a table (think MS Excel), and 3) using pickle, NumPy and json for saving objects, dictionaries or more
structured data.
Using txt
In [10]: ## Saving text data

txtdata = ['A','B','C','D'] # Here is the text data


file = open("filename.txt", "w") # Creating a file -- "w" stands for write, "a" for append, "r" for read (default)

for i in txtdata:
file.writelines([i,'\n']) # Writing text -- use "write" if you just have a single data
file.close()

In [66]: file = open("filename.txt", "r")


print (file.read())
file.close()

ABCD

In [63]: file = open("filename.txt", "a")


file.write('E')
file.close()

Using CSV
CSV stands for comma separated values. Works well with pandas.
In [69]: import csv

heads = ['Name', 'Roll', 'Marks']

data = [ ['Arup', '090', '88'],


['Lata', '112', '95'],
['Varun', '202', '92'],
]
file = "midsems.csv"

# writing to csv file


with open(file, 'w') as csvfile:
csvwriter = csv.writer(csvfile)

csvwriter.writerow(heads)
csvwriter.writerows(data)

In [87]: # reading a csv file

with open("midsems.csv", mode='r')as file:


f = csv.reader(file)
for rows in f:
print (rows)

['Name', 'Roll', 'Marks']


['Arup', '090', '88']
['Lata', '112', '95']
['Varun', '202', '92']

Saving data using pickle


The pickle module implements binary protocols for serializing and deserializing a Python object structure. “Pickling” is the process whereby
a Python object hierarchy is converted into a byte stream, and “unpickling” is the inverse operation, whereby a byte stream (from a binary
file or bytes-like object) is converted back into an object hierarchy.
In [11]: import pickle
data_np = np.arange(100000)

In [12]: # Create the pickle file

with open("numpy_data.pickle", "wb") as f:


pickle.dump(data_np, f, protocol=pickle.HIGHEST_PROTOCOL)

In [13]: # Read from the pickle file

with open("numpy_data.pickle", "rb") as f:


out = pickle.load(f)
out

array([ 0, 1, 2, ..., 99997, 99998, 99999])


Out[13]:

In [104… with open("dict.pickle", "wb") as f1:


pickle.dump(new_dict, f1, protocol=pickle.HIGHEST_PROTOCOL)

In [110… file = open("dict.pickle", "rb")


pickle.load(file)
# print (pickle.load(file))
# file.close()

{'Name': ['Dravid', 'Ganguly', 'Tendulkar'],


Out[110]:
'Debut': (1996, 1992, 1989),
'Century': [48, 38, 100],
'Wicket': [5, 132, 200]}

Saving Python data and dictionaries using NumPy


using NumPy
In [14]: string = np.arange(10000)
np.savetxt('strings_numpy.txt', string)

In [138… string2 = np.loadtxt('strings_numpy.txt')


string2

array([0.000e+00, 1.000e+00, 2.000e+00, ..., 9.997e+03, 9.998e+03,


Out[138]:
9.999e+03])

In [113… np.save('numpy_data.npy',data_np,allow_pickle = True)

In [30]: np.save('cricket.npy',new_dict,allow_pickle = True) # The pickle module implements binary protocols for
# serializing and de-serializing a Python object structure

new_dict["Name"][0] = 'Dravid' # Edit a specific value in a specific key


new_dict["Name"][2] = 'Tendulkar' # Edit a specific value in a specific key

print(new_dict["Name"])
['Dravid', 'Ganguly', 'Tendulkar']

In [31]: old_dict = np.load('cricket.npy',allow_pickle = True).item(0) # set allow_pickle = True while loading

In [32]: print (old_dict)


print ()
print (new_dict)

{'Name': ['Rahul', 'Ganguly', 'Sachin'], 'Debut': (1996, 1992, 1989), 'Century': array([ 48, 38, 100]), 'Wicket':
[5, 132, 200]}

{'Name': ['Dravid', 'Ganguly', 'Tendulkar'], 'Debut': (1996, 1992, 1989), 'Century': array([ 48, 38, 100]), 'Wicke
t': [5, 132, 200]}

In [33]: np.save('cricket.npy',new_dict,allow_pickle = True) # will overwrite the old file

Tasks for today


Solve the following problems.
Solve the differential equation:
dx
= x(t) + 1
dt

dy 1
= − (y(t) − x(t)),
dt 5

where, x(0) = y(0) = 0 and t ∈ [0, 10] .


Create a dictionary: dict = {'time': ,'x_value': , 'y_value': }, and store the output of the differential equation in the dictionary. Save it as
[] [] []

both a pickle and numpy file.


Load the pickle file that you have saved and, using matplotlib, plot vs and vs , on the same plot.
x t y t

Create a dictionary containing the name of your five of your friends, their city of birth, hometown, and a fictional passport number.
Now write a function that can add a new friend's information. Add the Head as the new friend, {'HOD','Kolkata','Mumbai','XYZ789'}.
Saving Python dictionaries using json
What is json? JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript
object syntax but applicable to other platforms. It is commonly used for transmitting data in various applications (e.g., sending some data
from the server to the client or vice versa).
using json
In [ ]: import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

import pickle
import json

In [ ]: # Load this file from last class -- attached with code

new_dict = np.load('cricket.npy',allow_pickle = True).item(0)

new_dict

In [ ]: with open("dict.json", "w") as outfile: # Open a new json file, for writing "w"
json.dump(new_dict,outfile,indent=4) # Dump the new_dict dictionary to the open json file

In [ ]: new_dict

In [ ]: # nd.arrays are not "json" serializable

new_dict["Century"]= new_dict["Century"].tolist() # Convert it into a list


print (new_dict)

In [ ]: with open("dict.json", "w") as outfile: # Open a new json file, for writing "w"
json.dump(new_dict,outfile,indent=4) # Dump the new_dict dictionary to the open json file

In [ ]: loadfile = open("dict.json") # Open a saved json file


json_dict = json.load(loadfile) # Dump the opened a file to a variable ('dictionary')
json_dict
In [ ]: with open("dict.json") as loadfile: # Open a saved json file
json_dict = json.load(loadfile) # Dump the opened a file to a variable ('dictionary')
json_dict

In [ ]: json_dict["City"] = ['Bengaluru','Kolkata','Mumbai']

In [ ]: # json_dict
with open("dict.json","w") as loadfile:
json.dump(json_dict,loadfile,indent=4)

In [ ]: # Write a function that automatically adds data to your json file

# def add_jsonfile(add_key,add_value,filename='dict.json'):
# with open(filename, "r+") as outfile: # Open a json file, for reading and writing "r+"
# dict_ = json.load(outfile)
# dict_[add_key] = add_value
# with open(filename, "w") as outfile: # Open the json file, only for writing "w" to overwrite the file
# json.dump(dict_,outfile,indent=4)

# add_jsonfile('City',['Bengaluru','Kolkata','Mumbai','Hyderabad'],'dict.json')

Second json file


In [ ]: students = {
"2022": [{'Name':'Ajay',
'Age':18,
'Roll':'22ND075'},
{'Name':'Vijay',
'Age':19,
'Roll':'22ND867'},
{'Name':'Tanuj',
'Age':19,
'Roll':'22ND105'}
]
}

In [ ]: with open("student.json", "w") as outfile: # Create a new json file "w"


json.dump(students,outfile,indent=2)
In [ ]: students['2021']=[]
print (students)

with open("student.json", "w") as outfile: # If I want to overwrite then always use "w"
json.dump(students,outfile,indent=2)

In [ ]: def append_jsonfile(add_key,add_value,filename='stud.json'):
with open(filename, "r+") as outfile: # Open the json file, for reading and writing "r+"
dict_ = json.load(outfile)
dict_[add_key].append(add_value)
with open(filename, "w") as outfile:
json.dump(dict_,outfile,indent=2)

In [ ]: add_val = {'Name':'Alex',
'Age': 21,
'Roll':'21ND005'}

append_jsonfile('2021',add_val,'student.json')

In [ ]: add_jsonfile('2020',[],'student.json')

In [ ]: with open("student.json") as outfile: # If I want to overwrite then always use "w"


new_student = json.load(outfile)

In [ ]: new_student

Using pandas
Pandas is a Python library that provides various data structures and operations for manipulating numerical data. Built on top of the NumPy
library, Pandas is fast, productive and high performing.
https://www.geeksforgeeks.org/introduction-to-pandas-in-python/
In [ ]: import pandas as pd

list0 = (students['2022'][1])
print (list0,'\n')

print("This is a Panda series,\n")


A = pd.Series(list0) # Converting the above list to a Panda series
print(A)

In [ ]: list_2022 = pd.DataFrame(students['2022']) # Converting our dictionary object to a Panda Dataframe


print("This is a Panda Dataframe,\n")
print (list_2022)

In [ ]: print(list_2022[['Name','Age']])

In [ ]: large_data = pd.read_csv("ipl_data.csv") # Loading a large data set

In [ ]: large_data.keys()

In [ ]: print (large_data['city'])

In [ ]: print (large_data.loc[list(range(20,25)),['toss_winner','winner']])

In [ ]: count = 0
for i,j in large_data.iterrows():
if j['city'] == 'Kolkata':
print(j['id'],j['winner'])
count += 1
print()
print ("Games held in Kolkata: ",count)

In [ ]: large_data = pd.read_csv("ipl_data.csv",index_col='season') # Loading a large data set with a fixed index

In [ ]: # print (large_data.loc[[2008],['winner','player_of_match']])

Tasks for today


Solve the following problems.
Create a dictionary containing the name of your five of your friends, their city of birth, hometown, and a fictional passport number.
Save the above dictionary a json file and create a function, that can upload the file and add a new friend's information, and then save the
json file. Add the Head as the new friend, {'SDhar','Kolkata','Mumbai','XYZ789'}. Upload the file again and display the dictionary as a
dataframe using pandas.
Upload the file "ipl_data.csv" and count the number of matches won by "Chennai Super Kings" in each season.
From the above file "ipl_data.csv" and count the number of matches where the umpire was 'DJ Harper'.
In [ ]:
untitled39

November 11, 2023

[8]: #Solve the following ode :


#2dx/dt = -x(t) +u(t)
#5dy/dt = -y(t) + x(t)
#where u(t) = 2S(t-5) and x(0) = y(0) = 0
#s(t-5)is a step function s = 0 for t< 5 and s = 1 for t>=5

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Define the system of ODEs


def system(t, variables):
x, y = variables
u = 1 if t >= 5 else 0 # Step function for u(t)
dxdt = (-x + u) / 2
dydt = (-y + x) / 5
return [dxdt, dydt]

# Initial conditions
initial_conditions = [0, 0]

# Time span
t_span = (0, 40) # Adjust the time range as needed

# Solve the ODEs using solve_ivp


solution = solve_ivp(system, t_span, initial_conditions, method='RK45',␣
↪dense_output=True)

# Evaluate the solution at specific time points


t_eval = np.linspace(0, 40, 400) # Adjust the time points as needed
y_eval = solution.sol(t_eval)

# Plot the results


plt.plot(t_eval, y_eval[0], label='x(t)')
plt.plot(t_eval, y_eval[1], label='y(t)')
plt.xlabel('Time')
plt.ylabel('Values')

1
plt.legend()
plt.show()

[9]: #Solve the following ODE :


# dx/dt = Ax
# where A is a matrix = [0,1,-2]
#[1,3,-1]
#[-2,-1,-2]
#Solve the above equation using solve ivp and compare with the solution␣
↪x(t) = e^(At)*x(0)

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Define the matrix A


A = np.array([[0, 1, -2],
[1, 3, -1],
[-2, -1, -2]])

# Define the system of ODEs

2
def system(t, x):
return np.dot(A, x)

# Initial condition
x0 = np.array([1, 0, 0]) # You can change the initial condition as needed

# Time span
t_span = (0, 5) # Adjust the time range as needed

# Solve the ODEs using solve_ivp


solution = solve_ivp(system, t_span, x0, dense_output=True)

# Analytical solution
def analytical_solution(t):
eigenvalues, eigenvectors = np.linalg.eig(A)
matrix_exp = np.dot(eigenvectors, np.dot(np.diag(np.exp(eigenvalues * t)),␣
↪np.linalg.inv(eigenvectors)))

return np.dot(matrix_exp, x0)

# Evaluate the analytical solution at specific time points


t_eval = np.linspace(0, 5, 1000) # Adjust the time points as needed
x_analytical = np.array([analytical_solution(t) for t in t_eval])

# Plot the results


plt.plot(solution.t, solution.y[0], label='Numerical solution x(t)')
plt.plot(t_eval, x_analytical[:, 0], '--', label='Analytical solution x(t)')
plt.xlabel('Time')
plt.ylabel('Values')
plt.legend()
plt.show()

3
[14]: #Convert the second order differential equation to a set of ODEs
# y" + 3 y' + yt = cost ,
#where y'(0) = -2 and y(0) = 2 . Plot the solutions of the ODEs for some time␣
↪range.

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Define the system of ODEs


def system(t, y):
dydt = y[1]
dvdt = np.cos(t) - 3 * y[1] - y[0]*t
return [dydt, dvdt]

# Initial conditions
initial_conditions = [2, -2]

# Time span

4
t_span = (0, 50) # Adjust the time range as needed

# Solve the ODEs using solve_ivp


solution = solve_ivp(system, t_span, initial_conditions, dense_output=True)

# Plot the results


plt.plot(solution.t, solution.y[0], label='y(t)')
plt.plot(solution.t, solution.y[1], label="y'(t)")
plt.xlabel('Time')
plt.ylabel('Values')
plt.legend()
plt.show()

[16]: #Solve the differential equation:


# dx0/dt = sigma(x1-x0)
#dx1/dt = x0(rho-x2)-x1
#dx2/dt = x0x1-beta*x2

#sigma , rho , beta = 5,4,1 You can take the time, . Plot vs , and vs . You can␣
↪use the initial state to be x = [1,1,1]

5
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Define the system of ODEs


def system(t, x, sigma, rho, beta):
dx0dt = sigma * (x[1] - x[0])
dx1dt = x[0] * (rho - x[2]) - x[1]
dx2dt = x[0] * x[1] - beta * x[2]
return [dx0dt, dx1dt, dx2dt]

# Parameters
sigma = 5
rho = 4
beta = 1

# Initial conditions
initial_conditions = [1, 1, 1]

# Time span
t_span = (0, 10) # Adjust the time range as needed

# Solve the ODEs using solve_ivp with a higher number of points


solution = solve_ivp(system, t_span, initial_conditions, args=(sigma, rho,␣
↪beta), dense_output=True, t_eval=np.linspace(0, 10, 1000))

# Plot the results


plt.plot(solution.t, solution.y[0], label='x0(t)')
plt.plot(solution.t, solution.y[1], label='x1(t)')
plt.plot(solution.t, solution.y[2], label='x2(t)')
plt.xlabel('Time')
plt.ylabel('Values')
plt.legend()
plt.show()

6
[18]: #Write a function that implements composite simpsons rule and integrates the␣
↪function f(x) = x^2 logx between x = 1 and x =10

import numpy as np

def f(x):
return x**2 * np.log(x)

def composite_simpsons_rule(f, a, b, n):


"""
Compute the definite integral of a function using Composite Simpson's Rule.

Parameters:
- f: The function to be integrated.
- a: The lower limit of integration.
- b: The upper limit of integration.
- n: The number of subintervals.

Returns:
The approximate definite integral.
"""
h = (b - a) / n

7
x_values = np.linspace(a, b, n + 1)

integral = f(a) + f(b)

for i in range(1, n):


if i % 2 == 0:
integral += 2 * f(x_values[i])
else:
integral += 4 * f(x_values[i])

return (h / 3) * integral

# Test the function with the given example


a = 1
b = 10
n = 100 # You can adjust the number of subintervals as needed

result = composite_simpsons_rule(f, a, b, n)
print("Approximate integral:", result)

#Compare the above results obtained using the function and trapezoidal method.
#Now, taking in both cases, show whether the Simpson's or trapezoidal rule␣
↪gives you a value closer to the inbuilt function.

import numpy as np
from scipy.integrate import quad

def f(x):
return x**2 * np.log(x)

def composite_simpsons_rule(f, a, b, n):


h = (b - a) / n
x_values = np.linspace(a, b, n + 1)

integral = f(a) + f(b)

for i in range(1, n):


if i % 2 == 0:
integral += 2 * f(x_values[i])
else:
integral += 4 * f(x_values[i])

return (h / 3) * integral

def trapezoidal_rule(f, a, b, n):

8
h = (b - a) / n
x_values = np.linspace(a, b, n + 1)

integral = 0.5 * (f(a) + f(b))

for i in range(1, n):


integral += f(x_values[i])

return h * integral

# Interval
a = 1
b = 10

# Number of subintervals
n = 1000 # Adjust as needed

# Compute integrals using different methods


result_simpsons = composite_simpsons_rule(f, a, b, n)
result_trapezoidal = trapezoidal_rule(f, a, b, n)
result_quad, _ = quad(f, a, b)

# Print results
print("Composite Simpson's Rule result:", result_simpsons)
print("Trapezoidal Rule result:", result_trapezoidal)
print("SciPy quad function result:", result_quad)

Approximate integral: 656.5283636766392


Composite Simpson's Rule result: 656.5283643312829
Trapezoidal Rule result: 656.5287359303527
SciPy quad function result: 656.5283643313485

[19]: #Consider a one-dimensional simple harmonic oscillator with m = 1 andk =1/2 .␣


↪Use a

#ODE solver to find the displacement of the oscillator.


#Using matplotlib, plot the displacement x with time , for t = [0,50]

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Define the differential equation for the simple harmonic oscillator


def harmonic_oscillator(t, y):
x, v = y
m = 1 # mass
k = 1/2 # spring constant
dxdt = v

9
dvdt = -k * x
return [dxdt, dvdt]

# Initial conditions: displacement x(0) = 1, velocity v(0) = 0


initial_conditions = [1, 0]

# Time span
t_span = (0, 50)

# Solve the ODE using solve_ivp


solution = solve_ivp(harmonic_oscillator, t_span, initial_conditions, t_eval=np.
↪linspace(0, 50, 1000))

# Plot the displacement x with time t


plt.plot(solution.t, solution.y[0])
plt.xlabel('Time (t)')
plt.ylabel('Displacement (x)')
plt.title('Simple Harmonic Oscillator')
plt.show()

10
[20]: #Now, consider two harmonic oscillators with displacements along the and axis,
#respectively. Using, for the two oscillators, use a ODE solver to find the
#displacement of the two oscillators along the x and yaxis.
#Using matplotlib, plot vs to create the Lissajous figures: a)k1 = 1 and k2 =␣
↪1, b)k1 =1

#and k2 = 1/2 , and c) k1 =1 and k2 = 1/4.

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Define the system of ODEs for the coupled harmonic oscillators


def coupled_harmonic_oscillators(t, y, k1, k2):
x, v_x, y, v_y = y
dxdt = v_x
dv_xdt = -k1 * x
dydt = v_y
dv_ydt = -k2 * y
return [dxdt, dv_xdt, dydt, dv_ydt]

# Time span
t_span = (0, 50)

# Initial conditions: displacement x(0) = 1, velocity v_x(0) = 0, y(0) = 1,␣


↪velocity v_y(0) = 0

initial_conditions = [1, 0, 1, 0]

# Define different values of k1 and k2 for Lissajous figures


k_values = [(1, 1), (1, 1/2), (1, 1/4)]

# Plot Lissajous figures for different k1 and k2


for k1, k2 in k_values:
# Solve the ODE using solve_ivp
solution = solve_ivp(coupled_harmonic_oscillators, t_span,␣
↪initial_conditions, args=(k1, k2), t_eval=np.linspace(0, 50, 1000))

# Plot the displacement x vs y


plt.plot(solution.y[0], solution.y[2], label=f'k1={k1}, k2={k2}')

plt.xlabel('Displacement along x-axis')


plt.ylabel('Displacement along y-axis')
plt.title('Lissajous Figures for Coupled Harmonic Oscillators')
plt.legend()
plt.show()

11
[23]: #The working of a dye-based, single-mode microlaser can be modelled using the
##following equation of motion:
#dn/dt = -kn + NE(n+1)p
#dp/dt = -[Td+E(n+1)p]+Tu(1-p)

#where is the average number of photons inside the cavity, is total number of␣
↪dye

#molecules inside the cavity, is the fraction of excited molecules, is the␣


↪rates at

#which is photon emitted by the dye (absorption is assumed to be negligible),␣


↪and finally Tu

#and Td are the molecular pumping rate and molecular de-excitation rates
#Write a function that allows you to find the steady state photon number, ,
#such that at some time . Take time steps of 20 (in units of kappa), i.e.,␣
↪t_span =[0,20]

##. Moreover, the solution of the first time span can be taken as the initial␣
↪state of the second

#Note your function needs to check for steady state by ensuring that the photon␣
↪number

12
#between two time steps change (absolute relative value) by a value smaller␣
↪that 10^-2

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

N = 10**8
E = 10**(-5)
Tu = 0.25*10**(-4)
Td = 0.25

def system_of_equations(t, variables):


n , p = variables
dndt = -n + N*E*(n+1)*p
dpdt = -p*(Td+E*(n+1))+Tu*(1-p)
return [dndt, dpdt]

time_spans = (0,20)

initial_conditions = [0, 0]

def find_difference_last_two(arr):
#print(len(arr))
if len(arr) >= 3:
last_element = arr[-1]
second_last_element = arr[-2]
difference = np.abs((last_element - second_last_element) /␣
↪second_last_element)

print(difference)
return difference
else:
return None

for io in range(1,100,1):

sol = solve_ivp(system_of_equations,time_spans, initial_conditions,␣


↪t_eval=np.linspace(0,20, 3))

t_values = sol.t

13
n_values = sol.y[0]
p_values = sol.y[1]

initial_conditions = [n_values[-1], p_values[-1]]

result = find_difference_last_two(n_values)

if result >= 10**(-2):


print(f"Time span {io*time_spans } has not reached the steady state")
else:
print(f"Time span {io*time_spans } has reached the steady state")
break

plt.plot(t_values, n_values, label='n')

plt.xlabel('Time')
plt.ylabel('Values')
plt.legend()
plt.show()

0.1303265633652936
Time span (0, 20) has not reached the steady state
0.0014174187624710543
Time span (0, 20, 0, 20) has reached the steady state

14
[24]: #In the above problem, change Tu/Td in the range , and by plotting the
#steady state photon number and the in the log scale, show that there is␣
↪driven}dissipative phase transition in the microlaser syst

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

N = 10**8
E = 10**(-5)
Td = 0.25

Tu_values = np.logspace(-6, -2, 100)

steady_state_photon_numbers = []

for Tu in Tu_values:
def system_of_equations(t, variables):
n, p = variables
dndt = -n + N * E * (n + 1) * p

15
dpdt = -p * (Td + E * (n + 1)) + Tu * (1 - p)
return [dndt, dpdt]

time_span = (0, 20)


initial_conditions = [0, 0]

sol = solve_ivp(system_of_equations, time_span, initial_conditions,␣


↪t_eval=[20])

n_values = sol.y[0]

steady_state_photon_numbers.append(n_values[0])

plt.semilogx(Tu_values, np.log10(steady_state_photon_numbers))
plt.xlabel('Tu (log scale)')
plt.ylabel('Steady-State Photon Number (log scale)')
plt.title('Steady-State Photon Number vs Tu (log scale)')
plt.grid(True)
plt.show()

16
[4]: #Solve the following differential equation
# dx/dt = x + 1
#dy/dt = (-1/5)*(y-x) where x(0) = y(0) = 0 and t = [0,10]

# Create a dictionary dict = [t value , x value , y value] and store the␣


↪output of the differential equaution in the dictionary
# save it both as a numpy and a pickle file

import numpy as np
from scipy.integrate import solve_ivp
import pickle

# Define the system of differential equations


def system(t, vars):
x, y = vars
dxdt = x + 1
dydt = (-1/5) * (y - x)
return [dxdt, dydt]

# Set the initial conditions and the time span


initial_conditions = [0, 0] # x(0) = 0, y(0) = 0
t_span = [0, 10] # Time span [0, 10]

# Solve the system of differential equations


solution = solve_ivp(system, t_span, initial_conditions, t_eval=np.linspace(0,␣
↪10, 1000))

# Create a dictionary to store the solution


output_dict = {
't': solution.t,
'x': solution.y[0],
'y': solution.y[1]
}

# Save the dictionary as a NumPy file


np.save('solution.npy', output_dict)

# Save the dictionary as a pickle file


with open('solution.pkl', 'wb') as f:
pickle.dump(output_dict, f)

import matplotlib.pyplot as plt

# Load the pickle file

17
with open('solution.pkl', 'rb') as f:
loaded_data = pickle.load(f)

# Extract data
t = loaded_data['t']
x = loaded_data['x']
y = loaded_data['y']

# Create a plot
plt.figure(figsize=(8, 6))
plt.plot(t, x, label='x(t)')
plt.plot(t, y, label='y(t)')
plt.xlabel('Time (t)')
plt.ylabel('Values')
plt.legend()
plt.title('Plot of x(t) and y(t)')
plt.grid(True)

# Show the plot


plt.show()

18
[5]: #Create a dictionary containing the name of your five of your friends, their␣
↪city of birth, hometown, and a fictional passport number.

#Now write a function that can add a new friend's information. Add the Head as␣
↪the new friend, {'HOD','Kolkata','Mumbai','XYZ789'}.

# Create a dictionary of your friends' information

friends_info = {
'Friend1': {'City of Birth': 'City1', 'Hometown': 'Hometown1', 'Passport␣
↪Number': 'Passport1'},

'Friend2': {'City of Birth': 'City2', 'Hometown': 'Hometown2', 'Passport␣


↪Number': 'Passport2'},

'Friend3': {'City of Birth': 'City3', 'Hometown': 'Hometown3', 'Passport␣


↪Number': 'Passport3'},

'Friend4': {'City of Birth': 'City4', 'Hometown': 'Hometown4', 'Passport␣


↪Number': 'Passport4'},

'Friend5': {'City of Birth': 'City5', 'Hometown': 'Hometown5', 'Passport␣


↪Number': 'Passport5'}

# Function to add a new friend's information


def add_new_friend(friends_dict, name, city_of_birth, hometown,␣
↪passport_number):

friends_dict[name] = {'City of Birth': city_of_birth, 'Hometown': hometown,␣


↪'Passport Number': passport_number}

# Add the Head as a new friend


add_new_friend(friends_info, 'Head', 'Kolkata', 'Mumbai', 'XYZ789')

# Print the updated dictionary


for friend, info in friends_info.items():
print(friend, info)

Friend1 {'City of Birth': 'City1', 'Hometown': 'Hometown1', 'Passport Number':


'Passport1'}
Friend2 {'City of Birth': 'City2', 'Hometown': 'Hometown2', 'Passport Number':
'Passport2'}
Friend3 {'City of Birth': 'City3', 'Hometown': 'Hometown3', 'Passport Number':
'Passport3'}
Friend4 {'City of Birth': 'City4', 'Hometown': 'Hometown4', 'Passport Number':
'Passport4'}
Friend5 {'City of Birth': 'City5', 'Hometown': 'Hometown5', 'Passport Number':
'Passport5'}
Head {'City of Birth': 'Kolkata', 'Hometown': 'Mumbai', 'Passport Number':
'XYZ789'}

19
[12]: #Consider the Lorenz system, a well-known example of a chaotic dynamical system.
↪ The Lorenz system is described by a set of first-order

#ordinary differential equations (ODEs) that model atmospheric convection. It␣


↪is characterized by its sensitivity to initial conditions and the

#emergence of strange attractors.


#The Lorenz system is defined by the following ODEs:
#dx/dt = sigma*(y-x)
#dy/dt = x*(rho-z)-y
#dz/dt = xy - beta*z
# here x,y ,z are state variables representing temperature, horizontal␣
↪velocity, and vertical velocity, respectively. The parameters , ,

#and control the behavior of the system.

#a. Simulate the Lorenz system for two different sets of parameter values:
# Set 1 : sigma = 10 rho = 24.184113703162021 and beta = 8/3
# Set 2 : sigma = 10 rho = 24.184113703162022 and beta = 8/3

# Provide separate plots for each set, showing the time evolution of the␣
↪state variables x, y, and z . Annotate each plot with the
#corresponding parameter values. Describe the observed behavior and differences␣
↪between the two sets

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

# Define the Lorenz system as a set of first-order ODEs


def lorenz_system(t, state, sigma, rho, beta):
x, y, z = state
dxdt = sigma * (y - x)
dydt = x * (rho - z) - y
dzdt = x * y - beta * z
return [dxdt, dydt, dzdt]

# Parameters for Set 1


sigma1 = 10
rho1 = 24.184113703162021
beta1 = 8/3

# Parameters for Set 2


sigma2 = 10
rho2 = 24.184113703162022
beta2 = 8/3

# Initial conditions
initial_state = [1, 0, 0]

20
# Time span
t_span = (0, 100)

# Simulate the Lorenz system for Set 1


solution1 = solve_ivp(lorenz_system, t_span, initial_state, args=(sigma1, rho1,␣
↪beta1), t_eval=np.linspace(0, 100, 5000))

# Simulate the Lorenz system for Set 2


solution2 = solve_ivp(lorenz_system, t_span, initial_state, args=(sigma2, rho2,␣
↪beta2), t_eval=np.linspace(0, 100, 5000))

# Create plots for Set 1


plt.figure(figsize=(12, 6))
plt.subplot(121)
plt.plot(solution1.t, solution1.y[0], label='x', color='b')
plt.plot(solution1.t, solution1.y[1], label='y', color='g')
plt.plot(solution1.t, solution1.y[2], label='z', color='r')
plt.xlabel('Time')
plt.ylabel('State Variables')
plt.title('Lorenz System - Set 1')
plt.legend()
plt.annotate(f'sigma={sigma1}, rho={rho1}, beta={beta1}', xy=(0.1, 0.9),␣
↪xycoords='axes fraction')

# Create plots for Set 2


plt.subplot(122)
plt.plot(solution2.t, solution2.y[0], label='x', color='b')
plt.plot(solution2.t, solution2.y[1], label='y', color='g')
plt.plot(solution2.t, solution2.y[2], label='z', color='r')
plt.xlabel('Time')
plt.ylabel('State Variables')
plt.title('Lorenz System - Set 2')
plt.legend()
plt.annotate(f'sigma={sigma2}, rho={rho2}, beta={beta2}', xy=(0.1, 0.9),␣
↪xycoords='axes fraction')

plt.tight_layout()
plt.show()

21
[14]: import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt

# define system in terms of separated differential equations


def lorenz(state, t, sigma, rho, beta):
x, y, z = state # Unpack the state vector
return sigma*(y-x), x*(rho-z)-y, x*y - beta*z # Derivatives

# time points
t = np.linspace(0, 100, 5000)

# initial condition
state0 = [1.0, 1.0, 1.0]

# parameter values
params_set1 = (10, 24.184113703162021, 8/3)
params_set2 = (10, 24.184113703162022, 8/3)

# solve system of differential equations


state_set1 = odeint(lorenz, state0, t, args=params_set1)
state_set2 = odeint(lorenz, state0, t, args=params_set2)

# plot results
fig, axs = plt.subplots(2, figsize=(10, 10))

axs[0].plot(t, state_set1)

22
axs[0].set_title('Lorenz system evolution for sigma={}, rho={}, beta={}'.
↪format(*params_set1))

axs[0].legend(['x', 'y', 'z'])


axs[0].grid()

axs[1].plot(t, state_set2)
axs[1].set_title('Lorenz system evolution for sigma={}, rho={}, beta={}'.
↪format(*params_set2))

axs[1].legend(['x', 'y', 'z'])


axs[1].grid()

plt.tight_layout()
plt.show()

23
[15]: #The trajectory of a cannon shell, fired from a point in ground, is given by a␣
↪set of equations:

#dx/dt = vx
#dy/dt = vy
#dvx/dt = -bvvx
#dvy/dt = -g-bvvy

# where b is a drag constant,g = 9.8 m/s^2 and v = (vx^2+vy^2)^0.5 is the␣


↪velocity of the projection . Choose b =0.0011 m/s and the initial launch
# velocity . Plot the V/s trajectories for various equally spaced launch␣
↪angles in the same frame. Note that the trajectory

#starts with the launch of cannon ball from origin and ends when cannon ball␣
↪hits the ground. . Plot the range as a function of launch angles.

#Among these launch angles, find the angle that has the greatest range.
#Hint 1: While solving the ODE, take the time span to greater than 20 ( Why?)␣
↪\\ Hint 2: The solution will give the parts where y<0. You don't

#need this data ( Why?). Write a function to filter out y<0.

import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt

# define system in terms of separated differential equations


def cannonball(state, t, b, g):
x, y, vx, vy = state # Unpack the state vector
v = np.sqrt(vx**2 + vy**2)
return vx, vy, -b*v*vx, -g-b*v*vy # Derivatives

# time points
t = np.linspace(0, 20, 1000)

# initial condition
state0 = [0, 0, 10, 10]

# parameter values
b = 0.0011
g = 9.8

# solve system of differential equations for different launch angles


angles = np.linspace(0, np.pi/2, 10) # 10 equally spaced launch angles from 0␣
↪to pi/2

ranges = []
for angle in angles:
state0[2] = 10*np.cos(angle) # vx
state0[3] = 10*np.sin(angle) # vy
state = odeint(cannonball, state0, t, args=(b, g))

24
# filter out parts where y < 0
state = state[state[:,1] >= 0]
# plot trajectory
plt.plot(state[:,0], state[:,1], label='angle = {:.2f}'.format(angle))
# calculate range
ranges.append(state[-1,0])

plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid()
plt.show()

# plot range as a function of launch angles


plt.figure()
plt.plot(angles, ranges, 'o-')
plt.xlabel('Launch angle (rad)')
plt.ylabel('Range (m)')
plt.grid()
plt.show()

# find the angle that gives the greatest range


max_range = max(ranges)
max_range_index = ranges.index(max_range)
max_range_angle = angles[max_range_index]
print('The angle that gives the greatest range is {:.2f} rad'.
↪format(max_range_angle))

25
26
The angle that gives the greatest range is 0.70 rad

[17]: #Import/upload the "ipl_data.csv" file shared on Teams (please log on to Teams␣
↪on your

#computer) and save it in a dictionary called "ipl_dict".


#Find the number of times Asad Rauf or Aleem Dar was the first umpire in 2008.

import pandas as pd

file_path = r'C:\Users\MRINMOY MONDAL\Downloads\ipl_data.csv'


# Load the data

df = pd.read_csv(file_path)

# Convert the DataFrame to a dictionary


ipl_dict = df.to_dict()

# Filter the data for 2008 and count the number of times Asad Rauf or Aleem Dar␣
↪was the first umpire

27
count = df[(df['season'] == 2008) & ((df['umpire1'] == 'Asad Rauf') |␣
↪(df['umpire1'] == 'Aleem Dar'))].shape[0]

print(f"Asad Rauf or Aleem Dar was the first umpire in {count} matches in 2008.
↪")

Asad Rauf or Aleem Dar was the first umpire in 15 matches in 2008.

[20]: #Using pandas, read the “ipl data.csv” file.


#Do the following:
##i) Create and print the smaller panda dataframe that contains the values␣
↪corresponding

#to season, host city and winners (please check the exact key).
#ii) Count the number of wins and losses for Kolkata Knight Riders in 2010,␣
↪when they

#have hosted the match in Kolkata.

import pandas as pd

import pandas as pd

file_path = r'C:\Users\MRINMOY MONDAL\Downloads\ipl_data.csv'

df = pd.read_csv(file_path)

smaller_df = df[['season', 'city', 'winner']]

print(smaller_df)

kolkata_knight_riders_2010 = df[(df['season'] == 2010) & (df['city'] ==␣


↪'Kolkata')]

kolkata_knight_riders_wins =␣
↪kolkata_knight_riders_2010[kolkata_knight_riders_2010['winner'] == 'Kolkata␣

↪Knight Riders']

kolkata_knight_riders_losses =␣
↪kolkata_knight_riders_2010[kolkata_knight_riders_2010['team1'] == 'Kolkata␣

↪Knight Riders']

print("Number of wins for Kolkata Knight Riders in 2010 in Kolkata:",␣


↪len(kolkata_knight_riders_wins))

print("Number of losses for Kolkata Knight Riders in 2010 in Kolkata:",␣


↪len(kolkata_knight_riders_wins)-len(kolkata_knight_riders_losses))

28
season city winner
0 2008 Bangalore Kolkata Knight Riders
1 2008 Chandigarh Chennai Super Kings
2 2008 Delhi Delhi Daredevils
3 2008 Mumbai Royal Challengers Bangalore
4 2008 Kolkata Kolkata Knight Riders
.. … … …
572 2016 Raipur Royal Challengers Bangalore
573 2016 Bangalore Royal Challengers Bangalore
574 2016 Delhi Sunrisers Hyderabad
575 2016 Delhi Sunrisers Hyderabad
576 2016 Bangalore Sunrisers Hyderabad

[577 rows x 3 columns]


Number of wins for Kolkata Knight Riders in 2010 in Kolkata: 5
Number of losses for Kolkata Knight Riders in 2010 in Kolkata: 2

[3]: #Say you are in a casino and are throwing a pair of dies.Using np.random.
↪randint write a single line of code that simulates a 10000 throws of the

#pair of dies . Write a code that counts 1) probability of getting 8 2)␣


↪probability of getting a hard 8 3)probability of getting an easy 8 .␣
↪Compare with exact solutions

import numpy as np

# Simulate 10000 throws of a pair of dice


throws = np.random.randint(1, 7, (10000, 2))

# Calculate the sum of the dice for each throw


sums = np.sum(throws, axis=1)

# Calculate the probability of getting 8


prob_8 = np.mean(sums == 8)

# Calculate the probability of getting a hard 8 (two 4s)


prob_hard_8 = np.mean(np.all(throws == 4, axis=1))

# Calculate the probability of getting an easy 8 (not two 4s)


prob_easy_8 = prob_8 - prob_hard_8

print(f"Probability of getting 8: {prob_8}")


print(f"Probability of getting a hard 8: {prob_hard_8}")
print(f"Probability of getting an easy 8: {prob_easy_8}")

Probability of getting 8: 0.1418


Probability of getting a hard 8: 0.0304
Probability of getting an easy 8: 0.11140000000000001

29
[7]: #Define a Matrix that takes a n*n matrix A as an input and outputs n*n-1 matrix␣
↪B containing only off diagonal elements.

#Using numpy define A as a 4 dimensional identity matrix and show that B is a␣


↪null matrix.

import numpy as np

# Define the dimension (n)


n = 4

# Create a 4x4 identity matrix A


A = np.eye(n)

# Define a matrix B containing only off-diagonal elements (n x (n-1) matrix)


B = np.delete(A, 0, axis=1) # Remove the first column

print("Matrix A (4x4 Identity Matrix):")


print(A)

print("\nMatrix B (Off-Diagonal Elements):")


print(B)

Matrix A (4x4 Identity Matrix):


[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]

Matrix B (Off-Diagonal Elements):


[[0. 0. 0.]
[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
The exponential function is given by the power series : exp(x) = lim k tends infinity
(1+x+x2/2!+….+x k/k!) where k! is the factorial. This can be approximated by the function :
# exp(x) = f(x,n) where is the integratiuon is done upto some finite integer n. Write a recursive
function for f(x,n) . Use np.factorial to find factorial of a number . Consider a range of whole
number values of x : 0<x<20. Use while statement to run a loop over values of x , calculate the
minimum n required for each x such that the relative error between f(x,n) and exp(x) is less than
10^-10.
import numpy as np
def f(x, n): if n == 0: return 1 # Base case: 0! = 1 else: return f(x, n - 1) + (x ** n) /
np.math.factorial(n)
def relative_error(approx, exact): return abs((approx - exact) / exact)
tolerance = 1e-10 x_values = range(1, 20)

30
for x in x_values: n = 0 approx = f(x, n) exact = np.exp(x)
while relative_error(approx, exact) > tolerance:
n += 1
approx = f(x, n)

print(f"For x = {x}, minimum n for accuracy < 10^-10: {n}")

[11]: #The exponential function is given by the power series : exp(x) = lim k tends␣
↪infinity (1+x+x^2/2!+....+x^k/k!) where k! is the factorial. This can be␣

↪approximated by the function : # exp(x) = f(x,n) where is the integratiuon␣

↪is done upto some finite integer n. Write a recursive function for f(x,n) .␣

↪Use np.factorial to find factorial of a number . Consider a range of whole␣

↪number values of x : 0<x<20. Use while statement to run a loop over values␣

↪of x ,

# calculate the minimum n required for each x such that the relative␣
↪error between f(x,n) and exp(x) is less than 10^-10.

import numpy as np
import matplotlib.pyplot as plt

def f(x, n):


if n == 0:
return 1 # Base case: 0! = 1
else:
return f(x, n - 1) + (x ** n) / np.math.factorial(n)

def relative_error(approx, exact):


return abs((approx - exact) / exact)

tolerance = 1e-10
x_values = range(1, 20)
n_values = []

for x in x_values:
n = 0
approx = f(x, n)
exact = np.exp(x)

while relative_error(approx, exact) > tolerance:


n += 1
approx = f(x, n)

n_values.append(n)

plt.plot(x_values, n_values)

31
plt.xlabel('x')
plt.ylabel('Minimum n for Accuracy < 10^-10')
plt.title('Minimum n vs. x')
plt.grid(True)
plt.show()

[1]: #3. Engineering - Heat Conduction:

#Problem: The heat conduction equation in one dimension is given by


#dU/dt = alpha * d^2U/dx^2

import numpy as np
import matplotlib.pyplot as plt

# Define parameters
alpha = 0.01
L = 1.0 # Length of the rod
Nx = 100 # Number of spatial grid points
Nt = 1000 # Number of time steps
dx = L / (Nx - 1)

32
dt = 0.001

# Initialize temperature field


U = np.zeros(Nx)
U[int(Nx / 4):int(3 * Nx / 4)] = 1.0 # Initial condition: Heat source in the␣
↪middle

# Create an array to store the temperature field at each time step


U_history = [U.copy()]

# Perform time-stepping
for n in range(Nt):
U_new = U.copy()
for i in range(1, Nx - 1):
U_new[i] = U[i] + alpha * dt / dx**2 * (U[i + 1] - 2 * U[i] + U[i - 1])
U = U_new
U_history.append(U.copy())

# Plot the temperature field over time


for i, U in enumerate(U_history):
plt.plot(np.linspace(0, L, Nx), U, label=f'Timestep {i * Nt}')
plt.xlabel('Spatial Coordinate (x)')
plt.ylabel('Temperature (U)')
plt.title('Heat Conduction in a 1D Rod')
plt.legend()
plt.show()

33
34
[3]: pip install pandoc

Requirement already satisfied: pandoc in c:\users\mrinmoy


mondal\anaconda3\lib\site-packages (2.3)
Requirement already satisfied: plumbum in c:\users\mrinmoy
mondal\anaconda3\lib\site-packages (from pandoc) (1.8.2)
Requirement already satisfied: ply in c:\users\mrinmoy
mondal\anaconda3\lib\site-packages (from pandoc) (3.11)
Requirement already satisfied: pywin32 in c:\users\mrinmoy
mondal\anaconda3\lib\site-packages (from plumbum->pandoc) (305.1)
Note: you may need to restart the kernel to use updated packages.

[ ]:

35

You might also like