{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9496520b",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "# A.3 The `numpy` package and arrays"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1877ba84",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "## NumPy\n",
    "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;The core of Python includes the basic features of the language. All other features are included in separate packages (called libraries or toolboxes in other programming languages). The package for creating and manipulating arrays is called `numpy`. The `numpy` package also includes a set of basic mathematical functions (`cos`, `exp`, `log`, `sum`, etc.). Extensive resources on `numpy`’s capabilities are available on the Internet, including the [`numpy` web site](https://numpy.org/doc/stable/reference/). This section focuses on how to use `numpy` arrays of numbers.\n",
    "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Before a package can be used, it needs to be imported. The following line imports the `numpy` package and renames it to `np` (the default short name used in the scientific Python community)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "484bcc1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e5acc4c4",
   "metadata": {},
   "source": [
    "All functions of the `numpy` package can now be called using `np.function_name()`, for example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a2a7b124",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "remove-output"
    ]
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "np.float64(3.1622776601683795)"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "np.sqrt(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f55ae767-4287-42ce-a290-233d84d1121e",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "remove-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3.1622776601683795\n"
     ]
    }
   ],
   "source": [
    "print(np.sqrt(10))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46dc81e4",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Mathematical `numpy` functions work on both real and complex variables. Note, however, that the square root of a negative number returns a `nan` (not a number) and a warning is thrown. The following command\\\n",
    "`np.sqrt(-1)`\\\n",
    "returns\\\n",
    "`nan`\n",
    "plus a nasty looking `RuntimeWarning invalid value encountered in sqrt` (error message not shown here). "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e5f6eaf2",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "A complex variable is returned when a complex variable is passed to the function (even when the imaginary part equals zero)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ad426cde-0872-40d4-b930-0f3ed8dae075",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "remove-output"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1j\n"
     ]
    }
   ],
   "source": [
    "print(np.sqrt(-1 + 0j))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad1ade9b-7626-46d2-b128-ad88ad414366",
   "metadata": {},
   "source": [
    "And for those users that want a complex number returned when taking the square root of a negative number, use the special `np.lib.scimath.sqrt` function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "0976cbc2",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "remove-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3.0 3j\n"
     ]
    }
   ],
   "source": [
    "print(np.lib.scimath.sqrt(9), np.lib.scimath.sqrt(-9))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1730df57",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "## Creating arrays\n",
    "\n",
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;There are multiple ways to create an array. For example, a `list` can be converted to a `numpy` `array` with the `np.array()` command"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "48670d7b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1 2 3 4] has type <class 'numpy.ndarray'>\n"
     ]
    }
   ],
   "source": [
    "a = np.array([1, 2, 3, 4])\n",
    "print(a, 'has type', type(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e773960f",
   "metadata": {},
   "source": [
    "The `type` function shows that `a` is an `ndarray`, which stands for n-dimensional array. All elements within such an array have the same variable type (contrary to a `list`, which can contain items of different types).\n",
    "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Functions that work for individual numbers can also be used on arrays, for example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "21375ed9",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sqrt(a) gives: [1.         1.41421356 1.73205081 2.        ]\n"
     ]
    }
   ],
   "source": [
    "print('sqrt(a) gives:', np.sqrt(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ed1148e",
   "metadata": {},
   "source": [
    "Arithmetic operators can also be used with arrays. The following code defines an array `b` and multiplies the elements of the arrays `a` and `b` element by element"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f5e56632",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a * b gives: [ 2  4  9 12]\n"
     ]
    }
   ],
   "source": [
    "b = np.array([2, 2, 3, 3])\n",
    "print('a * b gives:', a * b)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e99d9f04",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Readers working with matrices may want to compute the dot product of two vectors (arrays), rather than multiplying two vectors term by term. The dot product of arrays `a` and `b` is obtained by using the `@` operator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "de151fc4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "27\n"
     ]
    }
   ],
   "source": [
    "print(a @ b) # Or, alternatively: np.dot(a,b)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c79ecec",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; In addition to converting a list to an array using the `np.array()` function, arrays can be generated with the `np.arange` function (similar to the `range` function used earlier, but now resulting in an array). The `arange(start, end_before, step)` function creates an array starting at `start`, taking steps equal to `step`, and stopping before it reaches the specified `end_before` value. If only a single value is passed to the function, it is interpreted to be the `end_before` value (i.e., the default values for `start` and `step` are 0 and 1, respectively). Using a negative `step` is possible as long as the `start` argument is larger than the `end_before` argument. Note that the default type of `np.arange` is integers unless one of the arguments is a float."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "5efa6a77",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0 1 2]\n",
      "[2 3 4 5]\n",
      "[2 4 6 8]\n",
      "[10  8  6  4  2]\n",
      "[2. 4. 6. 8.]\n"
     ]
    }
   ],
   "source": [
    "print(np.arange(3)) # so start=0 and step=1\n",
    "print(np.arange(2, 6)) # so step=1\n",
    "print(np.arange(2, 10, 2))\n",
    "print(np.arange(10, 0, -2)) # steps can be negative\n",
    "print(np.arange(2, 10, 2.0)) # now returns array of floats"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d8608af",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Instead of specifying a `step`, it is also possible to create an array by specifying the number of elements using the `np.linspace(begin, end, num)` function. The following function call returns seven points with values of equal increment from 1 to 4. Note that, somewhat confusingly, and unlike `range` and `np.arange`, the end value in `linspace` is included as the last element of the array. The default return value of `np.linspace` is an array of floats."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "50c25034",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. ])"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "np.linspace(1, 4, 7)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a51c585",
   "metadata": {},
   "source": [
    "## Two-dimensional arrays\n",
    "\n",
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Arrays may have multiple dimensions. A two-dimensional array can be thought of as a matrix with rows and columns. It can be created by entering a list of rows, where each row is again a list that contains the values for that row. Care must be taken that each of the lists has the same number of elements (equal to the number of columns). When the array is printed to the screen, each row is on a separate line and the columns are separated by a space."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "2ca56e98",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[ 0  1  2  3]\n",
      " [ 4  5  6  7]\n",
      " [ 8  9 10 11]]\n"
     ]
    }
   ],
   "source": [
    "A = np.array([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]])\n",
    "print(A)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03aa07ab",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Arrays filled with zeros or ones may be created with the functions `np.zeros` and `np.ones`, respectively. Each of these functions requires the specification of a shape (e.g., the length of the array, or a tuple with the number of rows and columns for a two-dimensional array). The default return type is `float`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "95634c5c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0. 0. 0. 0. 0. 0. 0.]\n",
      "[[1. 1. 1.]\n",
      " [1. 1. 1.]]\n"
     ]
    }
   ],
   "source": [
    "print(np.zeros(7))\n",
    "print(np.ones((2, 3))) # 2 rows, 3 columns"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82aadc6c",
   "metadata": {},
   "source": [
    "## Slicing arrays\n",
    "\n",
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Just like for a `list`, the individual elements of an array can be accessed by indexing using square brackets (remember that the first index has number 0). The syntax for a onedimensional array `x` is `x`[`start`:`end_before`] or `x`[`start`:`end_before`:`step`]. If the start isn’t specified, 0 will be used. If the step isn’t specified, 1 will be used.\n",
    "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; When programming, the index of the end is generally not known as the array size can vary. To find out how long the array is and access the last value of array `x`, one can type `x[len(x) - 1]`, but that is a bit inconvenient. Luckily, there is a shortcut: `x[-1]` is the same as `x[len(x) - 1]` and represents the last value in the array. Typing `x[-2]` returns the second to last value in the array, etc. The following code examples demonstrate various slicing operations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "a46dace6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[20 21 22 23 24 25 26 27 28 29]\n",
      "20\n",
      "[20 21 22 23 24]\n",
      "[25 26 27 28 29]\n",
      "[23 24 25 26]\n",
      "[22 24 26 28]\n",
      "[29 27 25]\n",
      "[20 23 27]\n"
     ]
    }
   ],
   "source": [
    "x = np.arange(20, 30)\n",
    "print(x)\n",
    "print(x[0])\n",
    "print(x[0:5]) # same as x[:5]\n",
    "print(x[-5:]) # returns the last 5 values\n",
    "print(x[3:7])\n",
    "print(x[2:9:2]) # step is 2\n",
    "print(x[-1:4:-2]) # starts at back, stops before reaching index 4 with step -2\n",
    "print(x[[0, 3, 7]]) # passes indices as a list"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8923a9da",
   "metadata": {},
   "source": [
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The contents of an array can be modified by assigning values to an array slice. In the following example, the first 5 elements of array x are replaced by 40, after which the last value is changed to 40 in a second command."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "919614bb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[20. 20. 20. 20. 20. 20. 20. 20. 20. 20.]\n",
      "[40. 40. 40. 40. 40. 20. 20. 20. 20. 20.]\n",
      "[40. 40. 40. 40. 40. 20. 20. 20. 20. 40.]\n"
     ]
    }
   ],
   "source": [
    "x = 20 * np.ones(10)\n",
    "print(x)\n",
    "x[0:5] = 40\n",
    "print(x)\n",
    "x[-1] = 40 # change last value\n",
    "print(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52b94f6b",
   "metadata": {},
   "source": [
    "### Slicing arrays with conditional operators\n",
    "\n",
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Parts or values of an array may be selected based on conditions. This is called advanced slicing. For example,"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "a6796ffb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0 1 2 3 4]\n",
      "[ True  True  True False False]\n"
     ]
    }
   ],
   "source": [
    "a = np.arange(5)\n",
    "print(a)\n",
    "print(a < 3) # a < 3 returns a Boolean array"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "278c0913",
   "metadata": {},
   "source": [
    "Because `<` is a conditional operator, the statement `a < 3` returns a Boolean array. Instead of printing the outcome to the screen, as in the above example, a more useful application is to store it as a variable. The variable can then be used to slice the array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "ccc0191c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "values in array a less than 3: [0 1 2]\n"
     ]
    }
   ],
   "source": [
    "a_less_than_3 = a < 3\n",
    "print('values in array a less than 3:', a[a_less_than_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fcf7c7c2",
   "metadata": {},
   "source": [
    "The `~` operator swaps the `True` and `False` values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "4a8e6560",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "values in array a not less than 3: [3 4]\n"
     ]
    }
   ],
   "source": [
    "print('values in array a not less than 3:', a[~a_less_than_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f57cbd32",
   "metadata": {},
   "source": [
    "For more compact and readable code, the conditional statement can be specified inside the square brackets. It can even be used to assign a different value to selected elements of an array"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "977ee802",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "new a: [10 10 10  3  4]\n"
     ]
    }
   ],
   "source": [
    "a[a < 3] = 10 # Replace all values less than 3 by 10\n",
    "print('new a:', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d52b8ec7",
   "metadata": {},
   "source": [
    "Conditions for indices can be compounded with either `&` (and) or `|` (or), when the conditions are put between parentheses."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "167cccce",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array: [0 1 2 3 4 5 6 7 8 9]\n",
      "less than 3 or larger than 7: [0 1 2 8 9]\n",
      "larger than 3 and less than 7: [4 5 6]\n"
     ]
    }
   ],
   "source": [
    "a = np.arange(10)\n",
    "print('array:', a)\n",
    "print('less than 3 or larger than 7:', a[(a < 3) | (a > 7)])\n",
    "print('larger than 3 and less than 7:', a[(a > 3) & (a < 7)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "170a052d",
   "metadata": {},
   "source": [
    "## Manipulating arrays\n",
    "\n",
    "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Arrays can be stacked next to each other (horizontally) using the `np.hstack` command and above each other (vertically) using the `np.vstack` function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "18a23f6f",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0 1 2 0 1 2]\n",
      "[[0 1 2]\n",
      " [0 1 2]]\n"
     ]
    }
   ],
   "source": [
    "a = np.arange(3)\n",
    "print(np.hstack((a, a)))\n",
    "print(np.vstack((a, a)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "53b85b82",
   "metadata": {},
   "source": [
    "Arrays can be combined to form larger arrays in this way, but it is not very efficient computationally, so programs may run slowly when large arrays are stacked or arrays are stacked multiple times, for example in a loop. It is better to create an array of the correct size first and then fill it with a `for` loop (this may be difficult when the size of the array is not known upfront). Note the use of `enumerate` in the code cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "c7560f2c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0.00000000e+00 8.66025404e-01 8.66025404e-01 1.22464680e-16]\n"
     ]
    }
   ],
   "source": [
    "x = np.linspace(0, np.pi, 4)\n",
    "y = np.empty(len(x))\n",
    "for i, xx in enumerate(x):\n",
    "    y[i] = np.sin(xx)\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4bd45982",
   "metadata": {},
   "source": [
    "The code cell above is a demonstration of creating an empty array and filling it with a `for` loop. For this specific example, `y = sin(x)` is, of course, much easier and faster."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
