Source code for numdifftools.fornberg

from __future__ import absolute_import, division
from collections import namedtuple
import numpy as np
from scipy.special import factorial
from numdifftools.extrapolation import EPS, dea3
from numdifftools.limits import _Limit

_INFO = namedtuple('info', ['error_estimate',
                            'degenerate',
                            'final_radius',
                            'function_count',
                            'iterations', 'failed'])
CENTRAL_WEIGHTS_AND_POINTS = {
    (1, 3): (np.array([-1., 0, 1]) / 2.0, np.arange(-1, 2)),
    (1, 5): (np.array([1., -8, 0, 8, -1]) / 12.0, np.arange(-2, 3)),
    (1, 7): (np.array([-1., 9, -45, 0, 45, -9, 1]) / 60.0, np.arange(-3, 4)),
    (1, 9): (np.array([3., -32, 168, -672, 0, 672, -168, 32, -3]) / 840.0,
             np.arange(-4, 5)),
    (2, 3): (np.array([1., -2.0, 1]), np.arange(-1, 2)),
    (2, 5): (np.array([-1., 16, -30, 16, -1]) / 12.0, np.arange(-2, 3)),
    (2, 7): (np.array([2., -27, 270, -490, 270, -27, 2]) / 180.0,
             np.arange(-3, 4)),
    (2, 9): (np.array([-9., 128, -1008, 8064, -14350,
                       8064, -1008, 128, -9]) / 5040.0,
             np.arange(-4, 5))}


def _assert(cond, msg):
    if not cond:
        raise ValueError(msg)


[docs]def fd_weights_all(x, x0=0, n=1): """ Return finite difference weights for derivatives of all orders up to n. Parameters ---------- x : vector, length m x-coordinates for grid points x0 : scalar location where approximations are to be accurate n : scalar integer highest derivative that we want to find weights for Returns ------- weights : array, shape n+1 x m contains coefficients for the j'th derivative in row j (0 <= j <= n) Notes ----- The x values can be arbitrarily spaced but must be distinct and len(x) > n. The Fornberg algorithm is much more stable numerically than regular vandermonde systems for large values of n. See also -------- fd_weights References ---------- B. Fornberg (1998) "Calculation of weights_and_points in finite difference formulas", SIAM Review 40, pp. 685-691. http://www.scholarpedia.org/article/Finite_difference_method """ m = len(x) _assert(n < m, 'len(x) must be larger than n') weights = np.zeros((m, n + 1)) _fd_weights_all(weights, x, x0, n) return weights.T
# from numba import jit, float64, int64, int32, int8, void # @jit(void(float64[:,:], float64[:], float64, int64)) def _fd_weights_all(weights, x, x0, n): m = len(x) c_1, c_4 = 1, x[0] - x0 weights[0, 0] = 1 for i in range(1, m): j = np.arange(0, min(i, n) + 1) c_2, c_5, c_4 = 1, c_4, x[i] - x0 for v in range(i): c_3 = x[i] - x[v] c_2, c_6, c_7 = c_2 * c_3, j * weights[v, j - 1], weights[v, j] weights[v, j] = (c_4 * c_7 - c_6) / c_3 weights[i, j] = c_1 * (c_6 - c_5 * c_7) / c_2 c_1 = c_2
[docs]def fd_weights(x, x0=0, n=1): """ Return finite difference weights for the n'th derivative. Parameters ---------- x : vector abscissas used for the evaluation for the derivative at x0. x0 : scalar location where approximations are to be accurate n : scalar integer order of derivative. Note for n=0 this can be used to evaluate the interpolating polynomial itself. Examples -------- >>> import numpy as np >>> import numdifftools.fornberg as ndf >>> x = np.linspace(-1, 1, 5) * 1e-3 >>> w = ndf.fd_weights(x, x0=0, n=1) >>> df = np.dot(w, np.exp(x)) >>> np.allclose(df, 1) True See also -------- fd_weights_all """ return fd_weights_all(x, x0, n)[-1]
[docs]def fd_derivative(fx, x, n=1, m=2): """ Return the n'th derivative for all points using Finite Difference method. Parameters ---------- fx : vector function values which are evaluated on x i.e. fx[i] = f(x[i]) x : vector abscissas on which fx is evaluated. The x values can be arbitrarily spaced but must be distinct and len(x) > n. n : scalar integer order of derivative. m : scalar integer defines the stencil size. The stencil size is of 2 * mm + 1 points in the interior, and 2 * mm + 2 points for each of the 2 * mm boundary points where mm = n // 2 + m. fd_derivative evaluates an approximation for the n'th derivative of the vector function f(x) using the Fornberg finite difference method. Restrictions: 0 < n < len(x) and 2*mm+2 <= len(x) Examples -------- >>> import numpy as np >>> import numdifftools.fornberg as ndf >>> x = np.linspace(-1, 1, 25) >>> fx = np.exp(x) >>> df = ndf.fd_derivative(fx, x, n=1) >>> np.allclose(df, fx) True See also -------- fd_weights """ num_x = len(x) _assert(n < num_x, 'len(x) must be larger than n') _assert(num_x == len(fx), 'len(x) must be equal len(fx)') du = np.zeros_like(fx) mm = n // 2 + m size = 2 * mm + 2 # stencil size at boundary # 2 * mm boundary points for i in range(mm): du[i] = np.dot(fd_weights(x[:size], x0=x[i], n=n), fx[:size]) du[-i - 1] = np.dot(fd_weights(x[-size:], x0=x[-i - 1], n=n), fx[-size:]) # interior points for i in range(mm, num_x - mm): du[i] = np.dot(fd_weights(x[i - mm:i + mm + 1], x0=x[i], n=n), fx[i - mm:i + mm + 1]) return du
def _circle(z, r, m): theta = np.linspace(0.0, 2.0 * np.pi, num=m, endpoint=False) return z + r * np.exp(theta * 1j) def _poor_convergence(z, r, f, bn, mvec): """ Test for poor convergence based on three function evaluations. This test evaluates the function at the three points and returns false if the relative error is greater than 1e-3. """ check_points = (-0.4 + 0.3j, 0.7 + 0.2j, 0.02 - 0.06j) diffs = [] ftests = [] for check_point in check_points: rtest = r * check_point ztest = z + rtest ftest = f(ztest) # Evaluate powerseries: comp = np.sum(bn * np.power(check_point, mvec)) ftests.append(ftest) diffs.append(comp - ftest) max_abs_error = np.max(np.abs(diffs)) max_f_value = np.max(np.abs(ftests)) return max_abs_error > 1e-3 * max_f_value def _get_logn(n): if n == 1: return 0 return np.int_(np.log2(n - 1) - 1.5849625007211561).clip(min=0) def _num_taylor_coefficients(n): """ Return number of taylor coefficients Parameters ---------- n : scalar integer Wanted number of taylor coefficients Returns ------- m : scalar integer Number of taylor coefficients calculated 8 if n <= 6 16 if 6 < n <= 12 32 if 12 < n <= 25 64 if 25 < n <= 51 128 if 51 < n <= 103 256 if 103 < n <= 192 """ _assert(n < 193, 'Number of derivatives too large. Must be less than 193') correction = np.array([0, 0, 1, 3, 4, 7])[_get_logn(n)] log2n = _get_logn(n - correction) m = 2 ** (log2n + 3) return m
[docs]def richardson_parameter(vals, k): c = np.real((vals[k - 1] - vals[k - 2]) / (vals[k] - vals[k - 1])) - 1. # The lower bound 0.07 admits the singularity x.^-0.9 c = np.maximum(c, 0.07) return -c
[docs]def richardson(vals, k, c=None): """Richardson extrapolation with parameter estimation""" if c is None: c = richardson_parameter(vals, k) return vals[k] - (vals[k] - vals[k - 1]) / c
def _extrapolate(bs, rs, m): # Begin Richardson Extrapolation. Presumably we have bs[i]'s around three # successive circles and can now extrapolate those coefficients, zeroing # out higher order error terms. nk = len(rs) extrap0 = [] extrap = [] for k in range(1, nk): extrap0.append(richardson(bs, k=k, c=1.0 - (rs[k - 1] / rs[k]) ** m)) for k in range(1, nk - 1): extrap.append(richardson(extrap0, k=k, c=1.0 - (rs[k - 1] / rs[k + 1]) ** m)) return extrap def _get_best_taylor_coefficients(bs, rs, m, max_m1m2): extrap = _extrapolate(bs, rs, m) mvec = np.arange(m) if len(extrap) > 2: all_coefs, all_errors = dea3(extrap[:-2], extrap[1:-1], extrap[2:]) steps = np.atleast_1d(rs[4:])[:, None] * mvec # pylint: disable=protected-access coefs, info = _Limit._get_best_estimate(all_coefs, all_errors, steps, (m,)) errors = info.error_estimate else: errors = EPS / np.power(rs[2], mvec) * max_m1m2() coefs = extrap[-1] return coefs, errors def _check_fft(m1, m2, check_degenerate=True): # If not degenerate, check for geometric progression in the FFT by comparing m1 and m2: # If there's an extreme mismatch, then we can consider the # geometric progression degenerate, whether one way or the other, # and just alternate directions instead of trying to target a # specific error bound (not ideal, but not a good reason to fail # catastrophically): # # Note: only consider it degenerate if we've had a chance to steer # the radius in the direction at least `min_iter` times: degenerate = check_degenerate and (m1 < m2 * 1e-8 or m2 < m1 * 1e-8) needs_smaller = np.isnan(m1) or np.isnan(m2) or m1 < m2 return degenerate, needs_smaller
[docs]class Taylor(object): """ Return Taylor coefficients of complex analytic function using FFT Parameters ---------- fun : callable function to differentiate z0 : real or complex scalar at which to evaluate the derivatives n : scalar integer, default 1 Number of taylor coefficents to compute. Maximum number is 100. r : real scalar, default 0.0059 Initial radius at which to evaluate. For well-behaved functions, the computation should be insensitive to the initial radius to within about four orders of magnitude. num_extrap : scalar integer, default 3 number of extrapolation steps used in the calculation step_ratio : real scalar, default 1.6 Initial grow/shrinking factor for finding the best radius. max_iter : scalar integer, default 30 Maximum number of iterations min_iter : scalar integer, default max_iter // 2 Minimum number of iterations before the solution may be deemed degenerate. A larger number allows the algorithm to correct a bad initial radius. full_output : bool, optional If `full_output` is False, only the coefficents is returned (default). If `full_output` is True, then (coefs, status) is returned Returns ------- coefs : ndarray array of taylor coefficents status: Optional object into which output information is written: degenerate: True if the algorithm was unable to bound the error iterations: Number of iterations executed function_count: Number of function calls final_radius: Ending radius of the algorithm failed: True if the maximum number of iterations was reached error_estimate: approximate bounds of the rounding error. Notes ----- This module uses the method of Fornberg to compute the Taylor series coefficients of a complex analytic function along with error bounds. The method uses a Fast Fourier Transform to invert function evaluations around a circle into Taylor series coefficients and uses Richardson Extrapolation to improve and bound the estimate. Unlike real-valued finite differences, the method searches for a desirable radius and so is reasonably insensitive to the initial radius-to within a number of orders of magnitude at least. For most cases, the default configuration is likely to succeed. Restrictions: The method uses the coefficients themselves to control the truncation error, so the error will not be properly bounded for functions like low-order polynomials whose Taylor series coefficients are nearly zero. If the error cannot be bounded, degenerate flag will be set to true, and an answer will still be computed and returned but should be used with caution. Examples -------- Compute the first 6 taylor coefficients 1 / (1 - z) expanded round z0 = 0: >>> import numdifftools.fornberg as ndf >>> import numpy as np >>> c, info = ndf.Taylor(lambda x: 1./(1-x), n=6, full_output=True)(z0=0) >>> np.allclose(c, np.ones(8)) True >>> np.all(info.error_estimate < 1e-9) True >>> (info.function_count, info.iterations, info.failed) == (136, 17, False) True References ---------- [1] Fornberg, B. (1981). Numerical Differentiation of Analytic Functions. ACM Transactions on Mathematical Software (TOMS), 7(4), 512-526. http://doi.org/10.1145/355972.355979 """ def __init__(self, fun, n=1, r=0.0059, num_extrap=3, step_ratio=1.6, **kwds): self.fun = fun self.max_iter = kwds.pop('max_iter', 30) self.min_iter = kwds.pop('min_iter', self.max_iter // 2) self.full_output = kwds.pop('full_output', False) self.n = n self.r = r self.num_extrap = num_extrap self.step_ratio = step_ratio def _initialize(self): m = _num_taylor_coefficients(self.n) self._step_ratio = self.step_ratio self._direction_changes = 0 self._previous_direction = None self._degenerate = self._failed = False self._m = m self._mvec = np.arange(m) # A factor for testing against the targeted geometric progression of # FFT coefficients: self._crat = m * (np.exp(np.log(1e-4) / (m - 1))) ** self._mvec self._num_changes = 0 return m, self._mvec def _get_max_m1m2(self, bn, m): m1, m2 = self._get_m1_m2(bn, m) return np.maximum(m1, m2) def _get_m1_m2(self, bn, m): # If not degenerate, check for geometric progression in the FFT: bnc = bn / self._crat m1 = np.max(np.abs(bnc[:m // 2])) m2 = np.max(np.abs(bnc[m // 2:])) return m1, m2 def _check_convergence(self, i, z0, r, m, bn): if self._direction_changes > 1 or self._degenerate: self._num_changes += 1 if self._num_changes >= 1 + self.num_extrap: return True, r if not self._degenerate: m1, m2 = self._get_m1_m2(bn, m) # Note: only consider it degenerate if we've had a chance to steer # the radius in the direction at least `min_iter` times: check_degenerate = i > self.min_iter self._degenerate, needs_smaller = _check_fft(m1, m2, check_degenerate) needs_smaller = needs_smaller or _poor_convergence(z0, r, self.fun, bn, self._mvec) if self._degenerate: needs_smaller = i % 2 == 0 if self._previous_direction is not None and needs_smaller != self._previous_direction: self._direction_changes += 1 if self._direction_changes > 0: # Once we've started changing directions, we've found our range so # start taking the square root of the growth factor so that # richardson extrapolation is well-behaved: self._step_ratio = np.sqrt(self._step_ratio) if needs_smaller: r /= self._step_ratio else: r *= self._step_ratio self._previous_direction = needs_smaller return False, r def __call__(self, z0=0): m, mvec = self._initialize() # Start iterating. The goal of this loops is to select a circle radius that # yields a nice geometric progression of the coefficients (which controls # the error), and then to accumulate *three* successive approximations as a # function of the circle radius r so that we can perform Richardson # Extrapolation and zero out error terms, *greatly* improving the quality # of the approximation. rs = [] bs = [] i = 0 r = self.r fun = self.fun for i in range(self.max_iter): # print('r = %g' % (r)) bn = np.fft.fft(fun(_circle(z0, r, m))) / m bs.append(bn * np.power(r, -mvec)) rs.append(r) converged, r = self._check_convergence(i, z0, r, m, bn) if converged: break coefs, errors = _get_best_taylor_coefficients(bs, rs, m, lambda: self._get_max_m1m2(bn, m)) if self.full_output: failed = not converged info = _INFO(errors, self._degenerate, final_radius=r, function_count=i * m, iterations=i, failed=failed) return coefs, info return coefs
[docs]def taylor(fun, z0=0, n=1, r=0.0059, num_extrap=3, step_ratio=1.6, **kwds): """ Return Taylor coefficients of complex analytic function using FFT Parameters ---------- fun : callable function to differentiate z0 : real or complex scalar at which to evaluate the derivatives n : scalar integer, default 1 Number of taylor coefficents to compute. Maximum number is 100. r : real scalar, default 0.0059 Initial radius at which to evaluate. For well-behaved functions, the computation should be insensitive to the initial radius to within about four orders of magnitude. num_extrap : scalar integer, default 3 number of extrapolation steps used in the calculation step_ratio : real scalar, default 1.6 Initial grow/shrinking factor for finding the best radius. max_iter : scalar integer, default 30 Maximum number of iterations min_iter : scalar integer, default max_iter // 2 Minimum number of iterations before the solution may be deemed degenerate. A larger number allows the algorithm to correct a bad initial radius. full_output : bool, optional If `full_output` is False, only the coefficents is returned (default). If `full_output` is True, then (coefs, status) is returned Returns ------- coefs : ndarray array of taylor coefficents status: Optional object into which output information is written: degenerate: True if the algorithm was unable to bound the error iterations: Number of iterations executed function_count: Number of function calls final_radius: Ending radius of the algorithm failed: True if the maximum number of iterations was reached error_estimate: approximate bounds of the rounding error. Notes ----- This module uses the method of Fornberg to compute the Taylor series coefficents of a complex analytic function along with error bounds. The method uses a Fast Fourier Transform to invert function evaluations around a circle into Taylor series coefficients and uses Richardson Extrapolation to improve and bound the estimate. Unlike real-valued finite differences, the method searches for a desirable radius and so is reasonably insensitive to the initial radius-to within a number of orders of magnitude at least. For most cases, the default configuration is likely to succeed. Restrictions: The method uses the coefficients themselves to control the truncation error, so the error will not be properly bounded for functions like low-order polynomials whose Taylor series coefficients are nearly zero. If the error cannot be bounded, degenerate flag will be set to true, and an answer will still be computed and returned but should be used with caution. Examples -------- Compute the first 6 taylor coefficients 1 / (1 - z) expanded round z0 = 0: >>> import numdifftools.fornberg as ndf >>> import numpy as np >>> c, info = ndf.taylor(lambda x: 1./(1-x), z0=0, n=6, full_output=True) >>> np.allclose(c, np.ones(8)) True >>> np.all(info.error_estimate < 1e-9) True >>> (info.function_count, info.iterations, info.failed) == (136, 17, False) True References ---------- [1] Fornberg, B. (1981). Numerical Differentiation of Analytic Functions. ACM Transactions on Mathematical Software (TOMS), 7(4), 512-526. http://doi.org/10.1145/355972.355979 """ return Taylor(fun, n=n, r=r, num_extrap=num_extrap, step_ratio=step_ratio, **kwds)(z0)
[docs]def derivative(fun, z0, n=1, **kwds): """ Calculate n-th derivative of complex analytic function using FFT Parameters ---------- fun : callable function to differentiate z0 : real or complex scalar at which to evaluate the derivatives n : scalar integer, default 1 Number of derivatives to compute where 0 represents the value of the function and n represents the nth derivative. Maximum number is 100. r : real scalar, default 0.0061 Initial radius at which to evaluate. For well-behaved functions, the computation should be insensitive to the initial radius to within about four orders of magnitude. max_iter : scalar integer, default 30 Maximum number of iterations min_iter : scalar integer, default max_iter // 2 Minimum number of iterations before the solution may be deemed degenerate. A larger number allows the algorithm to correct a bad initial radius. step_ratio : real scalar, default 1.6 Initial grow/shrinking factor for finding the best radius. num_extrap : scalar integer, default 3 number of extrapolation steps used in the calculation full_output : bool, optional If `full_output` is False, only the derivative is returned (default). If `full_output` is True, then (der, status) is returned `der` is the derivative, and `status` is a Results object. Returns ------- der : ndarray array of derivatives status: Optional object into which output information is written. Fields: degenerate: True if the algorithm was unable to bound the error iterations: Number of iterations executed function_count: Number of function calls final_radius: Ending radius of the algorithm failed: True if the maximum number of iterations was reached error_estimate: approximate bounds of the rounding error. Notes ----- This module uses the method of Fornberg to compute the derivatives of a complex analytic function along with error bounds. The method uses a Fast Fourier Transform to invert function evaluations around a circle into Taylor series coefficients, uses Richardson Extrapolation to improve and bound the estimate, then multiplies by a factorial to compute the derivatives. Unlike real-valued finite differences, the method searches for a desirable radius and so is reasonably insensitive to the initial radius-to within a number of orders of magnitude at least. For most cases, the default configuration is likely to succeed. Restrictions: The method uses the coefficients themselves to control the truncation error, so the error will not be properly bounded for functions like low-order polynomials whose Taylor series coefficients are nearly zero. If the error cannot be bounded, degenerate flag will be set to true, and an answer will still be computed and returned but should be used with caution. Examples -------- To compute the first five derivatives of 1 / (1 - z) at z = 0: Compute the first 6 taylor derivatives of 1 / (1 - z) at z0 = 0: >>> import numdifftools.fornberg as ndf >>> import numpy as np >>> def fun(x): ... return 1./(1-x) >>> c, info = ndf.derivative(fun, z0=0, n=6, full_output=True) >>> np.allclose(c, [1, 1, 2, 6, 24, 120, 720, 5040]) True >>> np.all(info.error_estimate < 1e-9*c.real) True >>> (info.function_count, info.iterations, info.failed) == (136, 17, False) True References ---------- [1] Fornberg, B. (1981). Numerical Differentiation of Analytic Functions. ACM Transactions on Mathematical Software (TOMS), 7(4), 512-526. http://doi.org/10.1145/355972.355979 """ result = taylor(fun, z0, n=n, **kwds) # convert taylor series --> actual derivatives. m = _num_taylor_coefficients(n) fact = factorial(np.arange(m)) if kwds.get('full_output'): coefs, info_ = result info = _INFO(info_.error_estimate * fact, *info_[1:]) return coefs * fact, info return result * fact
if __name__ == '__main__': from numdifftools.testing import test_docstrings test_docstrings()