Skip to content

Commit

Permalink
Merge pull request #1020 from alanlujan91/CHI_new_feats
Browse files Browse the repository at this point in the history
Additional Features for CubicHermiteInterp
  • Loading branch information
mnwhite authored Feb 10, 2025
2 parents ed2eeff + 4d57fb2 commit 2d76243
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 81 deletions.
3 changes: 2 additions & 1 deletion Documentation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ Release Date: TBD
- Removes a specific way of accounting for ``employment'' in the idiosyncratic-shocks income process. [#1473](https://github.com/econ-ark/HARK/pull/1473)
- Adds income process constructor for the discrete Markov state consumption-saving model. [#1484](https://github.com/econ-ark/HARK/pull/1484)
- Changes the behavior of make_lognormal_RiskyDstn so that the standard deviation represents the standard deviation of log(returns)
- Adds detailed parameter and latex documentation to most models.
- Adds detailed parameter and LaTeX documentation to most models.
- Add PermGroFac constructor that explicitly combines idiosyncratic and aggregate sources of growth. [#1489](https://github.com/econ-ark/HARK/pull/1489)
- Suppress warning from calc_stable_points when it would be raised by inapplicable AgentType subclasses. [#1493](https://github.com/econ-ark/HARK/pull/1493)
- Fixes notation errors in IndShockConsumerType.make_euler_error_func from prior changes. [#1495](https://github.com/econ-ark/HARK/pull/1495)
- Fixes typos in IdentityFunction interpolator class. [#1492](https://github.com/econ-ark/HARK/pull/1492)
- Expands functionality of Cobb-Douglas aggregator for CRRA utility. [#1363](https://github.com/econ-ark/HARK/pull/1363)
- Adds a new function for using Tauchen's method to approximate an AR1 process. [#1521](https://github.com/econ-ark/HARK/pull/1521)
- Adds additional functionality to the CubicHermiteInterp class, imported from scipy.interpolate. [#1020](https://github.com/econ-ark/HARK/pull/1020/)

### 0.15.1

Expand Down
7 changes: 3 additions & 4 deletions HARK/ConsumptionSaving/ConsIndShockModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
expected,
)
from HARK.interpolation import (
CubicInterp,
LinearInterp,
LowerEnvelope,
MargMargValueFuncCRRA,
MargValueFuncCRRA,
ValueFuncCRRA,
)
from HARK.interpolation import CubicHermiteInterp as CubicInterp
from HARK.metric import MetricObject
from HARK.rewards import (
CRRAutility,
Expand Down Expand Up @@ -863,14 +863,13 @@ def solve_one_period_ConsKinkedR(

# Construct the assets grid by adjusting aXtra by the natural borrowing constraint
aNrmNow = np.sort(
np.hstack((np.asarray(aXtraGrid) + mNrmMinNow, np.array([0.0, 0.0]))),
np.hstack((np.asarray(aXtraGrid) + mNrmMinNow, np.array([0.0, 1e-15]))),
)

# Make a 1D array of the interest factor at each asset gridpoint
Rfree = Rsave * np.ones_like(aNrmNow)
Rfree[aNrmNow < 0] = Rboro
Rfree[aNrmNow <= 0] = Rboro
i_kink = np.argwhere(aNrmNow == 0.0)[0][0]
Rfree[i_kink] = Rboro

# Calculate end-of-period marginal value of assets at each gridpoint
vPfacEff = DiscFacEff * Rfree * PermGroFac ** (-CRRA)
Expand Down
51 changes: 48 additions & 3 deletions HARK/interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import numpy as np
from scipy.interpolate import CubicHermiteSpline

from HARK.metric import MetricObject
from HARK.rewards import CRRAutility, CRRAutilityP, CRRAutilityPP

Expand Down Expand Up @@ -1231,7 +1230,7 @@ def __init__(
_check_grid_dimensions(1, self.y_list, self.x_list)
_check_grid_dimensions(1, self.dydx_list, self.x_list)

self.n = self.x_list.size
self.n = len(x_list)

self._chs = CubicHermiteSpline(
self.x_list, self.y_list, self.dydx_list, extrapolate=None
Expand Down Expand Up @@ -1330,6 +1329,53 @@ def _evalAndDer(self, x):
dydx = self._der_helper(x, out_bot, out_top)
return y, dydx

def der_interp(self, nu=1):
"""
Construct a new piecewise polynomial representing the derivative.
See `scipy` for additional documentation:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html
"""
return self._chs.derivative(nu)

def antider_interp(self, nu=1):
"""
Construct a new piecewise polynomial representing the antiderivative.
Antiderivative is also the indefinite integral of the function,
and derivative is its inverse operation.
See `scipy` for additional documentation:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html
"""
return self._chs.antiderivative(nu)

def integrate(self, a, b, extrapolate=False):
"""
Compute a definite integral over a piecewise polynomial.
See `scipy` for additional documentation:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html
"""
return self._chs.integrate(a, b, extrapolate)

def roots(self, discontinuity=True, extrapolate=False):
"""
Find real roots of the the piecewise polynomial.
See `scipy` for additional documentation:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html
"""
return self._chs.roots(discontinuity, extrapolate)

def solve(self, y=0.0, discontinuity=True, extrapolate=False):
"""
Find real solutions of the the equation ``pp(x) == y``.
See `scipy` for additional documentation:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.CubicHermiteSpline.html
"""
return self._chs.solve(y, discontinuity, extrapolate)


class BilinearInterp(HARKinterpolator2D):
"""
Expand Down Expand Up @@ -4716,5 +4762,4 @@ def __call__(self, *cFuncArgs):
"cFunc does not have a 'derivativeX' attribute. Can't compute"
+ "marginal marginal value."
)

return MPC * CRRAutilityPP(c, rho=self.CRRA)
16 changes: 10 additions & 6 deletions HARK/tests/test_interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
This file implements unit tests for interpolation methods
"""

import unittest
from HARK.interpolation import (
IdentityFunction,
LinearInterp,
BilinearInterp,
TrilinearInterp,
QuadlinearInterp,
)
from HARK.interpolation import CubicHermiteInterp as CubicInterp

import numpy as np

from HARK.interpolation import BilinearInterp
from HARK.interpolation import CubicHermiteInterp as CubicInterp
from HARK.interpolation import LinearInterp, QuadlinearInterp, TrilinearInterp
from HARK.interpolation import IdentityFunction
import unittest
import numpy as np


class testsLinearInterp(unittest.TestCase):
Expand Down
196 changes: 129 additions & 67 deletions examples/Interpolation/CubicInterp.ipynb

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions examples/Interpolation/CubicInterp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# ---
# jupyter:
# jupytext:
# formats: ipynb,py:percent
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.11.2
# kernelspec:
# display_name: Python 3
# language: python
# name: python3
# ---

# %% [markdown]
# # Cubic Interpolation with Scipy

# %% pycharm={"name": "#%%\n"}
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import CubicHermiteSpline

from HARK.interpolation import CubicInterp, CubicHermiteInterp

# %% [markdown]
# ### Creating a HARK wrapper for scipy's CubicHermiteSpline
#
# The class CubicHermiteInterp in HARK.interpolation implements a HARK wrapper for scipy's CubicHermiteSpline. A HARK wrapper is needed due to the way interpolators are used in solution methods accross HARK, and in particular due to the `distance_criteria` attribute used for VFI convergence.

# %% pycharm={"name": "#%%\n"}
x = np.linspace(0, 10, num=11, endpoint=True)
y = np.cos(-(x**2) / 9.0)
dydx = 2.0 * x / 9.0 * np.sin(-(x**2) / 9.0)

f = CubicInterp(x, y, dydx, lower_extrap=True)
f2 = CubicHermiteSpline(x, y, dydx)
f3 = CubicHermiteInterp(x, y, dydx, lower_extrap=True)

# %% [markdown]
# Above are 3 interpolators, which are:
# 1. **CubicInterp** from HARK.interpolation
# 2. **CubicHermiteSpline** from scipy.interpolate
# 3. **CubicHermiteInterp** hybrid newly implemented in HARK.interpolation
#
# Below we see that they behave in much the same way.

# %% pycharm={"name": "#%%\n"}
xnew = np.linspace(0, 10, num=41, endpoint=True)

plt.plot(x, y, "o", xnew, f(xnew), "-", xnew, f2(xnew), "--", xnew, f3(xnew), "-.")
plt.legend(["data", "hark", "scipy", "hark_new"], loc="best")
plt.show()

# %% [markdown]
# We can also verify that **CubicHermiteInterp** works as intended when extrapolating. Scipy's **CubicHermiteSpline** behaves differently when extrapolating, as it extrapolates using the last polynomial, whereas HARK implements linear decay extrapolation, so it is not shown below.

# %% pycharm={"name": "#%%\n"}
x_out = np.linspace(-1, 11, num=41, endpoint=True)

plt.plot(x, y, "o", x_out, f(x_out), "-", x_out, f3(x_out), "-.")
plt.legend(["data", "hark", "hark_new"], loc="best")
plt.show()

# %% [markdown]
# ### Timings
#
# Below we can compare timings for interpolation and extrapolation among the 3 interpolators. As expected, `scipy`'s CubicHermiteInterpolator (`f2` below) is the fastest, but it's not HARK compatible. `HARK.interpolation`'s CubicInterp (`f`) is the slowest, and `HARK.interpolation`'s new CubicHermiteInterp (`f3`) is somewhere in between.

# %% pycharm={"name": "#%%\n"}
# %timeit f(xnew)
# %timeit f(x_out)

# %% pycharm={"name": "#%%\n"}
# %timeit f2(xnew)
# %timeit f2(x_out)

# %% pycharm={"name": "#%%\n"}
# %timeit f3(xnew)
# %timeit f3(x_out)

# %% [markdown] pycharm={"name": "#%%\n"}
# Notice in particular the difference between interpolating and extrapolating for the new ** CubicHermiteInterp **.The difference comes from having to calculate the extrapolation "by hand", since `HARK` uses linear decay extrapolation, whereas for interpolation it returns `scipy`'s result directly.

# %% [markdown]
# ### Additional features from `scipy`
#
# Since we are using `scipy`'s **CubicHermiteSpline** already, we can add a few new features to `HARK.interpolation`'s new **CubicHermiteInterp** without much effort. These include:
#
# 1. `der_interp(self[, nu])` Construct a new piecewise polynomial representing the derivative.
# 2. `antider_interp(self[, nu])` Construct a new piecewise polynomial representing the antiderivative.
# 3. `integrate(self, a, b[, extrapolate])` Compute a definite integral over a piecewise polynomial.
# 4. `roots(self[, discontinuity, extrapolate])` Find real roots of the the piecewise polynomial.
# 5. `solve(self[, y, discontinuity, extrapolate])` Find real solutions of the the equation pp(x) == y.

# %%
int_0_10 = f3.integrate(a=0, b=10)
int_2_8 = f3.integrate(a=2, b=8)
antiderivative = f3.antider_interp()
int_0_10_calc = antiderivative(10) - antiderivative(0)
int_2_8_calc = antiderivative(8) - antiderivative(2)

# %% [markdown]
# First, we evaluate integration and the antiderivative. Below, we see the numerical integral between 0 and 10 using `integrate` or the `antiderivative` directly. The actual solution is `~1.43325`.

# %%
int_0_10, int_0_10_calc

# %% [markdown]
# The value of the integral between 2 and 8 is `~0.302871`.

# %%
int_2_8, int_2_8_calc

# %% [markdown]
# ### `roots` and `solve`
#
# We evaluate these graphically, by finding zeros, and by finding where the function equals `0.5`.

# %%
roots = f3.roots()
intercept = f3.solve(y=0.5)

plt.plot(roots, np.zeros(roots.size), "o")
plt.plot(intercept, np.repeat(0.5, intercept.size), "o")
plt.plot(xnew, f3(xnew), "-.")
plt.legend(["roots", "intercept", "cubic"], loc="best")
plt.plot(xnew, np.zeros(xnew.size), ".")
plt.plot(xnew, np.ones(xnew.size) * 0.5, ".")
plt.show()

# %%

0 comments on commit 2d76243

Please sign in to comment.