import numdifftools as nd
import numpy as np
import pandas as pd
from estimagic.decorators import numpy_interface
from estimagic.differentiation import differentiation_auxiliary as aux
[docs]def gradient(
func,
params,
method="central",
extrapolation=True,
func_kwargs=None,
step_options=None,
):
"""
Calculate the gradient of *func*.
Args:
func (function): A function that maps params into a float.
params (DataFrame): see :ref:`params`
method (str): The method for the computation of the derivative. Default is
central as it gives the highest accuracy.
extrapolation (bool): This variable allows to specify the use of the
richardson extrapolation.
func_kwargs (dict): additional positional arguments for func.
step_options (dict): Options for the numdifftools step generator.
See :ref:`step_options`
Returns:
Series: The index is the index of params, the values contain the estimated
gradient.
"""
step_options = step_options if step_options is not None else {}
if method not in ["central", "forward", "backward"]:
raise ValueError("Method has to be in ['central', 'forward', 'backward']")
func_kwargs = {} if func_kwargs is None else func_kwargs
internal_func = _create_internal_func(func, params, func_kwargs)
params_value = params["value"].to_numpy()
if extrapolation:
grad_np = nd.Gradient(internal_func, method=method, **step_options)(
params_value
)
else:
grad_np = _no_extrapolation_gradient(internal_func, params_value, method)
return pd.Series(data=grad_np, index=params.index, name="gradient")
def _no_extrapolation_gradient(internal_func, params_value, method):
grad = np.empty_like(params_value)
f_x0 = internal_func(params_value)
finite_diff = getattr(aux, method)
for i, val in enumerate(params_value):
h = (1 + abs(val)) * np.sqrt(np.finfo(float).eps)
grad[i] = finite_diff(internal_func, f_x0, params_value, i, h) / h
return grad
[docs]def jacobian(
func,
params,
method="central",
extrapolation=True,
func_kwargs=None,
step_options=None,
):
"""
Calculate the jacobian of *func*.
Args:
func (function): A function that maps params into a numpy array or pd.Series.
params (DataFrame): see :ref:`params`
method (string): The method for the computation of the derivative. Default is
central as it gives the highest accuracy.
extrapolation (bool): This variable allows to specify the use of the
richardson extrapolation.
func_kwargs (dict): additional positional arguments for func.
step_options (dict): Options for the numdifftools step generator.
See :ref:`step_options`
Returns:
DataFrame: If func returns a Series, the index is the index of this Series or
the index is 0,1,2... if func returns a numpy array. The columns are the
index of params.
"""
step_options = step_options if step_options is not None else {}
if method not in ["central", "forward", "backward"]:
raise ValueError("Method has to be in ['central', 'forward', 'backward']")
func_kwargs = {} if func_kwargs is None else func_kwargs
f_x0 = func(params, **func_kwargs)
internal_func = _create_internal_func(func, params, func_kwargs)
params_value = params["value"].to_numpy()
if extrapolation:
jac_np = nd.Jacobian(internal_func, method=method, **step_options)(params_value)
else:
jac_np = _no_extrapolation_jacobian(internal_func, params_value, method)
if isinstance(f_x0, pd.Series):
return pd.DataFrame(index=f_x0.index, columns=params.index, data=jac_np)
else:
return pd.DataFrame(columns=params.index, data=jac_np)
def _no_extrapolation_jacobian(internal_func, params_value, method):
f_x0_np = internal_func(params_value)
jac = np.empty((len(f_x0_np), len(params_value)))
finite_diff = getattr(aux, method)
for i, val in enumerate(params_value):
# The rule of thumb for the stepsize is implemented
h = (1 + abs(val)) * np.sqrt(np.finfo(float).eps)
f_diff = finite_diff(internal_func, f_x0_np, params_value, i, h)
jac[:, i] = f_diff / h
return jac
[docs]def hessian(
func,
params,
method="central",
extrapolation=True,
func_kwargs=None,
step_options=None,
):
"""
Calculate the hessian of *func*.
Args:
func (function): A function that maps params into a float.
params (DataFrame): see :ref:`params`
method (string): The method for the computation of the derivative. Default is
central as it gives the highest accuracy.
extrapolation (bool): Use richardson extrapolations.
func_kwargs (dict): additional positional arguments for func.
step_options (dict): Options for the numdifftools step generator.
See :ref:`step_options`
Returns:
DataFrame: The index and columns are the index of params. The data is
the estimated hessian.
"""
step_options = step_options if step_options is not None else {}
if method != "central":
raise ValueError("Only the method 'central' is supported.")
func_kwargs = {} if func_kwargs is None else func_kwargs
internal_func = _create_internal_func(func, params, func_kwargs)
params_value = params["value"].to_numpy()
if extrapolation:
hess_np = nd.Hessian(internal_func, method=method, **step_options)(params_value)
else:
hess_np = _no_extrapolation_hessian(internal_func, params_value, method)
return pd.DataFrame(data=hess_np, index=params.index, columns=params.index)
def _no_extrapolation_hessian(internal_func, params_value, method):
finite_diff = getattr(aux, method)
hess = np.empty((len(params_value), len(params_value)))
for i, val_1 in enumerate(params_value):
h_1 = (1.0 + abs(val_1)) * np.cbrt(np.finfo(float).eps)
for j, val_2 in enumerate(params_value):
h_2 = (1.0 + abs(val_2)) * np.cbrt(np.finfo(float).eps)
params_r = params_value.copy()
params_r[j] += h_2
# Calculate the first derivative w.r.t. var_1 at (params + h_2) with
# the central method. This is not the right f_x0, but the real one
# isn't needed for the central method.
f_plus = finite_diff(internal_func, None, params_r, i, h_1)
params_l = params_value.copy()
params_l[j] -= h_2
# Calculate the first derivative w.r.t. var_1 at (params - h_2) with
# the central method. This is not the right f_x0, but the real one
# isn't needed for the central method.
f_minus = finite_diff(internal_func, None, params_l, i, h_1)
f_diff = (f_plus - f_minus) / (2.0 * h_1 * h_2)
hess[i, j] = f_diff
hess[i, j] = f_diff
return hess
def _create_internal_func(func, params, func_kwargs):
@numpy_interface(params)
def internal_func(p):
func_value = func(p, **func_kwargs)
return func_value
return internal_func