Source code for numdifftools.step_generators

"""
Step generators module
"""
from __future__ import absolute_import, division
from collections import namedtuple
import numpy as np
from numdifftools.extrapolation import EPS

_STATE = namedtuple('State', ['x', 'method', 'n', 'order'])
__all__ = ('one_step', 'make_exact', 'get_nominal_step', 'get_base_step',
           'default_scale', 'MinStepGenerator', 'MaxStepGenerator',
           'BasicMaxStepGenerator', 'BasicMinStepGenerator')


[docs]def make_exact(h): """Make sure h is an exact representable number This is important when calculating numerical derivatives and is accomplished by adding 1.0 and then subtracting 1.0. """ return (h + 1.0) - 1.0
[docs]def get_nominal_step(x=None): """Return nominal step""" if x is None: return 1.0 return np.log(1.718281828459045 + np.abs(x)).clip(min=1)
[docs]def get_base_step(scale): """Return base_step = EPS ** (1. / scale)""" return EPS ** (1. / scale)
[docs]def default_scale(method='forward', n=1, order=2): """Returns good scale for MinStepGenerator""" high_order = int(n > 1 or order >= 4) order2 = max(order // 2 - 1, 0) n_4 = n // 4 n_mod_4 = n % 4 c = ([n_4 * (10 + 1.5 * int(n > 10)), 3.65 + n_4 * (5 + 1.5 ** n_4), 3.65 + n_4 * (5 + 1.7 ** n_4), 7.30 + n_4 * (5 + 2.1 ** n_4)][n_mod_4]) if high_order else 0 return (dict(multicomplex=1.06, complex=1.06 + c).get(method, 2.5) + int(n - 1) * dict(multicomplex=0, complex=0.0).get(method, 1.3) + order2 * dict(central=3, forward=2, backward=2).get(method, 0))
[docs]class BasicMaxStepGenerator(object): """ Generates a sequence of steps of decreasing magnitude where steps = base_step * step_ratio ** (-i + offset) for i=0, 1,.., num_steps-1. Parameters ---------- base_step : float, array-like. Defines the start step, i.e., maximum step step_ratio : real scalar. Ratio between sequential steps generated. Note: Ratio > 1 num_steps : scalar integer. defines number of steps generated. offset : real scalar, optional, default 0 offset to the base step Examples -------- >>> from numdifftools.step_generators import BasicMaxStepGenerator >>> step_gen = BasicMaxStepGenerator(base_step=2.0, step_ratio=2, ... num_steps=4) >>> [s for s in step_gen()] [2.0, 1.0, 0.5, 0.25] """ _sign = -1
[docs] def __init__(self, base_step, step_ratio, num_steps, offset=0): self.base_step = base_step self.step_ratio = step_ratio self.num_steps = num_steps self.offset = offset
def _range(self): return range(self.num_steps) def __call__(self): base_step, step_ratio = self.base_step, self.step_ratio sgn, offset = self._sign, self.offset for i in self._range(): step = base_step * step_ratio ** (sgn * i + offset) if (np.abs(step) > 0).all(): yield step
[docs]class BasicMinStepGenerator(BasicMaxStepGenerator): """ Generates a sequence of steps of decreasing magnitude where steps = base_step * step_ratio ** (i + offset), i=num_steps-1,... 1, 0. Parameters ---------- base_step : float, array-like. Defines the end step, i.e., minimum step step_ratio : real scalar. Ratio between sequential steps generated. Note: Ratio > 1 num_steps : scalar integer. defines number of steps generated. offset : real scalar, optional, default 0 offset to the base step Examples -------- >>> from numdifftools.step_generators import BasicMinStepGenerator >>> step_gen = BasicMinStepGenerator(base_step=0.25, step_ratio=2, ... num_steps=4) >>> [s for s in step_gen()] [2.0, 1.0, 0.5, 0.25] """ _sign = 1 def _range(self): return range(self.num_steps - 1, -1, -1)
[docs]class MinStepGenerator(object): """ Generates a sequence of steps where steps = step_nom * base_step * step_ratio ** (i + offset) for i = num_steps-1,... 1, 0. Parameters ---------- base_step : float, array-like, optional Defines the minimum step, if None, the value is set to EPS**(1/scale) step_ratio : real scalar, optional, default 2 Ratio between sequential steps generated. Note: Ratio > 1 If None then step_ratio is 2 for n=1 otherwise step_ratio is 1.6 num_steps : scalar integer, optional, default min_num_steps + num_extrap defines number of steps generated. It should be larger than min_num_steps = (n + order - 1) / fact where fact is 1, 2 or 4 depending on differentiation method used. step_nom : default maximum(log(exp(1)+|x|), 1) Nominal step where x is supplied at runtime through the __call__ method. offset : real scalar, optional, default 0 offset to the base step num_extrap : scalar integer, default 0 number of points used for extrapolation check_num_steps : boolean, default True If True make sure num_steps is larger than the minimum required steps. use_exact_steps : boolean, default True If true make sure exact steps are generated scale : real scalar, optional scale used in base step. If not None it will override the default computed with the default_scale function. """ _step_generator = BasicMinStepGenerator
[docs] def __init__(self, base_step=None, step_ratio=None, num_steps=None, step_nom=None, offset=0, num_extrap=0, use_exact_steps=True, check_num_steps=True, scale=None): self.base_step = base_step self.step_nom = step_nom self.num_steps = num_steps self.step_ratio = step_ratio self.offset = offset self.num_extrap = num_extrap self.check_num_steps = check_num_steps self.use_exact_steps = use_exact_steps self.scale = scale self._state = _STATE(x=np.asarray(1), method='forward', n=1, order=2)
def __repr__(self): class_name = self.__class__.__name__ kwds = ['{0!s}={1!s}'.format(name, str(getattr(self, name))) for name in self.__dict__] return """{0!s}({1!s})""".format(class_name, ','.join(kwds)) @property def scale(self): """Scale used in base step.""" if self._scale is None: _unused_x, method, n, order = self._state return default_scale(method, n, order) return self._scale @scale.setter def scale(self, scale): self._scale = scale @property def base_step(self): """Base step defines the minimum or maximum step when offset==0.""" if self._base_step is None: return get_base_step(self.scale) return self._base_step @base_step.setter def base_step(self, base_step): self._base_step = base_step @staticmethod def _num_step_divisor(method, n, order): complex_divisior = 4 if (n > 1 or order >= 4) else 2 return dict(central=2, central2=2, complex=complex_divisior, multicomplex=2).get(method, 1) @property def min_num_steps(self): """Minimum number of steps required given the differentiation method and order.""" _unused_x, method, n, order = self._state num_steps = int(n + order - 1) divisor = self._num_step_divisor(method, n, order) return max(num_steps // divisor, 1) @property def num_steps(self): """Defines number of steps generated""" min_num_steps = self.min_num_steps if self._num_steps is not None: num_steps = int(self._num_steps) if self.check_num_steps: num_steps = max(num_steps, min_num_steps) return num_steps return min_num_steps + int(self.num_extrap) @num_steps.setter def num_steps(self, num_steps): self._num_steps = num_steps @property def step_ratio(self): """Ratio between sequential steps generated""" step_ratio = self._step_ratio if step_ratio is None: step_ratio = {1: 2.0}.get(self._state.n, 1.6) return float(step_ratio) @step_ratio.setter def step_ratio(self, step_ratio): self._step_ratio = step_ratio @property def step_nom(self): """Nominal step""" x = self._state.x if self._step_nom is None: return get_nominal_step(x) return np.full(x.shape, fill_value=self._step_nom) @step_nom.setter def step_nom(self, step_nom): self._step_nom = step_nom
[docs] def step_generator_function(self, x, method='forward', n=1, order=2): """Step generator function""" self._state = _STATE(np.asarray(x), method, n, order) base_step, step_ratio = self.base_step * self.step_nom, self.step_ratio if self.use_exact_steps: base_step = make_exact(base_step) step_ratio = make_exact(step_ratio) return self._step_generator(base_step=base_step, step_ratio=step_ratio, num_steps=self.num_steps, offset=self.offset)
def __call__(self, x, method='forward', n=1, order=2): step_generator = self.step_generator_function(x, method, n, order) return step_generator()
[docs]class MaxStepGenerator(MinStepGenerator): """ Generates a sequence of steps where steps = step_nom * base_step * step_ratio ** (-i + offset) for i = 0, 1, ..., num_steps-1. Parameters ---------- base_step : float, array-like, default 2.0 Defines the maximum step, if None, the value is set to EPS**(1/scale) step_ratio : real scalar, optional, default 2 or 1.6 Ratio between sequential steps generated. Note: Ratio > 1 If None then step_ratio is 2 for n=1 otherwise step_ratio is 1.6 num_steps : scalar integer, optional, default min_num_steps + num_extrap defines number of steps generated. It should be larger than min_num_steps = (n + order - 1) / fact where fact is 1, 2 or 4 depending on differentiation method used. step_nom : default maximum(log(exp(1)+|x|), 1) Nominal step where x is supplied at runtime through the __call__ method. offset : real scalar, optional, default 0 offset to the base step num_extrap : scalar integer, default 0 number of points used for extrapolation check_num_steps : boolean, default True If True make sure num_steps is larger than the minimum required steps. use_exact_steps : boolean, default True If true make sure exact steps are generated scale : real scalar, default 500 scale used in base step. """ _step_generator = BasicMaxStepGenerator
[docs] def __init__(self, base_step=2.0, step_ratio=None, num_steps=15, step_nom=None, offset=0, num_extrap=9, use_exact_steps=False, check_num_steps=True, scale=500): super(MaxStepGenerator, self).__init__(base_step=base_step, step_ratio=step_ratio, num_steps=num_steps, step_nom=step_nom, offset=offset, num_extrap=num_extrap, use_exact_steps=use_exact_steps, check_num_steps=check_num_steps, scale=scale)
one_step = MinStepGenerator(num_steps=1, scale=None, step_nom=None) if __name__ == '__main__': from numdifftools.testing import test_docstrings test_docstrings()