{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "mqAuLRBIqj3Z"
   },
   "source": [
    "# Lab 1: Introduction to NumPy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Replace this cell with names of both partners."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "mqAuLRBIqj3Z"
   },
   "source": [
    "### Overview\n",
    "In this lab, you will learn to use NumPy, a Python library for working with arrays. Hint: if you find yourself stuck, reviewing the documentation can be useful https://numpy.org/doc/stable/user/."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "uI2t6f6Dq06s"
   },
   "source": [
    "## Part I: NumPy array (vector) indexing and calculations\n",
    "\n",
    "Let $\\textbf{x}$ be a random 1D array of integers with 50 elements distributed between a lower bound of 5 and upper bound of 313. Follow the prompts in the text cells to complete the code cells below. Note: for this exercise and throughout the quarter, the first element of an array/vector is always at index 0 when coding in Python.    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Ored7fdgq-DA"
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "# Set random seed so that correct results are deterministic\n",
    "np.random.seed(10)\n",
    "# Get a random vector to play with\n",
    "x = np.random.randint(low=5, high=313, size=(50,))\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "yFjvAVaXsjbO"
   },
   "source": [
    "Select the ***first*** 15 elements of $\\textbf{x}$ in an array $\\textbf{y}$ using slice indexing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "xgHbY69Yr0dL"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "gUoNbqNRtk6z"
   },
   "source": [
    "Select the ***last*** 15 elements of $\\textbf{x}$ in an array $\\textbf{z}$ using slice indexing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "zhiC5EeJtw1R"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "3x3IxBert1w2"
   },
   "source": [
    "Store the elementwise sum of $\\textbf{y}$ and $\\textbf{z}$ in an array named $\\texttt{sumyz}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "QNt_qWf-mQhl"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "XWpXru3RuR_G"
   },
   "source": [
    "Store the elementwise difference of $\\textbf{z}$ subtracted from $\\textbf{y}$ in an array named $\\texttt{yminusz}$.\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "IudcoYu0uYYO"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "eJi2UOVBjx2-"
   },
   "source": [
    "Confirm that $\\texttt{sumyz} + \\texttt{yminusz}  = \\textbf{y} + \\textbf{y}$ using the $\\texttt{==}$ comparison operator. Store the resulting boolean matrix in an array called $\\texttt{check}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "9PEKPQ8eloWD"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "czTeT6Gkuy-_"
   },
   "source": [
    "Store the elementwise product of $\\textbf{y}$ and $\\textbf{z}$ in an array named $\\texttt{mulyz}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "4HVWcoaFvJxO"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Ly_i-1EivRHB"
   },
   "source": [
    "Store the 17th element of $\\textbf{x}$ in a variable $\\texttt{seventeen}$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "NnGU6Cm3v_KI"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "KhhvANmEwCSK"
   },
   "source": [
    "NumPy slicing can also be used to select a range of elements in the middle of an array. For $i < j$, to select elements [$i, i+1, i+2, ..., j-2, j-1$] we use the syntax:\n",
    "\n",
    "```\n",
    "x[i:j]\n",
    "```\n",
    "This syntax generalizes to matrices and general $n$-dimensional arrays.\n",
    "\n",
    "By default, NumPy slicing will select a consecutive series of elements. In addition, slicing can be used to select every 2nd, or 3rd, or in general every $n$-th element of an array. To select every other element of an array we use the syntax:\n",
    "\n",
    "```\n",
    "x[::2]\n",
    "```\n",
    "\n",
    "To select every 3rd element of an array we use the syntax:\n",
    "\n",
    "```\n",
    "x[::3]\n",
    "```\n",
    "\n",
    "In general, to select every $n$-th element of an array we use the syntax:\n",
    "\n",
    "```\n",
    "x[::n]\n",
    "```\n",
    "\n",
    "To select every $n$-th element of an array beginning at index $i$ and up to but not including index $j$ we use the syntax:\n",
    "\n",
    "```\n",
    "x[i:j:n]\n",
    "```\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "mG8OAxZ802Nc"
   },
   "source": [
    "Store the 3rd through the 11th element of $\\textbf{z}$ in an array $\\texttt{z311}$. Recall that since Python starts counting at 0, the 3rd element is at index 2 and the 11th element is at index 10. $\\texttt{z311}$ should have 9 elements which you can check with the $\\texttt{shape}$ method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "y1oAfBxl1YIU"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "mcGTafZf2WJt"
   },
   "source": [
    "Store every 7th element of $\\textbf{x}$ in an array $\\texttt{x7}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "HRd1Cjxt6iAS"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "q9aVP9fl0M3V"
   },
   "source": [
    "Store all elements of $\\textbf{x}$ less that 60 using ***boolean indexing*** in an array $\\texttt{under60}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "XKN755rqzJLr"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "_jT8YsUD1Z-c"
   },
   "source": [
    "Store the mean of $\\textbf{x}$ in a variable $\\texttt{xmean}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "YO9zgAX1107Y"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Qce_-xji112b"
   },
   "source": [
    "Store the minimum value of $\\textbf{x}$ in a variable $\\textbf{xmin}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "jafY13Eo17Iq"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "zEa8zjUn179n"
   },
   "source": [
    "Store the maximum value of $\\textbf{x}$ in a variable $\\texttt{xmax}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "G-dyVeoL2CZb"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "uWKz1PDv3IbP"
   },
   "source": [
    "Store the 1st, 28th, 2nd, and 19th elements (in that order) of $\\textbf{x}$ in\n",
    "an array $\\texttt{crazyx}$, using advanced indexing with a vector of integers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "8a0FaRs7qAJv"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "tiXNI96i21G2"
   },
   "source": [
    "## Part II: Matrix indexing\n",
    "\n",
    "MNIST (Modified National Institute of Standards and Technology) dataset is a collection of 70,000 28x28 pixel grayscale images of handwritten digits (0-9), with each pixel corresponding to an integer between 0 (black) and 255 (white). The MNIST test dataset consists of 10,000 such images. In the following exercises, we will load the MNIST test dataset into a NumPy ndarray and practice NumPy indexing and mathematical operations.\n",
    "\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "kjZ2xFno3v4j"
   },
   "outputs": [],
   "source": [
    "# Load the mnist test dataset from disc\n",
    "mnist = np.loadtxt('/cluster/academic/DATA311/202620/mnist_test.csv', delimiter=',')\n",
    "print(mnist.shape)\n",
    "print(mnist.dtype)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "TJx0XLvE-JnZ"
   },
   "source": [
    "Notice that the shape of the dataset isn't exactly what one would expect. Each image is 28 * 28 = 784 total pixels. Historically, when compute capability was more limited, these images were stored with compression algorithms that worked much better on the \"flattened\" images, i.e., the 28 rows (each 28 elements long) of the image matrices were concatenated into 784 element vectors. The first column of this matrix of flattened images corresponds to the digit (0-9) which was drawn."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "m3Ve9U1K-Ed2"
   },
   "outputs": [],
   "source": [
    "# The digits corresponding to the drawn images\n",
    "mnist[:, 0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "tLyxmdbQABgF"
   },
   "outputs": [],
   "source": [
    "# Let's remove the digit labels. But we don't want to throw away the labels so we'll save them.\n",
    "mnist_labels, mnist = mnist[:, 0], mnist[:, 1:]\n",
    "mnist.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "qOvgGTUEArvs"
   },
   "source": [
    "NumPy has an easy way to reconfigure data using the $\\texttt{reshape}$ method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "7eTYi6xzAJfb"
   },
   "outputs": [],
   "source": [
    "mnist_flat, mnist = mnist, mnist.reshape(10000, 28, 28)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "8rji7ZAZATsq"
   },
   "outputs": [],
   "source": [
    "# Reshape only works when you propose a shape that has the same number of elements as the original n-way array.\n",
    "# You can confirm this is the case here by taking the product of each shape tuple.\n",
    "mnist_flat.shape, mnist.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "xNdra3CvCF18"
   },
   "source": [
    "Store the first image from the dataset in a matrix $\\texttt{im1}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "4L_eYQpvCg34"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "6xwgo_hKBiTC"
   },
   "source": [
    "Matplotlib has an easy method to view image matrices rendered as images using the $\\texttt{imshow}$ method. You should see a picture of a 7 when you run the following code cell. If not, you should go back and check your work before continuing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "ZnrFtnkcAckd"
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "plt.imshow(im1, cmap='gray')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "NVL26wfGCzAg"
   },
   "source": [
    "Calculate the number of pixels in $\\texttt{im1}$ which are not black (i.e. have ink) using the comparison operator **>**, and the NumPy $\\texttt{sum}$ method. The sum method will count True as 1 and False as 0 when dealing with boolean values. Now calculate the proportion of pixels that have ink and store in the variable $\\texttt{ink}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "_Ohx0cbaEmSl"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "cxYfZifaFvs8"
   },
   "source": [
    "From the high proportion of ink to blank space, this digit was likely drawn with a sharpie or some other kind of marker. See what kind of interesting insights data science can provide!\n",
    "\n",
    "However, this image doesn't look quite right. The ink should be black, and the background white. This is how they were originally drawn prior to the dataset processing.\n",
    "\n",
    "Create a matrix $\\texttt{invert}$ where each element is equal to 255 minus the corresponding element in $\\texttt{im1}$. So for instance, if the pixel value in  $\\texttt{im1}$ is equal to zero, the corresponding element in $\\texttt{invert}$ will be equal to 255.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "PnXNtMghEuqJ"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "RHR2AsSLIzY0"
   },
   "source": [
    "Display $\\texttt{invert}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "5lwDGcGqI5gO"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Mx3E2tnLI7E4"
   },
   "source": [
    "Create the matrix $\\texttt{ident}$ which is the sum of $\\texttt{invert}$ and $\\texttt{im1}$. Display $\\texttt{ident}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "LMMnjJ8oJYwS"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "936t73EAKEZB"
   },
   "source": [
    "Using the $\\texttt{mnist\\_labels}$ vector, the **==** operator, and boolean indexing, create a 3-way array $\\texttt{mnist0}$ with all the images of the digit 0 from the $\\texttt{mnist}$ 3-way array. Display the last image in $\\texttt{mnist0}$ as a sanity check that you did the indexing  correctly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "6zlpqTEcJt52"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Bg7n8qdFMLEO"
   },
   "source": [
    "Using the NumPy $\\texttt{mean}$ method and its $\\texttt{axis}$ argument, create a 2-way array $\\texttt{mishmash}$ which is the elementwise average of all the images in $\\texttt{mnist0}$.\n",
    "Display $\\texttt{mishmash}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Bxi-pEkDM7uc"
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Z9cWTjJAtQ7A"
   },
   "source": [
    "Now that's a pretty nice looking zero!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part III: Simulating Slit Scan and Time Slice Photography "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Color images can be represented by 3D `ndarray`s with shape (`rows`, `columns`, `channels`), where `channels` is typically size 3, representing red, green, and blue values of each pixel. A **video** can be thought of as a stack of images: a 4D array, sometimes represented as an array with shape (`frames`, `rows`, `columns`, `channels`)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can imagine a video as being a cube-like object, with rows and columns as two dimensions and time (frames) as the third. The channel dimension just comes along for the ride - when displayed on your screen, those three values get stuck together into a single pixel. If you imagine taking a two-dimensional slice of the \"video cube\" in the plane of the `rows` and `columns` axis at some fixed value of the `frames` dimension, you'll just end up with a single frame of the video. But we can slice the cube in other directions as well, and this can result in some pretty interesting images! In this task, you'll experiment with \"slicing\" a video cube in a couple of non-traditional ways."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's some code that loads a list of URLs, one per image. These are just the frames of a video broken out into individual images to make them easier to work with."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import imageio.v3 as imageio\n",
    "plaza_urls = np.genfromtxt(\"https://facultyweb.cs.wwu.edu/~wehrwes/courses/data311_26s/data/plaza_urls.txt\", dtype=str).tolist()\n",
    "plaza_urls[:5] # show just the first 5 to see what they look like"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's what the first frame looks like:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.imshow(imageio.imread(plaza_urls[0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.0: Create a Video Cube\n",
    "\n",
    "First, write code below to load the frames into a single `ndarray`. Different approaches can work - here are a couple suggestions:\n",
    "\n",
    "* Load all the images into a python list, then call `np.array` on that list; this will concatenate the list of `(height, width, 3)` images into a `(frames, height, width, 3)` array.\n",
    "* Preallocate a `(frames, height, width, 3)` array, then read each image in assigning it to a slice of the array, as in `cube[i,:,:,:] = frame_i`.\n",
    "\n",
    "*Note*: loading all the images is not a super quick operation, and it also uses significant network bandwidth. Please avoid re-running the image loading cell above more than needed - once it's working, you should need to run this cell only once per work session. Also, when you're testing and developing, you may want to try working on a small subset of the images (e.g., the first 5 frames) so you can try stuff out more quickly."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.1: Slicing Directly Across Time\n",
    "\n",
    "To get a frame, you can fix the frame dimension and take all rows and all columns. What if instead, we fix the **column** dimension and take all rows and all frames? If we do this, we get an image that's similar to a \"slit scan\" photograph ([wikipedia](https://en.wikipedia.org/wiki/Slit-scan_photography), [examples](https://www.google.com/search?q=slit+scan+photography&client=firefox-b-1-d&source=lnms&tbm=isch&sa=X&ved=2ahUKEwiLio-L-szzAhVHFjQIHYKCAAsQ_AUoAXoECAEQAw&biw=1441&bih=924&dpr=1)). \n",
    "\n",
    "Your task is to play with creating \"time slice photographs\" that fix the image column (or row) and produce an image where one of the dimensions represents time. Here's an example output that I made from the given image set:\n",
    "\n",
    "![](https://facultyweb.cs.wwu.edu/~wehrwes/courses/data311_25f/lab1/timeslice_example.png)\n",
    "\n",
    "Write code to compute a time slice image like the above. Play around and see what you can create! The only requirement is that one dimension must represent time and another must represent a *fixed* row or column of the image's spatial dimensions. After slicing out your image, call `plt.imshow` to display your result. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2: Slicing Diagonally Across Time and Space\n",
    "\n",
    "Above, we took a slice straight along the time dimension, orthogonal to the (`rows`, `columns`) plane. But there's no reason we have to stick to that! If we slice the space-time cube diagonally, we can mix change over time and change across the image's spatial dimensions into a single image. Here's an example I created using a timelapse of the New York City skyline (you can see the original video [here](https://www.youtube.com/watch?v=tQBYm7_1hqs)):\n",
    "\n",
    "![](https://facultyweb.cs.wwu.edu/~wehrwes/courses/data311_25f/lab1/diagonal_example.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's load up the URLs for this video, which is a timelapse of the NYC skyline:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ny_urls = np.genfromtxt(\"https://facultyweb.cs.wwu.edu/~wehrwes/courses/data311_26s/data/ny_urls.txt\", dtype=str).tolist()\n",
    "plt.imshow(imageio.imread(ny_urls[100])) # the video fades in from black, so the first frame is boring"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Write code to compute a \"diagonal\" time slice image like the example above. Again, you can play around with the specifics and get creative, but for this result please make sure that your slice is **not** parallel to any of the video cube's axis-aligned planes.\n",
    "\n",
    "*Note*: as in Part 3.1, loading all the images is not a super quick operation. **Make sure you're loading the images in a separate code cell, and try to avoid loading the images more than once during a single session of work on this lab.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# load the NYC dataset into a video cube"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note:** while this kind of diagonal slicing is technically possible with a one-liner using boolean indexing, it is much more naturally implemented with a (single) loop."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# extract and display your \"diagonal\" time slice image"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "4-CQb1jS7QBi"
   },
   "source": [
    "## Submission\n",
    "\n",
    "Make sure the output is shown for all cells, then download your completed notebook as a .ipynb file and submit it to the Lab 1 assignment on Canvas.\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "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.12.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
