
from scratch.linear_algebra import Vector, dot

###################################
# Square sum of vector v

def sum_of_squares(v):
    """computes the sum of squared elements in v"""
    return sum(v_i ** 2 for v_i in v)

x = [1,2,3]
print(x, sum_of_squares(x))


###################################
# Approximate of f'(x)

def difference_quotient(f, x, h):
    return (f(x + h) - f(x)) / h


###########################################
# Approximate of f'(x) where f(x) = x^2

from functools import partial

def square(x):
    return x * x

def derivative(x):
    return 2 * x

derivative_estimate = partial(difference_quotient, square, h=0.00001)

################################################
# plot f'(x) = 2x and it's approximation

import matplotlib.pyplot as plt

xs = range(-10, 11)
actuals = [derivative(x) for x in xs]
estimates = [difference_quotient(square, x, h=0.001) for x in xs]

# plot to show they're basically the same
plt.plot(xs, actuals, 'rx', label='Actual')       # red  x
plt.plot(xs, estimates, 'b+', label='Estimate')   # blue +

plt.title("Actual Derivatives vs. Estimates")
plt.legend(loc=9)
# plt.show()


###################################################################
# Multiple variables
#
# partial_difference_quotient(f, v(n), i (int), h (float))
#
#  = estimate of partial derivate of f(v) at i-th component
#  = d(f(v))/d(x_i)

def partial_difference_quotient(f, v, i, h):
    """compute the ith partial difference quotient of f at v"""

    w = [v_j + (h if j == i else 0)    # add h to ONLY the ith element of v
         for j, v_j in enumerate(v)]
    return (f(w) - f(v)) / h

###############################################################
# Estimate the gradient
#
#  estimate_gradient(f, v) returns a VECTOR !!!
#  Each partial_difference_quotient() call returns a number
#  The list comprehension makes it a VECTOR !!!

def estimate_gradient(f, v, h=0.00001):
    return [partial_difference_quotient(f, v, i, h)
            for i, _ in enumerate(v)]


###########################################################
# Example:

def sum_of_squares(v):
    """computes the sum of squared elements in v"""
    return sum(v_i ** 2 for v_i in v)

x = [1,2,3]
df = estimate_gradient(sum_of_squares, x)

print(x, df)

