Tuples
- simple; non-mutable version of lists
 
- faster, safer
 
- can be expressed as 
x, y, z (or (x,y,z), probably clearer) 
- empty tuple: 
() 
- tuple with one element: 
(x,) 
- can do many of the same things as with lists
 
x = (1,4,"a",3)
print(x[1])   ## indexing
## 4
print(x[2:])  ## slicing
## ('a', 3)
print(x+(3,)) ## appending
## (1, 4, 'a', 3, 3)
print(x[:2] + (3,) + x[2:]) ## insertion in the middle
## (1, 4, 3, 'a', 3)
x.index(4)    ## indexing
## 1
"z" in x      ## looking for stuff
## False
x.count(4)    ## count
## 1
- you can’t modify the existing tuple at all (deletion, modification)
 
- unpacking: 
x,y,z = t 
- swapping: 
(a,b) = (b,a) 
- useful as the return value of functions; safe, and can be unpacked
 
- convert to/from lists (
tuple(), list()) 
x = (1,2,3)
def modify(x):
    y = list(x)
    y[0] = "a"
    return(tuple(y))
print(modify(x))
## ('a', 2, 3)
print(x)
## (1, 2, 3)
 
reminders/clarifications
- parentheses (
()) vs square brackets ([]) 
- square brackets
- indexing (lists or strings or tuples): 
x[5] 
- slicing (lists or strings or tuples): 
x[5:7] 
- defining lists: 
[1,2,3] 
 
- parentheses
- order of operations: 
(1+2)*3, a and (not b or c) 
- calling functions: 
len(x), range(5), print("hello") 
- calling methods: 
x.sort(), x.append(4) 
- defining functions: 
def f(x1,x2,x3): 
- returning values from functions: 
return(x) (*) 
- defining tuples: 
(), (1,), (2,3) (*) 
 
“" actually (mostly) optional*: see here
 
Root-finding methods
- Assume that \(f(x)\) is a continuous function on the real numbers.
 
- Suppose that \(a < b\)
 
- Suppose endpoints are of opposite signs:
\(f(a)<0\) and \(f(b)> 0\) or \(f(b)<0\) and \(f(a)>0\) 
- (or \(f(a)\cdot f(b) <0\))
 
- By the Intermediate Value Theorem, there is some number \(c\) between \(a\) and \(b\) with \(f(c) = 0\); this is called a root of the function \(f\)
 
We will use three methods (Grid, Bisection, and Newton’s method) to approximate such a number \(c\). (There may be more than one root of \(f\) in the interval between \(a\) and \(b\).)
 
Example
- We’ll use \(\exp(x)-x-3/2\) as an example
 
- impossible to do analytically!
 
- value at 0 = -3/2
 
- value at 1 = \(\exp(1)-1-3/2 \approx 2.78 - 2.5 = 0.28\)
 
 
Grid Method
- Break the interval \([a, b]\) into \(n\) subintervals of equal sizes, having endpoints \[
x_0 = a, x_1 , \dots , x_{n−1} , x_n = b \quad .
\]
 
- Compute \(f(x_0), f(x_1), \dots , f(x_n)\)
 
- Find the index \(i\) such that \(f(x_i)\) is closest to 0 and use this to approximate a root of \(f\) in the interval \([a, b]\).
 
- Project: Create a function 
grid_search(f, a, b, n) that implements the grid method. 
 
Bisection Method
- Bisect the interval \([a, b]\) into two equal subintervals \([a, m]\), \([m, b]\), where \(m = (a + b)/2\).
 
- If \(f(a)\) and \(f(m)\) have opposite signs, then there will be a root in \([a, m]\). Otherwise, there will be a root in \([m, b]\).
 
- Bisect this subinterval (\([a, m]\) in the former case, \([m, b]\) in the latter), and continue bisecting until the subinterval is small.
 
- A root of \(f\) will be located in this small subinterval.
 
- Project: Create a function 
bisect(f, a, b, tol) that approximates a root of f in the interval [a, b] with an error of at most tol. 
 
Newton’s Method
- Suppose we know the derivative (gradient) \(df/dx=f'(x)\) as well as \(f(x)\)
 
- For a given starting value \(x_0\), guess the position of the root according to \(x_1 = f(x_0)-\frac{x_0}{f'(x_0)}\).
 
- Repeat until we are within tolerance of the root (\(|f(x)|\) is small
 
- Project: Create a function 
newton(f, grad, x0, tol, nmax) that approximates a root of f with an error of at most tol, taking no more than nmax steps.