From 6ed089daf30bea4ca41cf6f4aa2c24308d4ad4a2 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 10 Aug 2023 17:12:39 +0000 Subject: [PATCH 1/7] Restructure sections in functions lesson --- lessons/python/6_functions.ipynb | 721 +++++++++++-------------------- 1 file changed, 260 insertions(+), 461 deletions(-) diff --git a/lessons/python/6_functions.ipynb b/lessons/python/6_functions.ipynb index a64deda..5917fd1 100644 --- a/lessons/python/6_functions.ipynb +++ b/lessons/python/6_functions.ipynb @@ -13,766 +13,565 @@ "source": [ "# Functions\n", "\n", - "At this point,\n", - "we've written code to draw some interesting features in our topographical data,\n", - "loop over all our data files to quickly draw these plots for each of them,\n", - "and have Python make decisions based on what it sees in our data.\n", - "But, our code is getting pretty long and complicated;\n", - "what if we had thousands of datasets,\n", - "and didn't want to generate a figure for every single one?\n", - "Commenting out the figure-drawing code is a nuisance.\n", - "Also, what if we want to use that code again,\n", - "on a different dataset or at a different point in our program?\n", - "Cutting and pasting it is going to make our code get very long and very repetitive,\n", - "very quickly.\n", - "We'd like a way to assemble our code so that it is easier to reuse,\n", - "and Python provides for this by letting us define things called *functions* -\n", - "a shorthand way of re-executing longer pieces of code.\n", - "\n", - "A *function* groups code into a program that can be called as a unit." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's start by defining a function `fahr_to_cel` that converts temperatures from Fahrenheit to degrees. To convert temperatures in degrees Fahrenheit to Celsius, subtract 32 and multiply by .5556 (or 5/9)." + "In the [diffusion](8_diffusion.ipynb) and [advection](9_advection.ipynb) notebooks,\n", + "we wrote code\n", + "to solve the one-dimensional diffusion and advection equations numerically,\n", + "evolve the solutions with time,\n", + "and visualize the results.\n", + "\n", + "However, the code in these notebooks is long and complicated and frequently repetitive.\n", + "What if we wanted to use the code again,\n", + "with different parameters or perhaps even in a different notebook?\n", + "Cutting and pasting is tedious, and it can easily lead to errors.\n", + "\n", + "We'd like a way to organize our code so that it's easier to reuse.\n", + "Python provides for this by letting us define *functions*.\n", + "A function groups code into a program that can be called as a unit.\n", + "\n", + "Before we start,\n", + "we'll need Numpy and a NumPy setting for the code in this notebook." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def fahr_to_cel(temp):\n", - " temp_new = (temp - 32) * 5 / 9\n", - " return temp_new" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The function definition opens with the `def` statement,\n", - "followed by the name of the function\n", - "and a list of parameters enclosed within parentheses,\n", - "ending with a colon `:`.\n", - "The body of the function --- the\n", - "statements that are executed when it runs --- is indented below the definition line.\n", - "\n", - "When we call the function,\n", - "the values we pass to it are assigned to those variables\n", - "so that we can use them inside the function.\n", - "Inside the function,\n", - "we use a [return statement](reference.html#return-statement) to send a result back to whoever asked for it.\n", + "import numpy as np\n", "\n", - "Let's try running our function.\n", - "Calling our own function is no different from calling any other function:" + "np.set_printoptions(precision=1, floatmode=\"fixed\")" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "fahr_to_cel(100)" + "## Definition" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We've successfully called the function that we defined,\n", - "and we have access to the value that we returned.\n", + "In the diffusion notebook,\n", + "we defined a timestep based on a stability criterion\n", + "for our numerical solution to the diffusion equation.\n", "\n", - "## Composing Functions\n", - "\n", - "Now that we've seen how to turn Fahrenheit into Celsius,\n", - "it's easy to turn Celsius into Kelvin (+ 273.15):" + "Let's group this code into a function." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def cel_2_kel(temp):\n", - " return temp + 273.15" + "def calculate_time_step(grid_spacing, diffusivity):\n", + " return grid_spacing ** 2 / diffusivity / 2.1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "What about converting Fahrenheit to Kelvin?\n", - "We could write out the formula,but we don't need to.\n", - "Instead,we can [compose](reference.html#compose) the two functions we have already created:" + "A function definition begins with the keyword `def`,\n", + "followed by the name of the function,\n", + "followed by a comma-delimited listing of arguments (also known as parameters in parentheses,\n", + "and ending with a colon `:`.\n", + "The code in the body of the function--run when the function is called--must be indented." ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "def fahr_to_kel(temp):\n", - " return cel_2_kel(fahr_to_cel(temp))\n", - "\n", - "\n", - "print(\"freezing point of water in Kelvin:\", fahr_to_kel(32.0))" + "We've named our function `calculate_time_step` (naming functions is often an art).\n", + "It takes two arguments,\n", + "the grid spacing of the model and the diffusivity.\n", + "The variables `grid_spacing` and `diffusivity` are *local* to the function--they don't exist outside of the body of the function.\n", + "In the body of the function,\n", + "the time step is calculated from the stability criterion\n", + "and returned to the caller." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This is our first taste of how larger programs are built:\n", - "we define basic operations,\n", - "then combine them in ever-larger chunks to get the effect we want.\n", - "Real-life functions will usually be larger than the ones shown here --- typically half a dozen to a few dozen lines --- but\n", - "they shouldn't ever be much longer than that,\n", - "or the next person who reads it won't be able to understand what's going on.\n", + "## Execution\n", "\n", - "## Tidying up\n", - "\n", - "Now that we know how to wrap bits of code up in functions,\n", - "we can make our topo analysis easier to read and easier to reuse.\n", - "First, let's make an `analyze` function that generates our plots:" + "Call the `calculate_time_step` function with a grid spacing `dx` of $10.0~m$ and a diffusivity `D` of $0.1~m^2 s^{-1}$." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def analyze(filename):\n", - " data = numpy.loadtxt(fname=filename, delimiter=\",\")\n", - "\n", - " fig = matplotlib.pyplot.figure(figsize=(10.0, 3.0))\n", - "\n", - " axes1 = fig.add_subplot(1, 3, 1)\n", - " axes2 = fig.add_subplot(1, 3, 2)\n", - " axes3 = fig.add_subplot(1, 3, 3)\n", - "\n", - " axes1.set_ylabel(\"average\")\n", - " axes1.plot(data.mean(axis=0))\n", - "\n", - " axes2.set_ylabel(\"max\")\n", - " axes2.plot(data.max(axis=0))\n", - "\n", - " axes3.set_ylabel(\"min\")\n", - " axes3.plot(data.min(axis=0))\n", - "\n", - " fig.tight_layout()\n", - " matplotlib.pyplot.show()" + "dx = 10.0\n", + "D = 0.1\n", + "dt = calculate_time_step(dx, D)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Import required packages" + "Note that we passed the arguments to the function in the order it expects:\n", + "first the grid spacing, then the diffusivity.\n", + "Calling a function we define is no different than calling any other Python function.\n", + "\n", + "Print the result." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "import matplotlib.pyplot\n", - "import numpy" + "print(f\"Time step = {dt:.2f} s\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Try it: " + "In Python,\n", + "we can also pass arguments by name." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "filename = \"data/topo.asc\"\n", - "analyze(filename)" + "dt1 = calculate_time_step(grid_spacing=dx, diffusivity=D)\n", + "dt1 == dt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Testing and Documenting\n", + "This is useful because it makes the function call more readable.\n", "\n", - "Once we start putting things in functions so that we can re-use them,\n", - "we need to start testing that those functions are working correctly.\n", - "To see how to do this,let's write a function to center a dataset around the mean of that dataset (or in other words, that the mean becomes 0 in the centered datasets, values smaller than the mean become negative, values greater than the mean become positive :" + "Further,\n", + "when passing arguments by name,\n", + "we can change the order of the arguments." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def center(data):\n", - " new_data = data - data.mean()\n", - " return new_data" + "dt2 = calculate_time_step(diffusivity=D, grid_spacing=dx)\n", + "dt2 == dt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We could test this on our actual data,\n", - "but since we don't know what the values ought to be,\n", - "it will be hard to tell if the result was correct.\n", - "Instead,let's use NumPy to create a matrix of 1's\n", - "and then center that around its mean:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "z = numpy.ones((5, 5))\n", - "print(z)\n", - "print(center(z))" + "This is useful because it makes the function easier to call--you don;t have to remember the argument order.\n", + "\n", + "These techniques can be used with any Python function, whether it's made by us or by someone else." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, let's write a function to center a dataset around any particular value :" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def center(data, desired):\n", - " return (data - data.mean()) + desired" + "## Extension\n", + "\n", + "Python functions have many interesting features,\n", + "more than we can address here.\n", + "We'll focus on a few,\n", + "and provide a list of additional resources in the summary. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Test " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "z = numpy.zeros((2, 2))\n", - "print(z)\n", - "print(center(z, 3))" + "### Default arguments" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That looks right,\n", - "so let's try `center` on our real data:" + "It's often useful to define default values for the arguments in a function.\n", + "\n", + "Let's create another function from a piece of repeated code in the diffusion notebook.\n", + "This one sets the initial profile of the diffused quantity\n", + "(e.g., temperature, aerosol concentration, sediment, etc.)." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "filename = \"data/topo.asc\"\n", - "\n", - "data = numpy.loadtxt(fname=filename, delimiter=\",\")\n", - "print(center(data, 0))\n", - "import matplotlib.pyplot as plt\n", - "\n", - "plt.imshow(center(data, -200))\n", - "plt.colorbar()" + "def set_initial_profile(domain_size=100, boundary_left=500, boundary_right=0):\n", + " concentration = np.empty(domain_size)\n", + " concentration[: int(domain_size / 2)] = boundary_left\n", + " concentration[int(domain_size / 2) :] = boundary_right\n", + " return concentration" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It's hard to tell from the default output whether the result is correct,\n", - "but there are a few simple tests that will reassure us:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"original min, mean, and max are:\", data.min(), data.mean(), data.max())\n", - "centered = center(data, 0)\n", - "print(\n", - " \"min, mean, and and max of centered data are:\",\n", - " centered.min(),\n", - " centered.mean(),\n", - " centered.max(),\n", - ")" + "Note that each of the arguments is assigned a default value.\n", + "If any argument is omitted from a call to this function,\n", + "its default value is used instead." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That seems almost right:\n", - "the original mean was about ca. 3153.6,\n", - "so the lower bound from zero is now about 3153.6-2565.0293.\n", - "The mean of the centered data isn't quite zero --- we'll explore why not in the challenges --- but it's pretty close.\n", - "We can even go further and check that the standard deviation hasn't changed:" + "Call `set_initial_profile` with a domain size `Lx` of $10~m$." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "print(\"std dev before and after:\", data.std(), centered.std())" + "Lx = 10\n", + "C = set_initial_profile(Lx)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Those values look the same,\n", - "but we probably wouldn't notice if they were different in the sixth decimal place.\n", - "Let's do this instead:" + "Although we omitted the left and right boundary condition values,\n", + "the function call didn't produce an error.\n", + "\n", + "Check the result by printing the returned concentration `C`." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "print(\n", - " \"difference in standard deviations before and after:\", data.std() - centered.std()\n", - ")" + "print(C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Again,\n", - "the difference is very small.\n", - "It's still possible that our function is wrong,\n", - "but it seems unlikely enough that we should probably get back to doing our analysis.\n", - "We have one more task first, though:\n", - "we should write some [documentation](reference.html#documentation) for our function\n", - "to remind ourselves later what it's for and how to use it.\n", + "The default values for the left and right boundary conditions were applied.\n", "\n", - "The usual way to put documentation in software is to add [comments](reference.html#comment) like this:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# center(data, desired): return a new array containing the original data centered around the desired value.\n", - "def center(data, desired):\n", - " return (data - data.mean()) + desired" + "Using default values makes calling a function easier." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "There's a better way, though.\n", - "If the first thing in a function is a string that isn't assigned to a variable,\n", - "that string is attached to the function as its documentation:" + "### Type hints\n", + "\n", + "Let's group some more repeated code in the diffusion notebook into a function.\n", + "This is the solver we used for the one-dimensional diffusion equation." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def center(data, desired):\n", - " \"\"\"Return a new array containing the original data centered around the desired value.\"\"\"\n", - " return (data - data.mean()) + desired" + "def solve1d(concentration, grid_spacing=1.0, time_step=1.0, diffusivity=1.0):\n", + " flux = -diffusivity * np.diff(concentration) / grid_spacing\n", + " concentration[1:-1] -= time_step * np.diff(flux) / grid_spacing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This is better because we can now ask Python's built-in help system to show us the documentation for the function:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "help(center)" + "The arguments for the grid spacing, time step, and diffusivity take default values,\n", + "but the `concenctration` argument does not.\n", + "\n", + "**Question:** Without looking at the body of the function,\n", + "can you tell what sort of variable goes into the `concentration` argument?\n", + "A float? A string? A NumPy array?\n", + "\n", + "\n", + "\n", + "Python is dynamically typed.\n", + "\n", + "What type should a parameter be? Integer, float, string, NumPy array?!\n", + "It's hard to tell.\n", + "\n", + "This is where *type hints* can be handy.\n", + "\n", + "Do I want to talk about pass by reference?\n", + "\n", + "How do we know what type/size of variables the function expects from its arguments?\n", + "This is where type hints can help.\n", + "\n", + "Redo the function with type hints." ] }, { "cell_type": "code", "execution_count": null, "metadata": { - "attributes": { - "classes": [ - "output" - ], - "id": "" - } + "tags": [] }, "outputs": [], "source": [ - "?center" + "def solve1d(concentration: np.ndarray, grid_spacing: float = 1.0, time_step: float = 1.0, diffusivity: float = 1.0) -> None:\n", + " flux = -diffusivity * np.diff(concentration) / grid_spacing\n", + " concentration[1:-1] -= time_step * np.diff(flux) / grid_spacing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "A string like this is called a [docstring](reference.html#docstring).\n", - "We don't need to use triple quotes when we write one,\n", - "but if we do,\n", - "we can break the string across multiple lines:" + "Type hints are optional, but useful.\n", + "\n", + "Type hints are not enforced." ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "def center(data, desired):\n", - " \"\"\"Return a new array containing the original data centered around the desired value.\n", - " Example: center([1, 2, 3], 0) => [-1, 0, 1]\"\"\"\n", - " return (data - data.mean()) + desired\n", - "\n", - "\n", - "help(center)" + "### Docstrings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Defining Defaults\n", - "\n", - "We have passed parameters to functions in two ways:\n", - "directly, as in `type(data)`,\n", - "and by name, as in `numpy.loadtxt(fname='something.csv', delimiter=',')`.\n", - "In fact,\n", - "we can pass the filename to `loadtxt` without the `fname=`:" + "Add documentation string (docstring) to `solve1d`." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "numpy.loadtxt(\"data/topo.asc\", delimiter=\",\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "but we still need to say `delimiter=`:\n", + "def solve1d(concentration: np.ndarray, grid_spacing: float = 1.0, time_step: float = 1.0, diffusivity: float = 1.0) -> None:\n", + " \"\"\"Solve the one-dimensional diffusion equation with fixed boundary conditions.\n", + "\n", + " Parameters\n", + " ----------\n", + " concentration : ndarray\n", + " The quantity being diffused.\n", + " grid_spacing : float (optional)\n", + " Distance between grid nodes.\n", + " time_step : float (optional)\n", + " Time step.\n", + " diffusivity : float (optional)\n", + " Diffusivity.\n", + "\n", + " Returns\n", + " -------\n", + " result : ndarray\n", + " The temperatures after time *time_step*.\n", "\n", - "What happens if you enter the following statement? \n", - " ~~~ {.python}\n", - " numpy.loadtxt('data/topo.asc', ',')\n", - " ~~~" + " Examples\n", + " --------\n", + " >>> import numpy as np\n", + " >>> from solver import solve1d\n", + " >>> z = np.zeros(5)\n", + " >>> z[2] = 5\n", + " >>> solve1d(z, diffusivity=0.25)\n", + " array([ 0.0, 1.2, 2.5, 1.2, 0.0])\n", + " \"\"\"\n", + " flux = -diffusivity * np.diff(concentration) / grid_spacing\n", + " concentration[1:-1] -= time_step * np.diff(flux) / grid_spacing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To understand what's going on,\n", - "and make our own functions easier to use,\n", - "let's re-define our `center` function like this:" + "Use the `help` function." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def center(data, desired=0.0):\n", - " \"\"\"Return a new array containing the original data centered around the desired value (0 by default).\n", - " Example: center([1, 2, 3], 0) => [-1, 0, 1]\"\"\"\n", - " return (data - data.mean()) + desired" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The key change is that the second parameter is now written `desired=0.0` instead of just `desired`.\n", - "If we call the function with two arguments,\n", - "it works as it did before:" + "help(solve1d)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "test_data = numpy.zeros((2, 2))\n", - "print(center(test_data, 3))" + "?solve1d" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "But we can also now call it with just one parameter,\n", - "in which case `desired` is automatically assigned the [default value](reference.html#default-value) of 0.0:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "more_data = 5 + numpy.zeros((2, 2))\n", - "print(\"data before centering:\")\n", - "print(more_data)\n", - "print(\"centered data:\")\n", - "print(center(more_data))" + "Docstring aren't necessary, but they're a good practice.\n", + "\n", + "Documentation systems such as sphinx (link) use information from docstrings to produce documentation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This is handy:\n", - "if we usually want a function to work one way,\n", - "but occasionally need it to do something else,\n", - "we can allow people to pass a parameter when they need to\n", - "but provide a default to make the normal case easier.\n", - "The example below shows how Python matches values to parameters:" + "## Composition\n", + "\n", + "Put the functions together.\n", + "*Compose* the functions." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def display(a=1, b=2, c=3):\n", - " print(\"a:\", a, \"b:\", b, \"c:\", c)\n", + "def example():\n", + " \"\"\"An example of running `solve1d`.\"\"\"\n", + " print(example.__doc__)\n", + " D = 100 # diffusivity\n", + " Lx = 10 # domain length\n", + " dx = 0.5 # grid spacing\n", "\n", + " dt = calculate_time_step(dx, D)\n", + " C = set_initial_profile(Lx)\n", "\n", - "print(\"no parameters:\")\n", - "display()\n", - "print(\"one parameter:\")\n", - "display(55)\n", - "print(\"two parameters:\")\n", - "display(55, 66)" + " print(\"Time = 0\\n\", C)\n", + " for t in range(1, 5):\n", + " solve1d(C, dx, dt, D)\n", + " print(f\"Time = {t*dt:.4f}\\n\", C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "As this example shows,\n", - "parameters are matched up from left to right,\n", - "and any that haven't been given a value explicitly get their default value.\n", - "We can override this behavior by naming the value as we pass it in:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"only setting the value of c\")\n", - "display(c=77)" + "This is our first taste of how larger programs are built:\n", + "we define basic operations,\n", + "then combine them in ever-larger chunks to get the effect we want.\n", + "Real-life functions will usually be larger than the ones shown here --- typically half a dozen to a few dozen lines --- but\n", + "they shouldn't ever be much longer than that,\n", + "or the next person who reads it won't be able to understand what's going on." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "With that in hand,\n", - "let's look at the help for `numpy.loadtxt`:" + "Run the example `example`." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "help(numpy.loadtxt)" + "example()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "There's a lot of information here,\n", - "but the most important part is the first couple of lines:\n", + "## Exercises\n", "\n", - "~~~ {.output}\n", - "loadtxt(fname, dtype=, comments='#', delimiter=None, converters=None, skiprows=0, usecols=None,\n", - " unpack=False, ndmin=0)\n", - "~~~\n", + "1. \"Adding\" two strings produces their concatenation: `'a' + 'b'` is `'ab'`. Write a function called `fence` that takes two parameters, `original` and `wrapper`, and returns a new string that has the wrapper character at the beginning and end of the original.\n", "\n", - "This tells us that `loadtxt` has one parameter called `fname` that doesn't have a default value,\n", - "and eight others that do.\n", - "If we call the function like this (try):\n", - " ~~~ {.python}\n", - " numpy.loadtxt('data/topo.asc', ',')\n", - " ~~~" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "then the filename is assigned to `fname` (which is what we want),\n", - "but the delimiter string `','` is assigned to `dtype` rather than `delimiter`,\n", - "because `dtype` is the second parameter in the list. However ',' isn't a known `dtype` so\n", - "our code produced an error message when we tried to run it.\n", - "When we call `loadtxt` we don't have to provide `fname=` for the filename because it's the\n", - "first item in the list, but if we want the ',' to be assigned to the variable `delimiter`,\n", - "we *do* have to provide `delimiter=` for the second parameter since `delimiter` is not\n", - "the second parameter in the list." + "1. Write a function, `normalize`, that takes an array as input and returns a corresponding array of values scaled to the range $[0,1]$. (Hint: Look at NumPy functions such as `arange` and `linspace` to see how their arguments are defined.)\n", + "\n", + "1. Rewrite your `normalize` function so that it scales data to $[0,1]$ by default, but allows a user to optionally specify the lower and upper bounds." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - " ## Test your skills 01: Combining strings \n", + "## Summary\n", "\n", - " \"Adding\" two strings produces their concatenation:\n", - " `'a' + 'b'` is `'ab'`.\n", - " Write a function called `fence` that takes two parameters called `original` and `wrapper`\n", - " and returns a new string that has the wrapper character at the beginning and end of the original.\n", - " A call to your function should look like this:\n", + "More info in the Python documentation.\n", "\n", - " ~~~ {.python}\n", - " print(fence('name', '*'))\n", - " ~~~\n", - " ~~~ {.output}\n", - " *name*\n", - " ~~~" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Test your skills 02: Selecting characters from strings \n", + "https://docs.python.org/3/tutorial/controlflow.html#defining-functions\n", "\n", - " If the variable `s` refers to a string,\n", - " then `s[0]` is the string's first character\n", - " and `s[-1]` is its last.\n", - " Write a function called `outer`\n", - " that returns a string made up of just the first and last characters of its input.\n", - " A call to your function should look like this:\n", + "https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions\n", + "including default arguments\n", "\n", - " ~~~ {.python}\n", - " print(outer('helium'))\n", - " ~~~\n", - " ~~~ {.output}\n", - " hm\n", - " ~~~" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Test your skills 03 : Rescaling an array \n", + "Unresolved: formal versus actual parameters.\n", "\n", - " Write a function `rescale` that takes an array as input\n", - " and returns a corresponding array of values scaled to lie in the range 0.0 to 1.0.\n", - " (Hint: If $L$ and $H$ are the lowest and highest values in the original array,\n", - " then the replacement for a value $v$ should be $(v-L) / (H-L)$.)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Test your skills 04: Testing and documenting your function \n", + "Unresolved: global variables.\n", "\n", - " Run the commands `help(numpy.arange)` and `help(numpy.linspace)`\n", - " to see how to use these functions to generate regularly-spaced values,\n", - " then use those values to test your `rescale` function.\n", - " Once you've successfully tested your function,\n", - " add a docstring that explains what it does.\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Test your skills 05: Defining defaults \n", + "If your function doesn't fit on a screen, it's too long.\n", + "Break it up.\n", "\n", - " Rewrite the `rescale` function so that it scales data to lie between 0.0 and 1.0 by default,\n", - " but will allow the caller to specify lower and upper bounds if they want.\n", - " Compare your implementation to your neighbor's:\n", - " do the two functions always behave the same way?" + "The process of building larger programs from smaller functions--composition--is a key element of scientific programing.\n", + "\n", + "How do we know a function is working as we expect?\n", + "This is *unit testing*, covered later." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, - "source": [ - "## Test your skills 06: Variables inside and outside functions\n", - "\n", - " What does the following piece of code display when run - and why?\n", - "\n", - " ~~~ {.python}\n", - " f = 0\n", - " k = 0\n", - "\n", - " def f2k(f):\n", - " k = ((f-32)*(5.0/9.0)) + 273.15\n", - " return k\n", - "\n", - " f2k(8)\n", - " f2k(41)\n", - " f2k(32)\n", - "\n", - " print(k)\n", - " ~~~" - ] + "outputs": [], + "source": [] } ], "metadata": { @@ -791,7 +590,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.9.9" } }, "nbformat": 4, From d1b1096fcf5590551c3776eec9e9020b67907085 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 10 Aug 2023 17:13:23 +0000 Subject: [PATCH 2/7] Be careful with name of stability criterion --- lessons/python/8_diffusion.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lessons/python/8_diffusion.ipynb b/lessons/python/8_diffusion.ipynb index 25363e7..c97455b 100644 --- a/lessons/python/8_diffusion.ipynb +++ b/lessons/python/8_diffusion.ipynb @@ -506,7 +506,7 @@ "- The Eyjafjallajökull volcano produced ashes almost continuously during a couple of weeks. Start from the initial condition above but now add 100 ppm ashes per hour to the volcano grid cell as a source term. \n", "- Assume Dirichlet boundary conditions (0 ppm at 0 km and 0 ppm at 3000 km)\n", "- Plot the initial concentration, also indicate the location of Brussels on the plot (HINT use `plt.scatter()`)\n", - "- Calculate and print out the time step (dt) using the CFL criterion" + "- Calculate and print out the time step (dt) determined through a stability criterion" ] }, { @@ -612,7 +612,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.9.9" } }, "nbformat": 4, From 13b1bfcb06c2cc6ebab33da8a0953f6e8acd24a0 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 10 Aug 2023 17:34:47 +0000 Subject: [PATCH 3/7] Discard type hints section --- lessons/python/6_functions.ipynb | 58 ++++++-------------------------- 1 file changed, 10 insertions(+), 48 deletions(-) diff --git a/lessons/python/6_functions.ipynb b/lessons/python/6_functions.ipynb index 5917fd1..4f54d3e 100644 --- a/lessons/python/6_functions.ipynb +++ b/lessons/python/6_functions.ipynb @@ -302,10 +302,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Type hints\n", + "### Documentation\n", "\n", - "Let's group some more repeated code in the diffusion notebook into a function.\n", - "This is the solver we used for the one-dimensional diffusion equation." + "Let's group some more repeated code from the diffusion notebook into a function;\n", + "in this case, the solver for the one-dimensional diffusion equation." ] }, { @@ -326,10 +326,10 @@ "metadata": {}, "source": [ "The arguments for the grid spacing, time step, and diffusivity take default values,\n", - "but the `concenctration` argument does not.\n", + "but `concenctration`, the argument for the diffused quantity, does not.\n", "\n", "**Question:** Without looking at the body of the function,\n", - "can you tell what sort of variable goes into the `concentration` argument?\n", + "can you tell what variable type goes into the `concentration` argument?\n", "A float? A string? A NumPy array?\n", "\n", "\n", @@ -339,49 +339,11 @@ "What type should a parameter be? Integer, float, string, NumPy array?!\n", "It's hard to tell.\n", "\n", - "This is where *type hints* can be handy.\n", - "\n", - "Do I want to talk about pass by reference?\n", + "This is where documentation is handy.\n", "\n", "How do we know what type/size of variables the function expects from its arguments?\n", - "This is where type hints can help.\n", + "This is where documentation can help.\n", "\n", - "Redo the function with type hints." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "def solve1d(concentration: np.ndarray, grid_spacing: float = 1.0, time_step: float = 1.0, diffusivity: float = 1.0) -> None:\n", - " flux = -diffusivity * np.diff(concentration) / grid_spacing\n", - " concentration[1:-1] -= time_step * np.diff(flux) / grid_spacing" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Type hints are optional, but useful.\n", - "\n", - "Type hints are not enforced." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Docstrings" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ "Add documentation string (docstring) to `solve1d`." ] }, @@ -393,7 +355,7 @@ }, "outputs": [], "source": [ - "def solve1d(concentration: np.ndarray, grid_spacing: float = 1.0, time_step: float = 1.0, diffusivity: float = 1.0) -> None:\n", + "def solve1d(concentration, grid_spacing=1.0, time_step=1.0, diffusivity=1.0):\n", " \"\"\"Solve the one-dimensional diffusion equation with fixed boundary conditions.\n", "\n", " Parameters\n", @@ -410,7 +372,7 @@ " Returns\n", " -------\n", " result : ndarray\n", - " The temperatures after time *time_step*.\n", + " The concentration after a time step.\n", "\n", " Examples\n", " --------\n", @@ -551,7 +513,7 @@ "https://docs.python.org/3/tutorial/controlflow.html#defining-functions\n", "\n", "https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions\n", - "including default arguments\n", + "including default arguments and type hints.\n", "\n", "Unresolved: formal versus actual parameters.\n", "\n", From 0a07e40bd561713093fb80c7422fb84d5f8f8630 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 10 Aug 2023 23:00:02 +0000 Subject: [PATCH 4/7] Complete notebook prose and code --- lessons/python/6_functions.ipynb | 130 +++++++++++++++++-------------- 1 file changed, 71 insertions(+), 59 deletions(-) diff --git a/lessons/python/6_functions.ipynb b/lessons/python/6_functions.ipynb index 4f54d3e..e9688f8 100644 --- a/lessons/python/6_functions.ipynb +++ b/lessons/python/6_functions.ipynb @@ -81,7 +81,7 @@ "source": [ "A function definition begins with the keyword `def`,\n", "followed by the name of the function,\n", - "followed by a comma-delimited listing of arguments (also known as parameters in parentheses,\n", + "followed by a comma-delimited listing of *arguments* (also known as *parameters*) in parentheses,\n", "and ending with a colon `:`.\n", "The code in the body of the function--run when the function is called--must be indented." ] @@ -167,7 +167,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This is useful because it makes the function call more readable.\n", + "This feature, *keyword arguments*, makes the function call more readable.\n", "\n", "Further,\n", "when passing arguments by name,\n", @@ -190,16 +190,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This is useful because it makes the function easier to call--you don;t have to remember the argument order.\n", + "This makes the function easier to call--you don't have to remember the argument order.\n", "\n", - "These techniques can be used with any Python function, whether it's made by us or by someone else." + "Keyword arguments can be used with any Python function, whether it's made by us or by someone else." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Extension\n", + "## Additional features\n", "\n", "Python functions have many interesting features,\n", "more than we can address here.\n", @@ -245,6 +245,7 @@ "metadata": {}, "source": [ "Note that each of the arguments is assigned a default value.\n", + "These are called *default arguments*.\n", "If any argument is omitted from a call to this function,\n", "its default value is used instead." ] @@ -273,7 +274,7 @@ "metadata": {}, "source": [ "Although we omitted the left and right boundary condition values,\n", - "the function call didn't produce an error.\n", + "the function call didn't raise an error.\n", "\n", "Check the result by printing the returned concentration `C`." ] @@ -295,7 +296,7 @@ "source": [ "The default values for the left and right boundary conditions were applied.\n", "\n", - "Using default values makes calling a function easier." + "Using default arguments makes calling a function easier." ] }, { @@ -304,7 +305,7 @@ "source": [ "### Documentation\n", "\n", - "Let's group some more repeated code from the diffusion notebook into a function;\n", + "Let's group one last chunk of repeated code from the diffusion notebook into a function;\n", "in this case, the solver for the one-dimensional diffusion equation." ] }, @@ -325,26 +326,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The arguments for the grid spacing, time step, and diffusivity take default values,\n", + "In our new function `solve1d`,\n", + "the arguments for the grid spacing, time step, and diffusivity take default values,\n", "but `concenctration`, the argument for the diffused quantity, does not.\n", "\n", "**Question:** Without looking at the body of the function,\n", - "can you tell what variable type goes into the `concentration` argument?\n", - "A float? A string? A NumPy array?\n", + "can you tell what variable type the `concentration` argument should be?\n", + "A float? A list? A NumPy array?\n", "\n", - "\n", - "\n", - "Python is dynamically typed.\n", - "\n", - "What type should a parameter be? Integer, float, string, NumPy array?!\n", - "It's hard to tell.\n", - "\n", - "This is where documentation is handy.\n", - "\n", - "How do we know what type/size of variables the function expects from its arguments?\n", "This is where documentation can help.\n", "\n", - "Add documentation string (docstring) to `solve1d`." + "The first statement of the body of a function can optionally hold\n", + "the function's documentation string, or *docstring*.\n", + "It's used to describe the function's purpose, its arguments, and its return value.\n", + "\n", + "Add a docstring to `solve1d`." ] }, { @@ -391,7 +387,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Use the `help` function." + "When a function has a docstring,\n", + "you can use the `help` function or the questions mark `?` to display it\n", + "in a Python session or in a notebook." ] }, { @@ -420,19 +418,38 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Docstring aren't necessary, but they're a good practice.\n", + "In a notebook,\n", + "you can also select the `Shift` + `Tab` keys to view the docstring." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Place the cursor in the line below and select the `Shift` + `Tab` keys.\n", + "solve1d" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Docstring aren't necessary, but they're helpful because they provide information about a function.\n", "\n", - "Documentation systems such as sphinx (link) use information from docstrings to produce documentation." + "Documentation systems such as [Sphinx](https://www.sphinx-doc.org/) use docstrings to produce formatted documentation.\n", + "[NumPy](https://numpy.org/doc/1.20/docs/howto_document.html#docstrings) and [Google](https://google.github.io/styleguide/pyguide.html#s3.8.1-comments-in-doc-strings) have style guidelines for docstrings.\n", + "It's a good practice to pick a style and use it consistently." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Composition\n", + "## Refactoring the diffusion example\n", "\n", - "Put the functions together.\n", - "*Compose* the functions." + "Let's combine the functions we've defined above into a new function that replicates the diffusion example." ] }, { @@ -444,7 +461,7 @@ "outputs": [], "source": [ "def example():\n", - " \"\"\"An example of running `solve1d`.\"\"\"\n", + " \"\"\"An example of using `solve1d` in a diffusion problem.\"\"\"\n", " print(example.__doc__)\n", " D = 100 # diffusivity\n", " Lx = 10 # domain length\n", @@ -463,18 +480,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This is our first taste of how larger programs are built:\n", - "we define basic operations,\n", - "then combine them in ever-larger chunks to get the effect we want.\n", - "Real-life functions will usually be larger than the ones shown here --- typically half a dozen to a few dozen lines --- but\n", - "they shouldn't ever be much longer than that,\n", - "or the next person who reads it won't be able to understand what's going on." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ + "This is a first taste of how programs can be built to solve a problem:\n", + "break the problem into smaller pieces,\n", + "write functions to address the smaller pieces,\n", + "then assemble the functions to solve the problem.\n", + "\n", "Run the example `example`." ] }, @@ -508,32 +518,34 @@ "source": [ "## Summary\n", "\n", - "More info in the Python documentation.\n", + "The process of building larger programs from smaller functions is a key element of scientific programing.\n", "\n", - "https://docs.python.org/3/tutorial/controlflow.html#defining-functions\n", + "Information from the Python documentation, including the sections\n", + "[Defining Functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions) and\n", + "[More on Defining Functions](https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions)\n", + "was used heavily in creating this notebook.\n", + "There's a lot more there, including many features of functions we didn't cover.\n", "\n", - "https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions\n", - "including default arguments and type hints.\n", + "### Topics not covered\n", "\n", - "Unresolved: formal versus actual parameters.\n", + "These are a few topics that we didn't cover in this lesson,\n", + "but they're important enough that we probably should have.\n", "\n", - "Unresolved: global variables.\n", + "* *formal* versus *actual* parameters\n", + "* the concept of *scope*\n", + "* *local* versus *global* variables\n", + "* use of *type hints*\n", "\n", - "If your function doesn't fit on a screen, it's too long.\n", - "Break it up.\n", + "More information on these topics can be found in the Python documentation.\n", + "\n", + "### Last thoughts\n", "\n", - "The process of building larger programs from smaller functions--composition--is a key element of scientific programing.\n", + "If your function doesn't fit on a screen, it's too long.\n", + "Break it up into smaller functions.\n", "\n", - "How do we know a function is working as we expect?\n", - "This is *unit testing*, covered later." + "How do we know a function is working as expected?\n", + "This is partially answered with *unit testing*, covered later." ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 0f396a7db6bf665f6ee92f833e66c18025badba6 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Fri, 11 Aug 2023 00:07:07 +0000 Subject: [PATCH 5/7] Remove lint --- lessons/python/6_functions.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lessons/python/6_functions.ipynb b/lessons/python/6_functions.ipynb index e9688f8..8dedc07 100644 --- a/lessons/python/6_functions.ipynb +++ b/lessons/python/6_functions.ipynb @@ -72,7 +72,7 @@ "outputs": [], "source": [ "def calculate_time_step(grid_spacing, diffusivity):\n", - " return grid_spacing ** 2 / diffusivity / 2.1" + " return grid_spacing**2 / diffusivity / 2.1" ] }, { @@ -465,7 +465,7 @@ " print(example.__doc__)\n", " D = 100 # diffusivity\n", " Lx = 10 # domain length\n", - " dx = 0.5 # grid spacing\n", + " dx = 0.5 # grid spacing\n", "\n", " dt = calculate_time_step(dx, D)\n", " C = set_initial_profile(Lx)\n", From 274762feaec19034baf1fb5175052452252d060f Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Fri, 11 Aug 2023 01:01:15 +0000 Subject: [PATCH 6/7] Ignore duplicate function definition in notebooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7d3d57..57aec98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: args: ["--py38-plus"] - id: nbqa-isort - id: nbqa-flake8 - args: ["--extend-ignore=E402"] + args: ["--extend-ignore=E402,F811"] exclude: | (?x)^( lessons/landlab/landlab-terrainbento/| From 1d2366cdd6723959d31e21de30bbeddbd2e209f0 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Fri, 11 Aug 2023 01:59:26 +0000 Subject: [PATCH 7/7] Give a simple example of calling solve1d --- lessons/python/6_functions.ipynb | 39 +++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lessons/python/6_functions.ipynb b/lessons/python/6_functions.ipynb index 8dedc07..d006218 100644 --- a/lessons/python/6_functions.ipynb +++ b/lessons/python/6_functions.ipynb @@ -440,7 +440,44 @@ "\n", "Documentation systems such as [Sphinx](https://www.sphinx-doc.org/) use docstrings to produce formatted documentation.\n", "[NumPy](https://numpy.org/doc/1.20/docs/howto_document.html#docstrings) and [Google](https://google.github.io/styleguide/pyguide.html#s3.8.1-comments-in-doc-strings) have style guidelines for docstrings.\n", - "It's a good practice to pick a style and use it consistently." + "It's a good practice to pick a style and use it consistently.\n", + "\n", + "Before we move on, try a simple example of using `solve1d`.\n", + "Start by defining a variable, `z`, to diffuse." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "z = np.zeros(5)\n", + "z[2] = 5\n", + "\n", + "print(z)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now call `solve1d` to diffuse `z` for a given time step and diffusivity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "solve1d(z, diffusivity=0.25, time_step=0.5)\n", + "\n", + "print(z)" ] }, {