Before we get on to the main topics in this week's handout - control flow and functions - there are a couple of topics\n",
"to level up in first: using the `print()`

function and a reminder of logical operators.

We're going to be using a bit more of the `print`

function today in our scripts, to help us to see what's\n",
"happening in our code.

Recall that `print()`

can be used with a value (of any type) inserted directly

It can also print more than one thing at once:

" ] }, { "cell_type": "code", "execution_count": null, "id": "424bae52", "metadata": {}, "outputs": [], "source": [ "print(\"Hello\", \"World\")" ] }, { "cell_type": "markdown", "id": "1ee5cd0b", "metadata": {}, "source": [ "It can be used to print a variable

" ] }, { "cell_type": "code", "execution_count": null, "id": "f94dcedf", "metadata": {}, "outputs": [], "source": [ "x = [1, 5, 7]\n", "print(x)" ] }, { "cell_type": "markdown", "id": "59239efe", "metadata": {}, "source": [ "But what if we want the output to be something like this, where the 5 is a variable?

" ] }, { "cell_type": "code", "execution_count": null, "id": "242d5a49", "metadata": {}, "outputs": [], "source": [ "The value is 5" ] }, { "cell_type": "markdown", "id": "5fd8a7a7", "metadata": {}, "source": [ "this is going to be a string with a variable value injected into it. The way this is done is as follows:

" ] }, { "cell_type": "code", "execution_count": null, "id": "054afa8a", "metadata": {}, "outputs": [], "source": [ "a = 5\n", "print(\"The value is {}\".format(a))\n", "``` \n", "\n", "`{}` is a placeholder for the variable that is specified inside `format()`. If we need to insert more than one variable\n", "then we can add them, comma-separated, inside `format()` as follows\n", "\n", "```python\n", "a = 5\n", "b = 6\n", "print(\"The values are {} and {}\".format(a, b))" ] }, { "cell_type": "markdown", "id": "08080ecb", "metadata": {}, "source": [ "The placeholders can be labelled so that a variable can be inserted twice in the same string:

" ] }, { "cell_type": "code", "execution_count": null, "id": "d8745d4a", "metadata": {}, "outputs": [], "source": [ "a = 5\n", "b = 6\n", "print(\"The values are {0} and {1}. Here's the first one again...{0}\".format(a, b))" ] }, { "cell_type": "markdown", "id": "c49b3b11", "metadata": {}, "source": [ "Things start to get a little complicated if you want to specify the format of the values you are inserting.\n", "Here's an example which prints $\\pi$ and $e$ to 2 decimal places:

" ] }, { "cell_type": "code", "execution_count": null, "id": "cf2f83ea", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "print(\"Some stuff to 2 decimal places... pi = {:.2f} and e = {:.2f}\".format(np.pi, np.e))\n", "# or including the labels \n", "print(\"Some stuff to 2 decimal places... pi = {0:.2f} and e = {1:.2f}\".format(np.pi, np.e))" ] }, { "cell_type": "markdown", "id": "77371659", "metadata": {}, "source": [ "In `{0:.2f}`

, the `0`

is the label (so `np.pi`

is inserted here), the `:`

separates the label from the format\n",
"specification, `.2`

is for 2 decimal places and `f`

is for float.

We could spend all day talking about string formatting, and at this stage it will be easier to look up what you\n", "want to do when you get to it; here's a guide\n", "for reference later, or if you're just really keen.

\n", "Recall from Handout 1 that we use logical operators to determine whether a statement is true or false

\n", "Others which can be used to make up such an expression are as follows:

\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "Operator | Description |
---|---|

< | Less than |

> | Greater than |

<= | Less than or equal to |

>= | Greater than or equal to |

== | Equal to |

!= | Not equal to |

a and b | is a AND b |

a or b | is a OR b |

Here are some more examples

" ] }, { "cell_type": "code", "execution_count": null, "id": "9163da04", "metadata": {}, "outputs": [], "source": [ "a = 3\n", "print(a <= 3) # True\n", "print(a < 5 or a > 25) # True\n", "print(a < 5 and a > 25) # False\n", "print(a % 2 == 0) # False (is the remainder when divided by 2 zero - in other words is a even)" ] }, { "cell_type": "markdown", "id": "b985f4b3", "metadata": {}, "source": [ "These will form an important part of the next section on control flow.

\n", "This section introduces loops and if statements, part of a family of tools used by programmers and referred to\n", "generally as control flow.

\n", "A 'for loop' is used when we would like to repeat a piece of code many times, usually incrementing a value so that\n", "something slightly different happens at each iteration.

\n", "The basic construction of a for loop is as follows:

\n", "Notice the syntax, importantly the colon at the end of the `for`

line, and the indentation. It doesn't really matter\n",
"what the indentation is, a tab or some spaces - a Python enthusiast will tell you that 4 spaces is best - the important\n",
"thing is that you are consistent!

Go on try it without...

" ] }, { "cell_type": "code", "execution_count": null, "id": "b9db721c", "metadata": {}, "outputs": [], "source": [ "for n in range(1,6):\n", "# do something with n" ] }, { "cell_type": "markdown", "id": "14299036", "metadata": {}, "source": [ "Yep, you get an error!

\n", "The comment labelled \"do something with n\" indicates exactly that, usually you would do something with the\n", "current value of $n$. The loop, in this case, runs 5 times, the first time $n=1$, then $n=2$ and so on until $n=5$,\n", "and then it stops.

\n", "So here my choice of \"doing something\" is to print the value of $n^2$:

" ] }, { "cell_type": "code", "execution_count": null, "id": "1cdcf92c", "metadata": {}, "outputs": [], "source": [ "for n in range(1, 6):\n", " print(n**2)" ] }, { "cell_type": "markdown", "id": "05900825", "metadata": {}, "source": [ "We could get fancy and print some text alongside the value:

" ] }, { "cell_type": "code", "execution_count": null, "id": "46c81e55", "metadata": {}, "outputs": [], "source": [ "for n in range(1, 6):\n", " print(\"The value squared is {}\".format(n**2))" ] }, { "cell_type": "markdown", "id": "c699d7d6", "metadata": {}, "source": [ "The `range(1,6)`

could be any list, so we can do this, for example

Or even other data types,

" ] }, { "cell_type": "code", "execution_count": null, "id": "e7fde034", "metadata": {}, "outputs": [], "source": [ "bears = [\"koala\", \"panda\", \"sun\"]\n", "for x in bears:\n", " print(x)" ] }, { "cell_type": "markdown", "id": "4cac5b0b", "metadata": {}, "source": [ "The loop could do pretty much anything with `n`

. For example here I add the values of n by initialising a variable s,\n",
"and then adding n to it at each step of the loop:

Notice that the line break and subsequent unindent marks the end of the for loop contents.

\n", "The `s += n`

adds `n`

to the current `s`

value and is equivalent to `s = s + n`

.

`+=`

is known as an assignment operator, and there are others such as `*=`

e.g. `s *= n`

equivalent to `s = s * n`

.

Practice looping with `for`

in this exercise.

Loops aren't always faster, but can offer a lot of flexibility. For example a `while`

loop can run whilst a certain\n",
"condition is satisfied, e.g.

Here, the `s < 1000`

is a logical expression, it returns either true or false, and so the while loop can be read\n",
"\"while s < 1000 is true\". Note that you might expect the final value of `s`

to be less than 1000. Have a read through\n",
"the code logic to convince yourself that it is sensible that its value should be greater than 1000.

Note that it's easy to get stuck in an infinite while loop, if the condition that is set happens to never be satisfied.\n", "If this happens to you then the stop button in the Console, or Ctrl-C will stop your code!

\n", "An `if`

statement ensures that a bit of code is only executed if a condition is met:

The `print`

command is only executed \"if x >= 2\" is true. On the other hand here,

the `print`

command in the if statement is not executed.

That isn't the whole story: we can use `if`

...`else`

..., with the command after `else`

acting as a fall back,\n",
"for example,

Or, for even more options, `if`

...`elif`

...`else`

...

Take note again of the indenting. This is particularly important for nested clauses. The following is an alternative\n",
"to the `while`

loop above:

Here the command `break`

ends the for loop when `s > 1000`

. Of course this wasn't as good as our while loop as we had\n",
"to guess how many iterations to use (i.e. that 100 was large enough). Notice how we can see which commands go with\n",
"the `if`

and `for`

respectively, thanks to the consistent indenting.

Recall that we can query a single element of a list or array like this:

\n", "We can also update the value inside a list:

" ] }, { "cell_type": "code", "execution_count": null, "id": "66cc300b", "metadata": {}, "outputs": [], "source": [ "x = [2, 5, 8, 5]\n", "x[2] = 4\n", "print(x)" ] }, { "cell_type": "markdown", "id": "3c88bbd0", "metadata": {}, "source": [ "We could use this inside a for loop. Suppose we try to fill an array t with values. We might expect that we\n",
"could do this to update the n-th element of `t`

at each iteration, but not quite. Python is happy over-writing\n",
"array values, but isn't happy if they don't yet exist. So this doesn't work:

The solution is to either append,

" ] }, { "cell_type": "code", "execution_count": null, "id": "181714ab", "metadata": {}, "outputs": [], "source": [ "t = []\n", "for n in range(0,10):\n", " t.append(n**2)" ] }, { "cell_type": "markdown", "id": "33da849c", "metadata": {}, "source": [ "or, using NumPy, initialise `t`

as an empty array first using the `np.zeros()`

function

then the code works perfectly. Check with

" ] }, { "cell_type": "code", "execution_count": null, "id": "ea1d9d20", "metadata": {}, "outputs": [], "source": [ "print(t)" ] }, { "cell_type": "markdown", "id": "1590c01d", "metadata": {}, "source": [ "Note this is the same as

" ] }, { "cell_type": "code", "execution_count": null, "id": "6b60555f", "metadata": {}, "outputs": [], "source": [ "n = np.arange(0, 10)\n", "t = n**2" ] }, { "cell_type": "markdown", "id": "b492fcda", "metadata": {}, "source": [ "and that this way (using vector arithmetic) is much more efficient; using for loops is not always the best solution.\n", "I take a look at the performance of the above two options in an interlude shortly.

\n", "Which produces this plot:

\n", "*Download the full source code for this plot*

Use the above as a template to help you tackle the following exercise:

\n", "\n", "Python makes it possible to write your own functions, which take some input and return a value or values, in just the\n", "same way as Python's built-in functions. This helps to keep your Python code as modular as possible.

\n", "The syntax for creating a function is as follows:

\n", "Note a similar syntax as for control flow: the function begins with the keyword `def`

and then the function name\n",
"\"my_func\". This is followed by input arguments inside brackets - for this function there are none, and finally a colon.\n",
"The contents of the function are then indented.

We can call the function with

" ] }, { "cell_type": "code", "execution_count": null, "id": "6ecc8300", "metadata": {}, "outputs": [], "source": [ "my_func()" ] }, { "cell_type": "markdown", "id": "5f9a56b0", "metadata": {}, "source": [ "either from the same file or the Console.

\n", "Now let's add an input argument to our function and a more descriptive name:

" ] }, { "cell_type": "code", "execution_count": null, "id": "84117f30", "metadata": {}, "outputs": [], "source": [ "def zoo_visit(animal):\n", " print(\"I went to the zoo and saw a {}\".format(animal))\n", "\n", "zoo_visit(\"koala\")\n", "zoo_visit(\"panda\")\n", "zoo_visit(\"sun bear\")" ] }, { "cell_type": "markdown", "id": "ad0756aa", "metadata": {}, "source": [ "We can also set a default value by using argument = ... in the parentheses. This default is used if the input\n", "argument is not set.

" ] }, { "cell_type": "code", "execution_count": null, "id": "b9ac6a79", "metadata": {}, "outputs": [], "source": [ "def zoo_visit(animal=\"bear\"):\n", " print(\"I went to the zoo and saw a {}\".format(animal))\n", "\n", "zoo_visit(\"koala\")\n", "zoo_visit(\"panda\")\n", "zoo_visit(\"sun bear\")\n", "zoo_visit()" ] }, { "cell_type": "markdown", "id": "e5f4196e", "metadata": {}, "source": [ "Any data type can be sent to a function. Here's a list, with a for loop to print each value - pay careful attention\n", "to the indenting:

" ] }, { "cell_type": "code", "execution_count": null, "id": "8adfca8f", "metadata": {}, "outputs": [], "source": [ "def print_my_list(list):\n", " for x in list:\n", " print(x)\n", "\n", "print_my_list([1, 5, 2, 6])" ] }, { "cell_type": "markdown", "id": "3c30a79c", "metadata": {}, "source": [ "And a simple one with a number

" ] }, { "cell_type": "code", "execution_count": null, "id": "162563cf", "metadata": {}, "outputs": [], "source": [ "def square_a_number(x):\n", " print(x**2)\n", "\n", "square_a_number(2)" ] }, { "cell_type": "markdown", "id": "dfc4f738", "metadata": {}, "source": [ "All of the above examples print something. The functions we have been using however return a value which can be\n", "assigned to a variable. For example at the moment this does not do what we might like

" ] }, { "cell_type": "code", "execution_count": null, "id": "2470369b", "metadata": {}, "outputs": [], "source": [ "x = square_a_number(2)" ] }, { "cell_type": "markdown", "id": "988abe42", "metadata": {}, "source": [ "To return a value (or values) from a function we need to use a `return`

statement. This is done as follows:

Note that the `x`

in the function argument and the `x`

outside the function are completely unrelated.\n",
"This is known as the scope of a variable.

Now let's extend this by accepting two input arguments:

" ] }, { "cell_type": "code", "execution_count": null, "id": "177f01ca", "metadata": {}, "outputs": [], "source": [ "def show_me_the_bigger(a, b):\n", " return max([a, b])\n", "\n", "x = show_me_the_bigger(4, 5)\n", "print(x)" ] }, { "cell_type": "markdown", "id": "7c8f29ac", "metadata": {}, "source": [ "Some function practice.

\n", "\n", "In this exercise we'll bring the work we did in exercise 3.1 into a function.

\n", "\n", "Here is a neat trick! It can be used when you have a function that returns a value, and you'd like to make a list using the output of that\n", "function for various inputs. For example, let's say you have some function:

\n", "and a list of values you want to calculate that function for:

" ] }, { "cell_type": "code", "execution_count": null, "id": "bb4f2c23", "metadata": {}, "outputs": [], "source": [ "a_list = [1, 6, 3.4, 27, 5.12]" ] }, { "cell_type": "markdown", "id": "c3e094ef", "metadata": {}, "source": [ "You can get the result of running the function on each element in the list by performing what's called a \"list comprehension\":

" ] }, { "cell_type": "code", "execution_count": null, "id": "02db3d4f", "metadata": {}, "outputs": [], "source": [ "x = [calculate_something(a) for a in a_list]\n", "print(x)" ] }, { "cell_type": "markdown", "id": "1bff1dbf", "metadata": {}, "source": [ "Of course, if you're using numpy functions, this is often not required because they can act on lists already (e.g. `np.sin([0.1,0.2,0.3])`

).\n",
"Still, list comprehension can be useful!

A comment contained within three quotes `\"\"\"`

at the start of our custom function is used to display help.\n",
"It is known as a *docstring* (documentation string)

Test your help with

" ] }, { "cell_type": "code", "execution_count": null, "id": "ff1e6f0c", "metadata": {}, "outputs": [], "source": [ "help(sin_plus_cos)" ] }, { "cell_type": "markdown", "id": "17dc874b", "metadata": {}, "source": [ "Consider the following function (which is a bit silly!)

\n", "We will get an error when we try the following:

" ] }, { "cell_type": "code", "execution_count": null, "id": "7a442974", "metadata": {}, "outputs": [], "source": [ "inverse(0)" ] }, { "cell_type": "markdown", "id": "609aa060", "metadata": {}, "source": [ "Not very easy to read, you have to go right to the bottom to see where the problem is!

\n", "The try and except block is used to catch and handle exceptions. Python executes code following the try statement,\n", "and if there is an exception then the code that follows the except statement is executed.

\n", "This gives quite a generic error message, which does not depend on which of the two problems\n", "(zero division or string input) that we have.

\n", "We can be more precise with error handling by using the exception classes provided by Python. In the long code output\n",
"above you will have noticed that the error was classified as a `ZeroDivisionError`

. There are others including\n",
"`TypeError`

which will be the problem when we use `fraction(1,\"2\")`

as input. For a full list\n",
"see the Python documentation.

We can catch these specific errors and send more specific info back as follows:

" ] }, { "cell_type": "code", "execution_count": null, "id": "0abe681c", "metadata": {}, "outputs": [], "source": [ "def inverse(x):\n", " try:\n", " return 1/x\n", " except ZeroDivisionError:\n", " print(\"Error: please enter a non-zero value\")\n", " except TypeError:\n", " print(\"Error: input argument should be a float or integer\")\n", " \n", "inverse(0)\n", "inverse(\"2\")\n", "inverse(2)" ] }, { "cell_type": "markdown", "id": "3b6eab65", "metadata": {}, "source": [ "One more thing we might want to do with this function is reject decimal input values. We can do this by proactively\n", "\"raising\" an exception by querying the input. The syntax goes like this:

\n", "When we raise an error like this we get the full traceback, like the example at the start of this section.

\n", "When does a piece of code become an algorithm? For us we're pretty much there already at that point.

\n", "An algorithm is a set of step-by-step instructions, in our case carried out by our Python code, to solve a problem.

\n", "As an analogy, in the problem of baking a cake, a recipe book lays out the step-by-step instructions to achieve the\n", "tasty goal. The detail is such that, if the instructions are followed precisely, a second cake produced using the same\n", "recipe will be identical.

\n", "Our goal in the next two worked examples will be to identify what the steps are to solving problems presented to us,\n", "and then how to code them in Python.

\n", "Here are two worked examples:

\n", "In the Fibonacci sequence, each number is the sum of the two preceding ones:

\n", "$ F_{n}=F_{n-1}+F_{n-2}$\n", "where typically the seed values $F_{0}=0,F_{1}=1$ are used.

\n", "Our task is to write an algorithm that puts (say the first 15) values of the sequence into a vector.

\n", "It's useful to plan out what we want to do in advance. In this case I note the following:

\n", "- \n",
"
- I will need a vector
`F`

which I can initialise with`zeros`

, ready to fill with 15 values \n",
" - The first two values are given, so I can set
`F[0]`

and`F[1]`

straight away ($F_0$ is going to be stored in`F[0]`

, $F_1$ in`F[1]`

and so on). \n",
" - For each value from $n = 2$ and upwards,
`F[n]`

is going to depend on the values of`F[n-1]`

and`F[n-2]`

. As this will get repetitive, it will be ideal for a`for`

or`while`

loop! \n",
"

Now that I've done a bit of planning, I am in a position to put this into Python:

\n", "Try increasing the number of Fibonacci values calculated. Note that they grow very fast! If you like, you could make a\n",
"plot showing how quickly they grow by plotting `F`

with matplotlib.

Spoiler alert: Later in your degree you will be confronted with all sorts of problems involving differential equations,\n", "as many physical behaviours can be easily described with them. A differential equation is simply an equation which\n", "relates some function(s) to its derivatives, for example

\n", "$ \\frac{\\mathrm{d}y}{\\mathrm{d}t}=-\\frac{y}{2}. $\n", "This sort of differential equation (involving the derivative of a function which depends on just one variable) is\n",
"known as an **ordinary differential equation (ODE)** and is usually associated with some sort of initial condition,\n",
"for example the value at $y(0)$, in which case it is known as an **initial value problem**.

To name but a few differential equations: Newton's second law, Maxwell's equations of electromagnetism,\n", "radioactive decay, Newton's law of cooling... some of these you will have already met in some form... others you will\n", "meet: the heat equation (thermodynamics), the SchrÃ¶dinger equation (quantum mechanics), Navier-Stokes (fluid dynamics),\n", "and many more...

\n", "It is interesting for us to look at solving differential equations for several reasons. One, that it is clearly going\n",
"to be useful to your later study. But secondly, the oldest algorithm to compute a numerical solution to straight forward\n",
"problems of this sort is **Eulerâ€™s method**, which involves little more than a `for`

loop.

Suppose that we know the value of $y(t_0)$ = $y_0$ and we want to find $y(t_1)$ = $y_1$ for some equation in the form

\n", "$ \\frac{\\mathrm{d}y}{\\mathrm{d}t}=f(t,y). $\n", "If $h = t_1-t_0$ is very small, then we can reasonably expect that $y_1$ will be close to the tangent line to $y(t)$\n", "at $t_0$. The gradient of that tangent line is $m = f(t_0,y_0)$, the right hand side of the ODE at $t_0$. So,\n", "from elementary geometry,

\n", "$ y_1-y_0 \\approx m(t_1 -t_0) = f(t_0,y_0)(t_1 -t_0) $\n", "And since $t_1 = h + t_0$, we obtain,

\n", "$ y_1 \\approx y_0 + f(t_0,y_0)h. $\n", "Now we can use the same method to find $y_2$ from $y_1$, and so on. So the Euler Method approximates the solution using

\n", "$ y_{n} = y_{n-1} + hf(t_{n-1},y_{n-1}). $\n", "Here's a visual of how it works:

\n", "The accuracy will clearly depend on choosing $h$ carefully. As we can see, as the stepsize $h$ is reduced,\n", "the solution approaches the exact (red curve below)

\n", "$ h = 1$\n", "$ h = 0.5$\n", "$ h = 0.25$\n", "Unfortunately, small $h$ means more iterations. Even more unfortunately, Eulerâ€™s method is often not stable,\n", "meaning that the error in the approximation can quickly accumulate to such a size that the numerical solution\n", "diverges wildly from the true solution.

\n", "The main value in Eulerâ€™s method is that it illustrates an important principle. Other, more reliable, methods use the\n", "same basic idea to find numerical solutions to differential equations, but use more information about $y(t)$ to move\n", "from one point to the next.

\n", "The general problem is:

\n", "$ \\frac{\\mathrm{d}y}{\\mathrm{d}t}=f(t,y). $\n", "and we would like to code up

\n", "$ y_{n} = y_{n-1} + hf(t_{n-1},y_{n-1}). $\n", "for some function $f$ which might depend on $t$ and $y$, some stepsize in $t$ given by $h$, and where $y_0$ is known.\n", "It's very similar in many ways to the Fibonacci example. In my example above, the differential equation is

\n", "$ \\frac{\\mathrm{d}y}{\\mathrm{d}t}=-\\frac{y}{2},\\quad y(0)=5. $\n", "So in fact my function will only depend on $y$. Here's my plan:

\n", "- \n",
"
- Set up an function to do the job of $f(y)=-y/2$\n", " \n", "
- Choose a step-size for $h$: let's pick $h=0.5$. \n", "
- Choose how many values of $y$ I'm going to compute - let's say 20 - and initialise a vector for
`y`

using`np.zeros`

. \n",
" - Set the initial value
`y[0]`

= 0. \n",
" - Write a for loop to calculate the remaining values of
`y[n]`

, based on the value of`y[n-1]`

. \n",
"

Here we go:

\n", "If we want to plot $y$ versus $t$ we will need a vector for $t$. Given the timestep $h=0.5$, we know that this is going to be

" ] }, { "cell_type": "code", "execution_count": null, "id": "a8c811bb", "metadata": {}, "outputs": [], "source": [ "t = np.arange(0, 10, 0.5)" ] }, { "cell_type": "markdown", "id": "fea8d677", "metadata": {}, "source": [ "So now we could plot

" ] }, { "cell_type": "code", "execution_count": null, "id": "33e4adbd", "metadata": {}, "outputs": [], "source": [ "plt.plot(t,y)" ] }, { "cell_type": "markdown", "id": "95d9984d", "metadata": {}, "source": [ "*Download the full source code for this plot*

- \n",
"
- Use the code above as a template (i.e. copy and paste it!) and change the problem to solve \n", "

You should need to change only two lines in the code (the function and the initial value).

\n", "- \n",
"
- Write a
`for`

loop over the whole bit of code above to change the value of $y(0)$ to different values between 0 and 1.\n", "It might look something like this: \n",
"

If you plot all of the solutions then you should get something like this:

\n", "The differential equation is a rather famous one, and is used for population modelling. The $y$ here represents the\n", "proportion of a population area that is filled. And the $0.5$ in the RHS of the ODE is a parameter which determines the rate of growth/decline\n", "(e.g. try changing it to -0.5 to see decline).

\n", "That's it for this week! We now have all the skills to import data, run algorithms on it, and produce beautiful plots\n", "of output. We'll expand on the data analysis side of things next week, as we look at some curve fitting techniques.

\n", "