Gradient

Computation of higher order derivatives with torch gradient function

ndmap.gradient.derivative(series: dict[tuple[int, ...], torch.Tensor], index: tuple[int, ...]) dict[tuple[int, ...], torch.Tensor][source]

Compute series derivative

Parameters:
  • series (dict[tuple[int, ...], Tensor]) – series

  • index – derivative index

Return type:

dict[tuple[int, …], Tensor]

Examples

>>> from pprint import pprint
>>> import torch
>>> def fn(x):
...    return 1.0 + x + x**2 + x**3 + x**4 + x**5
>>> x = torch.tensor([0.0])
>>> s = series((5, ), fn, x, retain=False)
>>> pprint(derivative(s, (1, )), sort_dicts=False)
{(0,): tensor([1.]),
 (1,): tensor([2.]),
 (2,): tensor([3.]),
 (3,): tensor([4.]),
 (4,): tensor([5.])}
ndmap.gradient.evaluate(series: dict[tuple[int, ...], torch.Tensor], delta: list[torch.Tensor]) torch.Tensor[source]

Evaluate series

Parameters:
  • series (Series) – input series representation

  • delta (list[Tensor]) – delta deviation

Return type:

Tensor

Examples

>>> import torch
>>> def fn(x, y, a, b):
...    x1, x2 = x
...    y1, y2 = y
...    return (a*(x1 + x2) + b*(x1**2 + x1*x2 + x2**2))*(1 + y1 + y2)
>>> x = torch.tensor([0.0, 0.0])
>>> y = torch.zeros_like(x)
>>> s = series((2, 1), fn, x, y, 1.0, 1.0, retain=False)
>>> dx = torch.tensor([1.0, 2.0])
>>> dy = torch.tensor([3.0, 4.0])
>>> fn(x + dx, y + dy, 1.0, 1.0)
tensor(80.)
>>> evaluate(s, [dx, dy])
tensor(80.)
>>> from pprint import pprint
>>> import torch
>>> def fn(x, y, a, b):
...    x1, x2 = x
...    y1, y2 = y
...    return (a*(x1 + x2) + b*(x1**2 + x1*x2 + x2**2))*(1 + y1 + y2)
>>> x = torch.tensor([0.0, 0.0])
>>> y = torch.zeros_like(x)
>>> s = series((2, 1), fn, x, y, 1.0, 1.0, retain=False)
>>> s = series((2, 1), lambda x, y: evaluate(s, [x, y]), x, y, retain=False)
>>> pprint(s, sort_dicts=False)
{(0, 0, 0, 0): tensor(0.),
 (0, 0, 1, 0): tensor(0.),
 (0, 0, 0, 1): tensor(0.),
 (1, 0, 0, 0): tensor(1.),
 (0, 1, 0, 0): tensor(1.),
 (1, 0, 1, 0): tensor(1.),
 (1, 0, 0, 1): tensor(1.),
 (0, 1, 1, 0): tensor(1.),
 (0, 1, 0, 1): tensor(1.),
 (2, 0, 0, 0): tensor(1.),
 (1, 1, 0, 0): tensor(1.),
 (0, 2, 0, 0): tensor(1.),
 (2, 0, 1, 0): tensor(1.),
 (2, 0, 0, 1): tensor(1.),
 (1, 1, 1, 0): tensor(1.),
 (1, 1, 0, 1): tensor(1.),
 (0, 2, 1, 0): tensor(1.),
 (0, 2, 0, 1): tensor(1.)}
ndmap.gradient.factor(arrays: list[tuple[int, ...]]) list[float][source]

Compute monomian factors given list of exponents

Parameters:

arrays (list[tuple[int, ...]], non-negative) – input indices

Return type:

list[float]

Examples

>>> factor([(1, 0)])
[1.0]
>>> factor([(2, 0)])
[0.5]
>>> factor([(2, 2)])
[0.25]
>>> factor([(2, 2, 2)])
[0.125]
ndmap.gradient.group(arrays: list[tuple[int, ...]], dimension: tuple[int, ...]) list[tuple[int, ...]][source]

Standard indices grouping

Parameters:
  • arrays (list[tuple[int, ...]], non-negative) – input indices

  • dimension (tuple[int, ...], positive) – input dimension

Return type:

list[tuple[int, …]]

Examples

>>> xs = [(0, 2), (1, 1), (2, 0)]
>>> group(xs, (2, ))
[(2, 0), (1, 1), (0, 2)]
>>> xs = [(0, 2, 0, 1), (0, 2, 1, 0), (1, 1, 0, 1), (1, 1, 1, 0), (2, 0, 0, 1), (2, 0, 1, 0)]
>>> group(xs, (2, 2))
[(2, 0, 1, 0), (2, 0, 0, 1), (1, 1, 1, 0), (1, 1, 0, 1), (0, 2, 1, 0), (0, 2, 0, 1)]
ndmap.gradient.hessian(function: Callable) Callable[source]

Compute function hessian

Parameters:

function (Callable) – function

Return type:

Callable

Examples

>>> import torch
>>> def fn(x):
...    x1, x2, x3, x4 = x
...    return 1.0*x1**2 + 2.0*x2**2+ 3.0*x3**2 + 4.0*x4**2
>>> x = torch.tensor([0.0, 0.0, 0.0, 0.0])
>>> torch.func.hessian(fn)(x)
tensor([[2., 0., 0., 0.],
        [0., 4., 0., 0.],
        [0., 0., 6., 0.],
        [0., 0., 0., 8.]])
>>> hessian(fn)(x).detach().permute(1, 0)
tensor([[2., 0., 0., 0.],
        [0., 4., 0., 0.],
        [0., 0., 6., 0.],
        [0., 0., 0., 8.]])
ndmap.gradient.index(dimension: tuple[int, ...], order: tuple[int, ...], *, group: Callable | None = None, signature: list[tuple[int, ...]] | None = None) list[tuple[int, ...]][source]

Generate monomial index table for a given dimension and order

Parameters:
  • dimension (tuple[int, ...], positive) – monomial dimensions

  • order (tuple[int, ...], non-negative) – derivative orders (total monomial degrees)

  • group (Optional[Callable]) – grouping function

  • signature (Optional[list[tuple[int, ...]]]) – allowed signatures

Returns:

monomial index table

Return type:

list[tuple[int, …]]

Examples

>>> index((2, ), (2, ))
[(0, 2), (1, 1), (2, 0)]
>>> index((4, ), (3, )) == index((2, 2), (2, 1))
True
>>> index((2, 2), (2, 1), signature=signature((2, 1)))
[(0, 2, 0, 1), (0, 2, 1, 0), (1, 1, 0, 1), (1, 1, 1, 0), (2, 0, 0, 1), (2, 0, 1, 0)]
>>> index((2, 2), (2, 1), signature=signature((2, 1)), group=group)
[(2, 0, 1, 0), (2, 0, 0, 1), (1, 1, 1, 0), (1, 1, 0, 1), (0, 2, 1, 0), (0, 2, 0, 1)]
ndmap.gradient.jacobian(function: Callable) Callable[source]

Compute function jacobian (can be composed)

Note, the output shape is different from jacfwd or jacrev

Parameters:

function (Callable) – function

Return type:

Callable

Examples

>>> import torch
>>> def fn(x):
...    x1, x2 = x
...    return torch.stack([1.0*x1 + 2.0*x2, 3.0*x1 + 4.0*x2])
>>> x = torch.tensor([0.0, 0.0])
>>> torch.func.jacrev(fn)(x)
tensor([[1., 2.],
        [3., 4.]])
>>> jacobian(fn)(x).detach().permute(1, 0)
tensor([[1., 2.],
        [3., 4.]])
>>> import torch
>>> def fn(x):
...    x1, x2 = x
...    y1 = torch.stack([1.0*x1 + 2.0*x2, 3.0*x1 + 4.0*x2])
...    y2 = torch.stack([5.0*x1 + 6.0*x2, 7.0*x1 + 8.0*x2])
...    return torch.stack([y1, y2])
>>> x = torch.tensor([0.0, 0.0])
>>> torch.func.jacrev(fn)(x).tolist()
[[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]]
>>> jacobian(fn)(x).detach().tolist()
[[[1.0, 3.0], [5.0, 7.0]], [[2.0, 4.0], [6.0, 8.0]]]
>>> jacobian(fn)(x).detach().permute(1, -1, 0).tolist()
[[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]]
ndmap.gradient.naught(state: torch.Tensor) torch.Tensor[source]

Infinitely differentiable zero function

Parameters:

state (Tensor) – input tensor

Return type:

Tensor

Examples

>>> import torch
>>> from torch.autograd import grad
>>> x = torch.tensor(0.0, requires_grad=True)
>>> y = (lambda x: x)(x)
>>> y, *_ = grad(y, x, retain_graph=True, create_graph=True)
>>> y
tensor(1.)
>>> y = (lambda x: x + naught(x))(x)
>>> y, *_ = grad(y, x, retain_graph=True, create_graph=True)
>>> y
tensor(1., grad_fn=<AddBackward0>)
>>> y, *_ = grad(y, x)
>>> y
tensor(-0.)
ndmap.gradient.reduce(array: tuple[int, ...]) list[tuple[int, ...]][source]

Generate direct path from given index to zero

Parameters:

array (tuple[int, ...], non-negative) – input index

Return type:

list[tuple[int, …]]

Examples

>>> reduce((4, 0))
[(4, 0), (3, 0), (2, 0), (1, 0), (0, 0)]
>>> reduce((2, 2))
[(2, 2), (1, 2), (0, 2), (0, 1), (0, 0)]
>>> reduce((2, 2, 2))
[(2, 2, 2), (1, 2, 2), (0, 2, 2), (0, 1, 2), (0, 0, 2), (0, 0, 1), (0, 0, 0)]
ndmap.gradient.scalar(function: Callable, table: tuple[int]) Callable[source]

Given f(x, y, …) and table = map(len, (x, y, …)) return g(*x, *y, …) = f(x, y, …)

Parameters:
  • function (Callable) – input function

  • table (tuple[int, ...]) – map(len, (x, y, …))

  • *pars (tuple) – passed to input function

Return type:

Callable

Examples

>>> import torch
>>> def fn(x, y):
...    x1, x2 = x
...    y1, y2, y3 = y
...    return x1*x2*y1*y2*y3
>>> def gn(x1, x2, y1, y2, y3):
...    return fn((x1, x2), (y1, y2, y3))
>>> x = torch.tensor([1, 1])
>>> y = torch.tensor([1, 1, 1])
>>> gn(*x, *y) == scalar(fn, (2, 3))(*x, *y)
tensor(True)
ndmap.gradient.select(function: ~typing.Callable, index: int, *, naught: ~typing.Callable = <function naught>) Callable[source]

Generate scalar function

Parameters:
  • function (Callable) – function

  • index (int, non-negative) – index

  • naught (Callable, default=naught) – zero function

Return type:

Callable

Examples

>>> import torch
>>> def fn(x1, x2, x3, x4):
...    return torch.stack([x1, x2, x3, x4])
>>> x = torch.tensor([1.0, 2.0, 3.0, 4.0])
>>> [select(fn, i)(*x) for i in range(4)]
[tensor(1.), tensor(2.), tensor(3.), tensor(4.)]

Note

Input fuction is assumed to have scalar tensor arguments

ndmap.gradient.series(order: tuple[int, ...], function: ~typing.Callable, *args: tuple, retain: bool = True, series: bool = True, intermediate: bool | tuple[int, ...] = True, group: ~typing.Callable = <function group>, naught: ~typing.Callable = <function naught>, shape: tuple[int, ...] | None = None) dict[tuple[int, ...], torch.Tensor][source]

Generate series representation of a given input function

c(i, j, k, …) * x**i * y**j * z**k * … => {…, (i, j, k, …) : c(i, j, k, …), …}

Note, the input function (returns a tensor) arguments are expected to be vector tensors

Parameters:
  • order (tuple[int, ...], non-negative) – maximum derivative orders

  • function (Callable) – input function

  • *args (tuple) – input function arguments

  • retain (bool, default=True) – flag to retain computation graph

  • series (bool, default=True) – flag to return series coefficiens

  • intermediate (Union[bool, tuple[int, ...]]) – flag to return indermidiate derivatives/coefficients

  • group (Callable, default=group) – indices grouping function

  • naught (Callable) – zero function

  • shape (Optional[tuple[int, ...]]) – input function output shape

Return type:

dict[tuple[int, …], Tensor]

Examples

>>> from pprint import pprint
>>> import torch
>>> def fn(x, y, a, b):
...    x1, x2 = x
...    y1, y2 = y
...    return (a*(x1 + x2) + b*(x1**2 + x1*x2 + x2**2))*(1 + y1 + y2)
>>> x = torch.tensor([0.0, 0.0])
>>> y = torch.zeros_like(x)
>>> t = series((2, 1), fn, x, y, 1.0, 1.0, retain=False, series=True, intermediate=True)
>>> pprint(t, sort_dicts=False)
{(0, 0, 0, 0): tensor(0.),
 (0, 0, 1, 0): tensor(0.),
 (0, 0, 0, 1): tensor(0.),
 (1, 0, 0, 0): tensor(1.),
 (0, 1, 0, 0): tensor(1.),
 (1, 0, 1, 0): tensor(1.),
 (1, 0, 0, 1): tensor(1.),
 (0, 1, 1, 0): tensor(1.),
 (0, 1, 0, 1): tensor(1.),
 (2, 0, 0, 0): tensor(1.),
 (1, 1, 0, 0): tensor(1.),
 (0, 2, 0, 0): tensor(1.),
 (2, 0, 1, 0): tensor(1.),
 (2, 0, 0, 1): tensor(1.),
 (1, 1, 1, 0): tensor(1.),
 (1, 1, 0, 1): tensor(1.),
 (0, 2, 1, 0): tensor(1.),
 (0, 2, 0, 1): tensor(1.)}
>>> t = series((2, 1), fn, x, y, 1.0, 1.0, retain=False, series=True, intermediate=False)
>>> pprint(t, sort_dicts=False)
{(2, 0, 1, 0): tensor(1.),
 (2, 0, 0, 1): tensor(1.),
 (1, 1, 1, 0): tensor(1.),
 (1, 1, 0, 1): tensor(1.),
 (0, 2, 1, 0): tensor(1.),
 (0, 2, 0, 1): tensor(1.)}
>>> t = series((2, 1), fn, x, y, 1.0, 1.0, retain=False, series=True, intermediate=(2, 0, 1, 0))
>>> pprint(t, sort_dicts=False)
{(2, 0, 1, 0): tensor(1.)}
>>> t = series((2, 1), fn, x, y, 1.0, 1.0, retain=False, series=False, intermediate=(2, 0, 1, 0))
>>> pprint(t, sort_dicts=False)
{(2, 0, 1, 0): tensor(2.)}
>>> t = series((2, 1), fn, x, y, 1.0, 1.0, retain=True, series=True, intermediate=(2, 0, 1, 0))
>>> pprint(t, sort_dicts=False)
{(2, 0, 1, 0): tensor(1., grad_fn=<MulBackward0>)}
ndmap.gradient.signature(order: tuple[int, ...]) list[tuple[int, ...]][source]

Compute derivative signatures from given total group orders

Parameters:

order (tuple[int, ...], non-negative) – tuple of orders

Returns:

list of signatures

Return type:

list[tuple[int, …]]

Examples

>>> signature((2, ))
[(0,), (1,), (2,)]
>>> signature((1, 2))
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]
>>> signature((2, 1))
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
>>> signature((2, 2))
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
ndmap.gradient.split(array: tuple[int, ...], chunks: tuple[int, ...]) list[tuple[int, ...]][source]

Split array into chuncks with given length

Parameters:
  • array (tuple[int, ...]) – array to split

  • chunks (tuple[int, ...], positive) – list of chunks to use

Return type:

list[tuple[int, …]]

Examples

>>> split((1, 2, 3, 4, 5), (2, 3))
[(1, 2), (3, 4, 5)]
>>> split((1, 2, 3, 4, 5), (2, 1))
[(1, 2), (3,)]
>>> split((1, 2, 3, 4, 5), (2, 1, 2))
[(1, 2), (3,), (4, 5)]