"""
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.0 / 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 (
{"multicomplex": 1.06, "complex": 1.06 + c}.get(method, 2.5)
+ int(n - 1) * {"multicomplex": 0, "complex": 0.0}.get(method, 1.3)
+ order2 * {"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 {"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()