Student's Name: PLEASE INSERT YOUR NAME HERE

**Directions:** Add work to this notebook to solve the problems below. Some other cells have been included which are important for testing your work and should give you some feeback.

Check your work.

**Your notebook should run without printing errors and without user input. If this is not the case, points may be deducted from your grade.**

Problem Sources:

**LL**:*Programming for Computations - Python*by Svein Linge and Hans Petter Langtangen, 2nd edition.**L**:*A Primer on Scientific Programming with Python*by Hans Petter Langtangen, 2nd edition.**TAK**:*Applied Scientific Computing With Python*by Peter R. Turner, Thomas Arildsen, and Kathleen Kavanagh.

In [ ]:

```
# Standard imports
import numpy as np
import matplotlib.pyplot as plt
import math as m
from mpmath import mp, iv
from scipy import linalg
from numpy import random
import random as random_number
```

According to an authorative youtube video a nickel has a $1/6000$ chance of landing on its edge. Write a function `nickel_flip()`

that takes no inputs and returns one of three strings, "Edge", "Heads" and "Tails". The function should return "Edge" with probability $1/6000$ and return "Heads" and Tails" with equal probability.

In [ ]:

```
```

A video game character lives in a $2$-dimensional integer lattice. That is, his coordinates are always a pair of integers $(x, y)$. He can move in the four compass directions one unit at a time.

Write a class `Position`

whose initialization method takes two parameters (in addition to `self`

), the initial x-coordinate `x0`

and the initial y-coordinate `y0`

. Both should be integers. If `p`

is a `Position`

object, it should have the following methods:

`p.north()`

should increase the character's y-coordinate by one.`p.east()`

should increase the character's x-coordinate by one.`p.south()`

should decrease the character's y-coordinate by one.`p.west()`

should decrease the character's x-coordinate by one.`p.beam(xb, yb)`

should move the character directly to position`(xb, yb)`

`p.x()`

should return the characters x-coordinate.`p.y()`

should return the characters y-coordinate.`p.coordinates()`

should return the character's coordinates as a pair.

In [ ]:

```
```

In [ ]:

```
# Test
p = Position(3, 5) # Start at position (3,5)
assert p.x() == 3
assert p.y() == 5
p.south()
assert p.x() == 3
assert p.y() == 4
p.west()
p.south()
assert p.coordinates() == (2, 3)
p.beam(-2, 3)
p.north()
p.east()
assert p.coordinates() == (-1, 4)
```

Write a class named `StatisticsTracker`

which keeps track of statistical quantities related to real number measurements. No data should be passed to the initializer of the class. A `StatisticalTracker`

object `st`

should have the following methods available:

`st.number()`

should return the number of measurements received so far. (This should be a non-negative integer.)`st.mean()`

should return the mean of the measurements received so far.`st.variance()`

should return the variance of the measurements received so far.`st.new_measurement(x)`

should update the internal variables of the class so that the quantities returned by the above methods are correct when adding the float`x`

to the prior received data set. This method should return nothing.

**Constraint:** You are not to store all the measurements received. Instead just store quantities necessary to keep track of the statistical quantities. Some hints are below.

To keep track of the number of measurements, your class should devote a variable to keeping count of the number of measurements received.

Recall that the mean of measurements $x_0, \ldots, x_{n-1}$ is $$\mu = \frac{1}{n} \sum_{i=0}^{n-1} x_i.$$ The number $n$ is the total number of measurements. So, it also makes sense to keep track of the sum of the measurements.

The variance should be computed by assuming all measurements made are equally likely. Thus the variance is given by the formula $$Var = \frac{1}{n} \sum_{i=0}^{n-1} (x_i - \mu)^2.$$ Since the mean $\mu$ will change as more data is received, it is best to treat $\mu$ as an unknown variable. Expanding the above formula we see: $$Var = \frac{1}{n} \left(\sum_{i=0}^{n-1} x_i^2\right) - \frac{2 \mu}{n}\left(\sum_{i=0}^{n-1} x_i\right) + \mu^2.$$ Thus it also makes sense to keep track of the sums of the squares of the measurements.

In [ ]:

```
```

In [ ]:

```
# Test
st = StatisticsTracker()
assert st.number() == 0, "The number of tests should be 0."
st.new_measurement(5.0) # Add one measurement of 5.0
assert st.number() == 1, "The number of tests should be 1."
assert st.mean() == 5.0, "The mean should be 5."
assert st.variance() == 0.0, "The variance should be 0."
st.new_measurement(1.0) # Add a second measurement of 1.0
assert st.number() == 2, "The number of tests should be 2."
assert st.mean() == 3.0, "The mean should be 3."
assert st.variance() == 4.0, "The variance should be 2."
st.new_measurement(7.0) # Add a third measurement of 7.0
st.new_measurement(-4.0) # Add a fourth measurement of -4.0
assert st.number() == 4, "The number of tests should be 2."
assert st.mean() == 2.25, "The mean should be 2.25."
assert st.variance() == 17.6875, "The variance should be 17.6875."
```

Write a class `TaylorSeries`

so that the initializer of a `TaylorSeries`

object takes two parameters, a coefficient function `c`

mapping degrees (non-negative integers) to coefficients (floating point real numbers) and a center `x0`

. The initialized object should represent the Taylor Series (expressed as a function of $x$):
$$\sum_{d=0}^{+\infty} c_d \cdot (x - x_0)^d.$$
(Here we have used $c_d$ to represent `c(d)`

and $x_0$ for `x0`

.)

A `TaylorSeries`

object `ts`

should have the following methods:

`ts.center()`

should return the center $x_0$.`ts.coefficient(d)`

should return the coefficient of the degree $d$ term. That is, this function should return $c_d$ in the expression above.`ts.partial_sum(x,N)`

should return the finite sum $$\sum_{d=0}^{N} c_d \cdot (x - x_0)^d.$$`ts.derivative()`

should return the derivative of this Taylor Series as a`TaylorSeries`

. The derivative of the series is $$\sum_{d=0}^{+\infty} (d+1) c_{d+1} \cdot (x - x_0)^d.$$

*Remark:* To define a derivative you should first define a coefficient function for the derivative series using currying. Then you can pass that new function to the `TaylorSeries`

initializer.

In [ ]:

```
```

In [ ]:

```
# Test
# Construct the exponential function
c = lambda d: 1.0/m.factorial(d)
ts = TaylorSeries(c, 0)
assert ts.partial_sum(0, 10) == 1.0
assert abs( ts.partial_sum(1.0, 15) - m.e ) < 10**-10, \
"ts.partial_sum(1.0, 15) should be close to e."
assert abs( ts.partial_sum(-1.0, 15) - 1/m.e ) < 10**-10, \
"ts.partial_sum(-1.0, 15) should be close to 1/e."
# The derivative should also be the exponential function
der = ts.derivative()
for d in range(10):
assert abs( der.coefficient(d) * m.factorial(d) - 1) < 10**-8, \
"The coefficient of der of degree {} is wrong.".format(d)
# Same checks as above.
assert der.partial_sum(0, 10) == 1.0
assert abs( der.partial_sum(1.0, 15) - m.e ) < 10**-10, \
"der.partial_sum(1.0, 15) should be close to e."
assert abs( der.partial_sum(-1.0, 15) - 1/m.e ) < 10**-10, \
"der.partial_sum(-1.0, 15) should be close to 1/e."
```

We'd like to construct a random function using Taylor series. To define a function in terms of Taylor series you just need to determine the coefficients. We'll choose the coefficients $c_d$ at uniformly from the interval $[\frac{-1}{d!}, \frac{1}{d!}]$. (This seems like a weird choice but guarantees that every derivative $f^{(n)}(x_0)$ is taken at random from $[-1,1]$.

The following is a first attempt:

In [ ]:

```
def random_coef(d):
bound = 1 / m.factorial(d)
return 2*bound*np.random.random_sample() - bound
```

Unfortunately it doesn't work as the following plot demonstrates.

In [ ]:

```
ts = TaylorSeries(random_coef, 0)
x = np.linspace(-5,5,1000)
y = [ts.partial_sum(val, 20) for val in x]
plt.plot(x,y)
```

What is happening is that every time the `TaylorSeries`

object needs a coefficient, it calls `random_coef`

. Each time this occurs it generates a new random number. But the coefficicient of degree `d`

should be a constant. Observe:

In [ ]:

```
c3 = random_coef(3)
print("The coeficient of degree 3 is {}.".format(c3))
c3 = random_coef(3)
print("The coeficient of degree 3 is {}.".format(c3))
```

To fix this, we will create a function class that *caches* (stores) coefficients that it has already selected.

Construct a class `RandomCoefficients`

whose initializer takes no input. An object `rc`

of type `RandomCoefficients`

should have a `__call__`

method so that `rc(d)`

returns a random number between $\frac{-1}{d!}$ and $\frac{1}{d!}$. The call method should return the same random number whenever called with the same value of `d`

.

*Suggestion:* You might want to have a `RandomCoefficients`

store a dictionary. When `rc(d)`

is called for the first time with input `d`

, the random number should be computed and stored as the image of `d`

under the dictionary. Then when `rc(d)`

is called a second time, the number should be restored from the dictionary and returned.

In [ ]:

```
```

In [ ]:

```
# Test
rc = RandomCoefficients()
val = rc(0)
assert -1 <= val <= 1
assert val == rc(0), "The return value of rc(0) has changed."
val = rc(2)
assert -1/2 <= val <= 1/2
assert val == rc(2), "The return value of rc(2) has changed."
assert val != rc(1), "The return value of rc(1) should differ from rc(2)."
```

Use objects created by the `RandomCoefficients`

class to create some random functions. Experiment with plotting these functions.

In [ ]:

```
```