import numpy as np

def bisection(func, a=0, b=1, tol=1e-6, maxits=1000, which="min"):
    """Implements a basic bisection search on the function f over the interval [a,b].

    Args:
        f (func): the function to optimize. 
        a (float): left end-point to optimize over.
        b (float): the right end-point to optimize over.
        tol (float): relative tolerance for when to stop iterating.
        maxits (int): maximum number of iterations.
    """
    if which == "max":
        func = lambda x: func(-x)
        
    if func(a)*func(b) >= 0:
        raise ValueError("Bisection failed... f(a) and f(b) must be of oposite sign!")
    
    if func(a) == 0:
        return a
    
    if func(b) == 0:
        return b
    
    for i in range(maxits):
        midpoint = (b+a)/2
        f_mid = func(midpoint)
        f_a = func(a)
        f_b = func(b)
        
        if f_a*f_mid < 0:
            b = midpoint
        elif f_b*f_mid < 0:
            a = midpoint
        elif f_mid == 0:
            return midpoint
        
        if np.abs((b+a)/2 - midpoint) < tol:
            return (b+a)/2
        else:
            pass


def newton(f, df, x0, epsilon, maxiter):
    x = x0
    for n in range(0, maxiter):
        f_x = f(x)
        if abs(f_x) < epsilon:
            return x
        df_x = df(x)
        if df_x == 0:
            print("Error: derivative is zero")
        x -= f_x/df_x
    return x







