#!/usr/bin/env python
# coding: utf-8
#
#
#
# # Introduction to NumPy
#
# ### Modules - Basics
#
# By Thorvald Ballestad, Niels Henrik Aase, Eilif Sommer Øyre and Jon Andreas Støvneng.
#
# Last edited: October 14th 2019
#
# ___
# ## Introduction
#
# NumPy is a Python package that is omnipresent in computational physics.
# It greatly increase calculation speed and offers a more intuitive way of working with our data.
#
# This notebook covers the very basics of using NumPy in computational physics, with the assumption that the reader has basic Python knowledge.
#
# Firstly, we must include the `numpy` package, so that we are allowed to use it.
# If you are unfamiliar with the concept of importing packages in Python, you can think of it as including a file with functions that we want to use.
# In[1]:
import numpy as np # 'as np' tells python to name NumPy np
# We will use this for demonstration. Plotting is not part of this notebook
import matplotlib.pyplot as plt
# Above we also imported `matplotlib` for plotting. See [this](https://nbviewer.jupyter.org/urls/www.numfys.net/media/notebooks/basic_plotting.ipynb) notebook if you wish to learn more about plotting.
#
# NumPy introduces a new data container, known as an _array_. Arrays are the most important feature of NumPy, and all features of the NumPy package are based around this data container. It looks much like a list, but has some important features that lists do not have. We will explore this by examples:
# In[2]:
my_list = [0, 1, 2, 3, 4] # Normal Python list
my_array = np.array([0, 1, 2, 3, 4]) # NumPy array
# Let us see what they look like:
print(my_list)
print(my_array)
# Note how they look almost the same, the only difference being that arrays do not have commas between it's elements (when printed).
#
# Let us look at the power of these arrays!
# In[3]:
# Remember how lists behave when we whish to do mathematical operations on them:
print("my_list*2:\t", my_list*2)
# We get our list repeated, clearly not what we intended!
# In[4]:
print("my_array*2:\t", my_array*2)
# Each element is multiplied by two, exactly the behavior we are used with from vector calculus.
#
# We can actually do this with most mathematical operators and functions.
# In[5]:
# remember that x**y is python syntax for x^y, that is x to the power of y
print("my_array**2:\t\t", my_array**2)
print("my_array**2 - 3/2:\t", my_array**2 - 3/2)
# We can even do this with functions!
def my_function(x):
y = x + 1
return y**2
print("my_function(my_array):\t", my_function(my_array))
# The latter example demonstrates the power of operating on arrays element-wise. If you use a normal Python list instead of a NumPy-array, one would have to iterate through all the elements of `x` using a `for` loop and then append the results to a new list to get the same result! This is shown below.
# In[6]:
def my_function2(x):
y = [] # Creating an empty python list
for i in range(len(x)):
y_element = x[i] + 1
y.append(y_element**2)
return y
print("my_function2(my_list):\t", my_function2(my_list))
# The lesson is: use NumPy arrays, not Python lists.
#
# NumPy includes many mathematical functions, like $\sin, \cos, \cosh,\exp, \log$.
# They usually have intuitive names, such as `np.sin`, `np.exp`, and so forth.
# One can often try the mathematical name, and hope that NumPy has the function.
# In[7]:
# Some mathematical functions
x = np.array([-1, -0.5, 0, 0.5, 1])
print("sin(x) =\t", np.sin(x))
print("arccos(x) =\t", np.arccos(x))
print("log(x+10) = \t", np.log(x+10))
print("log10(x+10) =\t", np.log10(x+10))
# You might already see how practical this is. For example if we wish to plot $y=f(x)$, we can just pass the entire $x$-array to our function, instead of using a `for` loop as we normally would with Python.
# In[8]:
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
y = np.sin(x) # find sin(x) for each of the elements in x
print("x:\t", x)
print("y:\t", y)
plt.plot(x, y) # Plotting is not part of this notebook, we use it here only for demonstration purposes
plt.show()
# With boring old Python lists, we would have to loop through `x` and calculate $y(x)$ for each value, as was shown above.
# That is not only more code to write, it is also significantly slower when the number of points become large.
# This is all well and good, but it is a bit tedious to write `np.array([1,2,3,4,5,6,7,8,9,10])`. And what if we wanted an even longer array, say a hundred numbers? Luckily NumPy has built in methods for getting various arrays.
#
# ## Generating arrays
# The two most common functions for generating arrays are `np.linspace` and `np.arange`.
# Both functions give us points on an interval.
# `np.linspace` lets us decide how many points we want and `np.arange` lets us decide the spacing between the points.
# Let us use some examples to make this clearer.
# In[9]:
# Get an array of 50 values between 0 and 2
x_lin = np.linspace(0, 2, 50)
print(x_lin)
# `np.linspace` is useful if we for example wish to plot a function $f(x)$.
# Then we need a list of $x$-values for which we can calculate the $y=f(x)$ values.
# Notice how the spacing between the points are the same, the points are *lin*early distributed in *space*.
# Sometimes we wish to have more control over the distance between points, rather than the number of points. For this we use `np.arange`.
# In[10]:
# Get an array of values between 0 and 2 with spacing 0.1
x_range = np.arange(0, 2, 0.1)
print(x_range)
# Notice that the endpoint, 2, is not included, much like the built-in Python function `range`.
#
# Choosing between `linspace` and `arange` depends on our need.
# Often we just need some points on an interval, and the exact spacing does not really matter.
# In this situation we obviously want to use `linspace`.
# However, sometimes the spacing is more important than the number of points, then `arange` is used.
# One can also generate an array from an existing list, using `np.array()`, as was done in the introduction of this notebook.
# There are many more functions that give us arrays of various shapes and sizes, we will not go through them all here. But two more functions should be mentioned, `np.zeros` and `np.ones`, which gives us a list of zeros and ones respectively.
# In[11]:
# Generate two lists, one with zeros and one with ones, both with length 20
zeros = np.zeros(20)
ones = np.ones(20)
# Let us see how they look
print(zeros)
print(ones)
# `np.zeros` and `np.ones` are often useful for initializing arrays that are to be used later.
# ## Slicing and indexing
# With arrays, it is useful to refer to only the elements we want, just like we do with indexing in normal lists.
#
# Lets review normal lists first.
# Remember that lists are 0-indexed, that is the first element is element 0, the second is 1 and so on.
# In[12]:
my_list = [1, 2, 3, 4] # Normal Python list
# We can access different parts of the list by slicing and indexing:
print("my_list[0]:\t", my_list[0]) # First element
print("my_list[:2]:\t", my_list[:2]) # The two first elements
print("my_list[-2:]:\t", my_list[-2:]) # The last two elements
# In general the syntax for slicing a lists is `my_list[start:end:step]`, where `start` is the first element we want, `end` is the last element (non-inclusive), and `step` is the step size.
# Note that if `start` or `end` is empty, we get from the start or to the end respectively.
# Negative values count from the end, so that -1 is the last element.
#
# If you are completely unfamiliar with this, you are advised to play around with it now, before moving on to `array` slicing.
# We can do the same with arrays, but arrays have even more ways of slicing!
#
# The syntax for slicing in NumPy is exactly the same as for lists, but we can do it for each dimension!
# For a two-dimensional array the syntax then becomes `my_array[start_1:end_1, start_2:end_2]`, where `start_1` and `end_1` is the start and end values for the first axis, and similarly with `start_2` and `end_2` for the second axis.
# ## Final note
# There is much more to be said about NumPy, this was merely the very basics.
#
# NumPy is very well documented and there is much useful information available on the internet.
# See the [official documentation](https://docs.scipy.org/doc/numpy).
#
# Some noteworthy functionality that was omitted in this notebook:
# - `np.loadtxt` and `np.savetxt` for easily saving and reading values from files.
# - The `numpy.linalg` library: useful linear algebra functions, such as solving matrix equations and decomposition.
# - Multidimensional arrays, that are analogous to matrices.
# ## All code
# The entire code in this notebook is available
#
# - In downloadable file: here.
# - In the cell below
# In[13]:
import numpy as np # 'as np' tells python to name NumPy np
# We will use this for demonstration. Plotting is not part of this notebook,
# if you wish to learn more about plotting see our notebook [here].
import matplotlib.pyplot as plt
my_list = [0, 1, 2, 3, 4] # Normal Python list
my_array = np.array([0, 1, 2, 3, 4]) # NumPy array
# Let us see what they look like:
print(my_list)
print(my_array)
# Remember how lists behave when we whish to do mathematical operations on them:
print("my_list*2:\t", my_list*2)
print("my_array*2:\t", my_array*2)
# remember that x**y is python syntax for x^y, that is x to the power of y
print("my_array**2:\t\t", my_array**2)
print("my_array**2 - 3/2:\t", my_array**2 - 3/2)
# We can even do this with functions!
def my_function(x):
y = x + 10*x
return y**2
print("my_function(my_array):\t", my_function(my_array))
def my_function2(x):
y = [] # Creating an empty python list
for i in range(len(x)):
y_element = x[i] + 10*x[i]
y.append(y_element**2)
return y
print("my_function2(my_list):\t", my_function2(my_list))
# Some mathematical functions
x = np.array([-1, -0.5, 0, 0.5, 1])
print("sin(x) =\t", np.sin(x))
print("arccos(x) =\t", np.arccos(x))
print("log(x+10) = \t", np.log(x+10))
print("log10(x+10) =\t", np.log10(x+10))
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
y = np.sin(x) # find sin(x) for each of the elements in x
print("x:\t", x)
print("y:\t", y)
plt.plot(x, y) # Plotting is not part of this notebook, we use it here only for demonstration purposes
plt.show()
# Get an array of 50 values between 0 and 2
x_lin = np.linspace(0, 2, 50)
print(x_lin)
# Get an array of values between 0 and 2 with spacing 0.1
x_range = np.arange(0, 2, 0.1)
print(x_range)
# Generate two lists, one with zeros and one with ones, both with length 20
zeros = np.zeros(20)
ones = np.ones(20)
# Let us see how they look
print(zeros)
print(ones)
my_list = [1, 2, 3, 4] # Normal Python list
# We can access different parts of the list by slicing and indexing:
print("my_list[0]:\t", my_list[0]) # First element
print("my_list[:2]:\t", my_list[:2]) # The two first elements
print("my_list[-2:]:\t", my_list[-2:]) # The last two elements