from IPython.display import SVG, display
display(SVG(url='https://upload.wikimedia.org/wikipedia/commons/9/90/Finite_difference_method.svg'))

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

def f(x):
    return -(0.1*x**4)-(0.15*x**3)-(0.5*x**2)-(0.25*x)+1.2
    #return np.sin(x)

def df(x):
    return (-0.4*x**3)-(0.45*x**2)-(1.0*x)-0.25
    #return np.cos(x)

x = np.linspace(0,1,100)   
x0 = 0.5
n = 30

h = np.zeros(n,float)
err = np.zeros(n,float)

for i in range(n):
    h[i] = float(10**(-i))
    dfnum = (f(x0+h[i]/2.)-f(x0-h[i]/2.))/(h[i])
    dftrue = df(x0)
    err[i] = np.abs(dfnum-dftrue)
    
fig, ax = plt.subplots(2)
ax[0].plot(x,f(x))
ax[1].loglog(h,err)

    

plt.scatter(x,f(x))

def f(x):
    #return -(0.1*x**4)-(0.15*x**3)-(0.5*x**2)-(0.25*x)+1.2
    return np.sin(x)

def df(x):
    #return (-0.4*x**3)-(0.45*x**2)-(1.0*x)-0.25
    return np.cos(x)

h = 1.0e-1

a = 0; b=2*np.pi

x = np.arange(a,b+h,h)
dfnum = np.zeros(x.size)

for i in range(x.size):
    if ((i > 0) & (i < x.size-2)):
        dfnum[i] = (f(x[i]+h)-f(x[i]-h))/(2*h)
    elif (i == 0):
        dfnum[i] = (f(x[i]+h)-f(x[i]))/h
    else:
        dfnum[i] = (f(x[i])-f(x[i]-h))/h
        
plt.plot(x,dfnum,'.r')
plt.plot(x,f(x))
plt.plot(x,df(x))
plt.show()

"""
Explanation: Derivadas numéricas
A definição de derivada é:
$$\frac{df}{dx} = \lim_{h \to 0} {f(x+h)-f(x) \over h}$$ 
O método mais simples para o cáculo de derivadas numéricas é o de diferenças finitas:
$$f'(x) \approx {f(x+h)-f(x) \over h}$$ 
foward diference
$$f'(x) \approx {f(x)-f(x-h) \over h}$$ 
backward diference
A escolha de um ou outro método vai depender do problema a ser resolvido. Por exemplo em casos onde se necessita da derivada na fronteira de um intervalo. Dependendo da posição da fronteira somente um ou outro método será aplicavel. De modo geral ambos dão aproximadamente o mesmo resultado.
Ao contrário das integrais, as derivadas numericas são afetadas pelo erro de arredondamento assim como erro de resolução devido a escolha do tamanho $h$ do intervalo.
No caso das derivadas, como estamos lidando com uma subtração de numeros e estes tendem a ser muito proximos pois $h$ deve ser o menor possível, o erro de arredondamento fica importante.
O livro texto mostra como obter o $h$ ideal para que não se cometam grandes erros de arredondamento.
De modo geral, usando expansão de Taylor para expressar a função $f(x)$ pode-se mostrar que:
$$ h = \sqrt{4C \left|\frac{f(x)}{f''(x)} \right |} $$
$$ \epsilon = h|f''(x)| = ( 4C|f(x)f''(x)|)^{1/2} $$
onde C é a precisão da máquina. Ou seja, o tamanho ideal seria da ordem de $\sqrt{C}$, assim como o erro obtido. No caso do Python este valor é da ordem de 1.0e-8.
Uma maneira de se obter um resultado melhor é usando a diferença central:
$$f'(x) \approx {f(x+h/2)-f(x-h/2) \over h}$$ 
ou central or symmetric diference
usando as expansões obtemos para o $h$ ideal e o erro:
$$ h = \left( 24C \left|\frac{f(x)}{f'''(x)} \right | \right)^{1/3} $$
$$ \epsilon = \frac{1}{8}h^2|f'''(x)| = ( \frac{9}{8}C^2[f(x)]^2|f'''(x)|)^{1/3} $$
neste caso usando a precisão típica do Python temos um $h$ ideal de 1.0e-5 que leva a um erro da ordem de 1.0e-10.
Veja abaixo a interpretação geométricas das 3 metodologias de cálculo:
fonte: https://en.wikipedia.org/wiki/Finite_differencehttps://en.wikipedia.org/wiki/Finite_difference
End of explanation
"""


from scipy.misc import derivative

x = np.arange(0,5)
der = derivative(np.exp,x,dx=0.1)

print(der)

"""
Explanation: Derivada de dados tabulados
Muitas vezes temos que lidar com o problema de obter derivadas de dados obtidos experimentalmente. Em geral para esses dados não temos a opção de calcular a função em pontos específicos e muitas vezes nem mesmo temos a função. Existem duas situações típicas: 1) dados amostrados regularmente 2) dados sem regularidade na amostragem.
No primeiro caso usamos a formula das diferenças centrais. No entanto, como em geral não temos como gerar outros pontos, devemos adaptar o método. No caso de dados amostrados em intervalos regulares de tamanho h usamos:
$$f'(x) \approx {f(x+h)-f(x-h) \over 2h}$$ 
Outra estratégia é calcular a derivada com a diferença central usual mas em um ponto no meio do intervalo. A desvantagem é que a derivada seria calculada em um ponto para o qual dados não foram obtidos e em algumas aplicações isso pode não ser útil.
No segundo caso, não ha outra alternativa a não ser fazer interpolação para regularizar a amostragem dos dados. Veremos isso mais adiante no curso.
aproximações de ordem mais alta
Uma maneira de interpretar o procedimento de cálculo de uma derivada numérica é que estamos ajustando uma reta em dois pontos distantes h um do outro e tomando a inclinação dessa reta como nossa derivada. Nesse contexto podemos pensar em ajustar polinomios de ordem mais altas para calcular as derivadas, assim como fizemos com os métodos de Newton-Cotes de ordens maiores.
No livro texto é mostrado uma aproximação de polinomio de segundo grau, que leva a formula da diferença central, assim como uma tabela com coeficientes para aproximações de ordens maiores:
http://www.umich.edu/~mejn/cp/chapters/int.pdf
Usando o Python
A função SciPy scipy.misc.derivative calcula derivados usando a fórmula de diferença central.
End of explanation
"""


interv[10]

"""
Explanation: Segunda Derivada
Derivadas Parciais
End of explanation
"""


