# Homework 4

*Due by 11:59pm on Tuesday, 7/14*

## Instructions

Download hw04.zip. Inside the archive, you will find a file called hw04.py, along with a copy of the OK autograder.

**Submission:** When you are done, submit with ```
python3 ok
--submit
```

. You may submit more than once before the deadline; only the
final submission will be scored. See Lab 1 for instructions on submitting
assignments.

**Using OK:** If you have any questions about using OK, please
refer to this guide.

**Readings:** You might find the following references
useful:

## Required Questions

### Question 1

A list that contains one or more lists as elements is called a *deep*
list. For example, `[1, [2, 3], 4]`

is a deep list.

Write a function `deep_len`

that takes a list and returns its deep
length. See the doctests for the function's behavior.

*Hint*: you can check if something is a list by using the built-in
`type`

function. For example,

```
>>> type(3) == list
False
>>> type([1, 2, 3]) == list
True
```

```
def deep_len(lst):
"""Returns the deep length of the list.
>>> deep_len([1, 2, 3]) # normal list
3
>>> x = [1, [2, 3], 4] # deep list
>>> deep_len(x)
4
>>> x = [[1, [1, 1]], 1, [1, 1]] # deep list
>>> deep_len(x)
6
"""
"*** YOUR CODE HERE ***"
```

Use OK to test your code:

`python3 ok -q deep_len`

### Question 2

Write a function `flatten`

that takes a (possibly deep) list and "flattens" it.
For example:

```
>>> lst = [1, [[2], 3], 4, [5, 6]]
>>> flatten(lst)
[1, 2, 3, 4, 5, 6]
```

*Hint*: you can check if something is a list by using the built-in
`type`

function. For example,

```
>>> type(3) == list
False
>>> type([1, 2, 3]) == list
True
```

```
def flatten(lst):
"""Returns a flattened version of lst.
>>> flatten([1, 2, 3]) # normal list
[1, 2, 3]
>>> x = [1, [2, 3], 4] # deep list
>>> flatten(x)
[1, 2, 3, 4]
>>> x = [[1, [1, 1]], 1, [1, 1]] # deep list
>>> flatten(x)
[1, 1, 1, 1, 1, 1]
"""
"*** YOUR CODE HERE ***"
```

Use OK to test your code:

`python3 ok -q flatten`

### Intervals (data abstraction)

**Acknowledgements.** This interval arithmetic example is based on
Structure and Interpretation of Computer Programs,
Section 2.1.4.

**Introduction.** Alyssa P. Hacker is designing a system to help people
solve engineering problems. One feature she wants to provide in her
system is the ability to manipulate inexact quantities (such as
measured parameters of physical devices) with known precision, so that
when computations are done with such approximate quantities the results
will be numbers of known precision.

Alyssa's idea is to implement interval arithmetic as a set of arithmetic operations for combining "intervals" (objects that represent the range of possible values of an inexact quantity). The result of adding, subracting, multiplying, or dividing two intervals is itself an interval, representing the range of the result.

Alyssa postulates the existence of an abstract object called an "interval" that has two endpoints: a lower bound and an upper bound. She also presumes that, given the endpoints of an interval, she can construct the interval using the data constructor interval. Using the constructor and selectors, she defines the following operations:

```
def str_interval(x):
"""Return a string representation of interval x.
>>> str_interval(interval(-1, 2))
'-1 to 2'
"""
return '{0} to {1}'.format(lower_bound(x), upper_bound(x))
def add_interval(x, y):
"""Return an interval that contains the sum of any value in interval x and
any value in interval y.
>>> str_interval(add_interval(interval(-1, 2), interval(4, 8)))
'3 to 10'
"""
lower = lower_bound(x) + lower_bound(y)
upper = upper_bound(x) + upper_bound(y)
return interval(lower, upper)
def mul_interval(x, y):
"""Return the interval that contains the product of any value in x and any
value in y.
>>> str_interval(mul_interval(interval(-1, 2), interval(4, 8)))
'-8 to 16'
"""
p1 = lower_bound(x) * lower_bound(y)
p2 = lower_bound(x) * upper_bound(y)
p3 = upper_bound(x) * lower_bound(y)
p4 = upper_bound(x) * upper_bound(y)
return interval(min(p1, p2, p3, p4), max(p1, p2, p3, p4))
```

### Question 3

Alyssa's program is incomplete because she has not specified the implementation of the interval abstraction. Define the constructor and selectors in terms of two-element lists:

```
def interval(a, b):
"""Construct an interval from a to b."""
"*** YOUR CODE HERE ***"
def lower_bound(x):
"""Return the lower bound of interval x."""
"*** YOUR CODE HERE ***"
def upper_bound(x):
"""Return the upper bound of interval x."""
"*** YOUR CODE HERE ***"
```

Use OK to test your code:

```
python3 ok -q str_interval
python3 ok -q add_interval
python3 ok -q mul_interval
```

### Question 4

Alyssa implements division below, by multiplying by the reciprocal of
`y`

. Ben Bitdiddle, an expert systems programmer, looks over Alyssa's
shoulder and comments that it is not clear what it means to divide by
an interval that spans zero. Add an assert statement to Alyssa's code
to ensure that no such interval is used as a divisor:

```
def div_interval(x, y):
"""Return the interval that contains the quotient of any value in x divided by any value in y.
Division is implemented as the multiplication of x by the reciprocal of y.
>>> str_interval(div_interval(interval(-1, 2), interval(4, 8)))
'-0.25 to 0.5'
>>> str_interval(div_interval(interval(4, 8), interval(-1, 2)))
AssertionError
"""
"*** YOUR CODE HERE ***"
reciprocal_y = interval(1/upper_bound(y), 1/lower_bound(y))
return mul_interval(x, reciprocal_y)
```

Use OK to test your code:

`python3 ok -q div_interval`

### Question 5

Using reasoning analogous to Alyssa's, define a subtraction function for intervals:

```
def sub_interval(x, y):
"""Return the interval that contains the difference between any value in x
and any value in y.
>>> str_interval(sub_interval(interval(-1, 2), interval(4, 8)))
'-9 to -2'
"""
"*** YOUR CODE HERE ***"
```

### Question 6

After considerable work, Alyssa P. Hacker delivers her finished system. Several years later, after she has forgotten all about it, she gets a frenzied call from an irate user, Lem E. Tweakit. It seems that Lem has noticed that the formula for parallel resistors can be written in two algebraically equivalent ways:

`par1(r1, r2) = (r1 * r2) / (r1 + r2)`

or

`par2(r1, r2) = 1 / (1/r1 + 1/r2)`

He has written the following two programs, each of which computes the
`parallel_resistors`

formula differently::

```
def par1(r1, r2):
return div_interval(mul_interval(r1, r2), add_interval(r1, r2))
def par2(r1, r2):
one = interval(1, 1)
rep_r1 = div_interval(one, r1)
rep_r2 = div_interval(one, r2)
return div_interval(one, add_interval(rep_r1, rep_r2))
```

Lem complains that Alyssa's program gives different answers for the two ways of computing. This is a serious complaint.

Demonstrate that Lem is right. Investigate the behavior of the system
on a variety of arithmetic expressions. Make some intervals `a`

and
`b`

, and show that `par1`

and `par2`

can give different results.

```
# These two intervals give different results for parallel resistors:
"*** YOUR CODE HERE ***"
```

*Note*: No tests will be run on your solution to this problem. Any answer will
be accepted, but please attempt to answer the question correctly.

### Question 7

Eva Lu Ator, another user, has also noticed the different intervals computed by different but algebraically equivalent expressions. She says that the problem is multiple references to the same interval.

The Multiple References Problem: a formula to compute with intervals using Alyssa's system will produce tighter error bounds if it can be written in such a form that no variable that represents an uncertain number is repeated.

Thus, she says, `par2`

is a better program for parallel resistances
than `par1`

. Is she right? Why? Write a function that returns a string
containing a written explanation of your answer:

```
def multiple_references_explanation():
return """The mulitple reference problem..."""
```

*Note*: No tests will be run on your solution to this problem. Any answer will
be accepted, but please attempt to answer the question correctly.

### Question 8

Write a function `quadratic`

that returns the interval of all values
`f(t)`

such that `t`

is in the argument interval `x`

and `f(t)`

is a
quadratic function:

`f(t) = a*t*t + b*t + c`

Make sure that your implementation returns the smallest such interval, one that does not suffer from the multiple references problem.

*Hint*: the derivative `f'(t) = 2*a*t + b`

, and so the extreme
point of the quadratic is `-b/(2*a)`

:

```
def quadratic(x, a, b, c):
"""Return the interval that is the range of the quadratic defined by
coefficients a, b, and c, for domain interval x.
>>> str_interval(quadratic(interval(0, 2), -2, 3, -1))
'-3 to 0.125'
>>> str_interval(quadratic(interval(1, 3), 2, -3, 1))
'0 to 10'
"""
"*** YOUR CODE HERE ***"
```

Use OK to test your code:

`python3 ok -q quadratic`

## Extra Questions

Extra questions are not worth extra credit and are entirely optional. They are designed to challenge you to think creatively!

### Question 9

Write a function polynomial that takes an interval `x`

and a list of
coefficients `c`

, and returns the interval containing all values of
`f(t)`

for `t`

in interval `x`

, where:

`f(t) = c[k-1] * pow(t, k-1) + c[k-2] * pow(t, k-2) + ... + c[0] * 1`

Like quadratic, your polynomial function should return the smallest such interval, one that does not suffer from the multiple references problem.

*Hint*: You can approximate this result. Try using Newton's
method.

```
def polynomial(x, c):
"""Return the interval that is the range of the polynomial defined by
coefficients c, for domain interval x.
>>> str_interval(polynomial(interval(0, 2), [-1, 3, -2]))
'-3 to 0.125'
>>> str_interval(polynomial(interval(1, 3), [1, -3, 2]))
'0 to 10'
>>> str_interval(polynomial(interval(0.5, 2.25), [10, 24, -6, -8, 3]))
'18.0 to 23.0'
"""
"*** YOUR CODE HERE ***"
```