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 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.

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.

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.

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.

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.

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
```