#!/usr/bin/env python
# coding: utf-8
# # Approximating using the CompEcon toolbox
#
# **Randall Romero Aguilar, PhD**
#
# This demo is based on the original Matlab demo accompanying the Computational Economics and Finance 2001 textbook by Mario Miranda and Paul Fackler.
#
# Original (Matlab) CompEcon file: **demapp00.m**
#
# Running this file requires the Python version of CompEcon. This can be installed with pip by running
#
# !pip install compecon --upgrade
#
# Last updated: 2021-Oct-01
#
#
# ## Initial tasks
# In[1]:
import numpy as np
import matplotlib.pyplot as plt
from compecon import BasisChebyshev,nodeunif
from matplotlib import cm
#
#
# ## Univariate approximation
# Approximating the function $f(x) = e^{-2x}$. Its derivative is $f'(x) = -2e^{-2x}$
# In[2]:
f1 = lambda x: np.exp(-2 * x)
d1 = lambda x: -2 * np.exp(-2 * x)
# ### Fit approximant
#
# The CompEcon toolbox defines the ```BasisChebyshev``` class for Chebyshev interpolation. Its positional arguments are `n` (number of nodes), `a` (lower bound) and `b` (upper bound). The optional keyword argument `f` indicates a function (the lambda `f1` in our example) to be approximated.
# In[3]:
n, a, b = 10, -1, 1
f1fit = BasisChebyshev(n, a, b, f=f1)
# Here, `f1fit` is an instance of the `BasisChebyshev` class. Once a function is specified (as with the keyword argument `f` above), it can be evaluated at a given vector `x` by *calling* `f1fit` as any other function
# ```
# f1fit(x) # returns a vector containing the interpolation of each element of x
# f1fit() # without arguments, it evaluates the function at the basis nodes
# ```
#
# When a function is specified with the option `f`, the `BasisChebyshev` object computes the interpolation coefficients $c = \Phi(x)^{-1}f(x)$, where $x$ represent the nodes of bhe basis. Alternatively, if the values `fx` of the function at the nodes are available (instead of the function itself, as is usually the case), then the basis is created by:
# ```
# BasisChebyshev(n, a, b, y=fx)
# ```
# ### Graph approximation error for function and derivative
# To evaluate the precission of the interpolation, we compare the the fitted function `f1fit` to the true values `f1` over a grid of 1001 points. We do the same for the derivative function.
#
# In the figures, the red dots represent the 10 interpolation nodes (where residuals equal zero, by construction). These are returned by the `.nodes` attribute of the basis.
# In[4]:
axopts = {'xlabel': 'x', 'ylabel': 'Error', 'xticks': [-1, 0, 1]}
x = np.linspace(a, b, 1001)
fig = plt.figure(figsize=[12, 6])
ax1 = fig.add_subplot(121, title='Function approximation error', **axopts)
ax1.axhline(linestyle='--', color='gray', linewidth=2)
ax1.plot(f1fit.nodes, np.zeros_like(f1fit.nodes), 'ro', markersize=12)
ax1.plot(x, f1fit(x) - f1(x))
ax2 = fig.add_subplot(122, title='Derivative approximation error', **axopts)
ax2.plot(x, np.zeros_like(x), '--', color='gray', linewidth=2)
ax2.plot(f1fit.nodes, np.zeros_like(f1fit.nodes), 'ro', markersize=12)
ax2.plot(x, f1fit(x, 1) - d1(x))
#
#
# ## Bivariate Interpolation
# Approximating the function $f(x_1, x_2) = \dfrac{\cos(x_1)}{e^{x_2}}$.
# In[5]:
f2 = lambda x: np.cos(x[0]) / np.exp(x[1])
# ### Set degree and domain interpolation
# The ```BasisChebyshev``` class can also interpolate *d*-dimensional functions. If one of the positional arguments is a scalar (like the bounds below), it is assumed that the same value holds in every dimension.
# In[6]:
n, a, b = 7, 0.0, 1.0
f2fit = BasisChebyshev([n, n], a, b, f=f2)
# By default, multidimensional interpolation is done by taking the tensor product of each dimension. Other options are available with the keyword `method`, as in:
# ```
# BasisChebyshev(n, a, b, method='smolyak', qn=3, qp= 3) # for Smolyak interpolation
# BasisChebyshev(n, a, b, method='complete', qp=2) # for complete polynomials
# BasisChebyshev(n, a, b, method='tensor') # tensor product (default)
# ```
#
# Notice that Smolyak and complete polynomials interpolation require the setting of keywords `qn` and `qp`, to control node and polynomial selection, respectively.
# ### Nice plot of function approximation error
# Now evaluate the residuals over a 101 by 101 grid.
# In[7]:
nplot = [101, 101]
x = nodeunif(nplot, a, b)
x1, x2 = x
error = f2fit(x) - f2(x)
error.shape = nplot
x1.shape = nplot
x2.shape = nplot
fig = plt.figure(figsize=[15, 6])
ax = fig.gca(projection='3d', title='Chebyshev Approximation Error',
xlabel='$x_1$', ylabel='$x_2$', zlabel='Error')
ax.plot_surface(x1, x2, error, rstride=1, cstride=1, cmap=cm.coolwarm,
linewidth=0, antialiased=False)
# ### Compute partial derivatives
# Partial derivatives can be computed by *calling* a `BasisChebyshev` object, indicating the order of derivatives by a second argument `order`, as in
# ```
# f2fit(x, order)
# ```
#
# Notice that unlike the MATLAB version of CompEcon, in this Python version the first index of `x` indicates the dimension, while the last index indicates an "observation" (evaluation point). Something similar applies to the `order` parameter: `order[i, j]` indicates the order of differentiation with respect to `i` in evaluation `j`.
# In[8]:
x = np.array([[0.5], [0.5]])
order = [[1, 0, 2, 1, 0],
[0, 1, 0, 1, 2]]
ff = f2fit(x, order)
print(('x = [0.5, 0.5]\n' +
'f1 = {:7.4f}\n' +
'f2 = {:7.4f}\n' +
'f11 = {:7.4f}\n' +
'f12 = {:7.4f}\n' +
'f22 = {:7.4f}').format(*ff))