#
# This file is part of TransportMaps.
#
# TransportMaps is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# TransportMaps is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with TransportMaps. If not, see <http://www.gnu.org/licenses/>.
#
# Transport Maps Library
# Copyright (C) 2015-2018 Massachusetts Institute of Technology
# Uncertainty Quantification group
# Department of Aeronautics and Astronautics
#
# Authors: Transport Map Team
# Website: transportmaps.mit.edu
# Support: transportmaps.mit.edu/qa/
#
from ..Misc import required_kwargs, counted, cached, cached_tuple
from ..ObjectBase import TMO
from ..DerivativesChecks import \
fd_gradient_check, \
action_hess_check
__all__ = ['Map']
[docs]class Map(TMO):
r""" Abstract map :math:`T:\mathbb{R}^{d_x}\rightarrow\mathbb{R}^{d_y}`
Args:
dim_in (int): input dimension
dim_out (int): output dimension
"""
@required_kwargs('dim_in', 'dim_out')
def __init__(self, **kwargs):
r"""
Kwargs:
dim_in (int): input dimension :math:`d_x`
dim_out (int): output dimension :math:`d_y`
"""
super(Map, self).__init__()
self._dim_in = kwargs['dim_in']
self._dim_out = kwargs['dim_out']
@property
[docs] def dim_in(self):
try:
self._dim_in
except AttributeError:
# backward compatibility
self._dim_in = vars(self)['dim_in']
del vars(self)['dim_in']
finally:
return self._dim_in
@dim_in.setter
def dim_in(self, dim_in):
self._dim_in = dim_in
vars(self).pop('dim_in', None)
@property
[docs] def dim_out(self):
try:
self._dim_out
except AttributeError:
# backward compatibility
self._dim_out = vars(self)['dim_out']
del vars(self)['dim_out']
finally:
return self._dim_out
@dim_out.setter
def dim_out(self, dim_out):
self._dim_out = dim_out
vars(self).pop('dim_out', None)
@property
[docs] def dim(self):
vars(self).pop('dim', None)
if self.dim_in == self.dim_out:
return self.dim_in
else:
return None
[docs] def __call__(self, x):
r"""
Calls :func:`evaluate`.
"""
return self.evaluate( x )
[docs] def get_ncalls_tree(self, indent=""):
out = super(Map, self).get_ncalls_tree(indent)
return out
[docs] def get_nevals_tree(self, indent=""):
out = super(Map, self).get_nevals_tree(indent)
return out
[docs] def get_teval_tree(self, indent=""):
out = super(Map, self).get_teval_tree(indent)
return out
[docs] def update_ncalls_tree(self, obj):
super(Map, self).update_ncalls_tree( obj )
[docs] def update_nevals_tree(self, obj):
super(Map, self).update_nevals_tree( obj )
[docs] def update_teval_tree(self, obj):
super(Map, self).update_teval_tree( obj )
[docs] def reset_counters(self):
super(Map, self).reset_counters()
@cached()
@counted
[docs] def evaluate(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the map :math:`T` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached()
@counted
[docs] def grad_x(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the gradient :math:`\nabla_{\bf x}T` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,d_x`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached()
@counted
[docs] def action_grad_x(self, x, dx, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the action of the gradient :math:`\langle\nabla_{\bf x}T({\bf x}),\delta{\bf x}\rangle` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}` on the vector :math:`\delta{\bf x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
dx (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x,...`]): vector :math:`\delta{\bf x}`
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,...`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached()
@counted
[docs] def action_adjoint_grad_x(self, x, dx, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the action of the gradient :math:`\langle\delta{\bf x},\nabla_{\bf x}T({\bf x})\rangle` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}` on the vector :math:`\delta{\bf x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
dx (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x,...`]): vector :math:`\delta{\bf x}`
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,...`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached_tuple(['evaluate','grad_x'])
@counted
[docs] def tuple_grad_x(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the function and gradient.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`tuple`) -- function and gradient evaluation
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached_tuple(['evaluate','grad_x'])
@counted
[docs] def action_tuple_grad_x(self, x, dx, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the function and action of the gradient.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
dx (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x,...`]): vector :math:`\delta{\bf x}`
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`tuple`) -- function and action of the gradient evaluation
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached(caching=False)
@counted
[docs] def hess_x(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the Hessian :math:`\nabla^2_{\bf x}T` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,d_x,d_x`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached(caching=False)
@counted
[docs] def action_hess_x(self, x, dx, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the action of the Hessian :math:`\langle\nabla^2_{\bf x}T,\delta{\bf x}\rangle` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
dx (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): direction
on which to evaluate the Hessian
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,d_x`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@counted
[docs] def inverse(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the Moore-Penrose inverse map :math:`T^\dagger` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@counted
[docs] def grad_x_inverse(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the gradient of the Moore-Penrose inverse :math:`\nabla_{\bf x}T^\dagger` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,d_x`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@counted
[docs] def tuple_grad_x_inverse(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the Moore-Penrose inverse function and gradient.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`tuple`) -- function and gradient evaluation
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@counted
[docs] def hess_x_inverse(self, x, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the Hessian of the Moore-Penrose inverse :math:`\nabla^2_{\bf x}T^\dagger` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,d_x,d_x`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
@cached(caching=False)
@counted
[docs] def action_hess_x_inverse(self, x, dx, precomp=None, idxs_slice=slice(None), **kwargs):
r""" [Abstract] Evaluate the action of the Hessian of the Moore-Penrose inverse :math:`\langle\nabla^2_{\bf x}T^\dagger,\delta{\bf x}\rangle` at the points :math:`{\bf x} \in \mathbb{R}^{m \times d_x}`.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): evaluation points
dx (:class:`ndarray<numpy.ndarray>` [:math:`m,d_x`]): direction
on which to evaluate the Hessian
precomp (:class:`dict<dict>`): dictionary of precomputed values
idxs_slice (slice): if precomputed values are present, this parameter
indicates at which of the points to evaluate. The number of indices
represented by ``idxs_slice`` must match ``x.shape[0]``.
Returns:
(:class:`ndarray<numpy.ndarray>` [:math:`m,d_y,d_x`]) -- transformed points
Raises:
NotImplementedError: to be implemented in sub-classes
"""
raise NotImplementedError("To be implemented in sub-classes")
[docs] def test_gradients(
self,
x,
v=None,
method='fd',
fd_dx=1e-4,
verbose=True,
):
r""" Automatically tests all the gradients implemented.
Args:
x (:class:`ndarray<numpy.ndarray>` [:math:`m,d`]): evaluation points
v (:class:`ndarray<numpy.ndarray>` [:math:`d`]): direction
on which to evaluate the action of the Hessian
method (str): method to use for testing. Default ``fd`` is finite difference
fd_dx (float): finite difference perturbation
Returns:
:class:`bool` -- whether all the implemented gradients pass the test.
"""
success = True
has_grad = True
has_tuple_grad = True
has_hess = True
if method == 'fd':
try:
success &= fd_gradient_check(
self.evaluate, self.grad_x, x, fd_dx,
title='grad_x from evaluate', verbose=verbose)
except NotImplementedError:
has_grad = False
try:
success &= fd_gradient_check(
self.tuple_grad_x, None, x, fd_dx,
title='tuple_grad_x from evaluate', verbose=verbose)
except NotImplementedError:
print("not implemented")
has_tuple_grad = False
if has_grad or has_tuple_grad:
gx = self.grad_x if has_grad else self.tuple_grad_x
try:
success &= fd_gradient_check(
gx, self.hess_x, x, fd_dx,
title='hess_x from grad_x', verbose=verbose)
except NotImplementedError:
has_hess = False
if has_hess and v is not None:
try:
success &= action_hess_check(
self.hess_x, self.action_hess_x, x, v,
title='action_hess_x from hess_x', verbose=verbose)
except NotImplementedError:
pass
elif (has_grad or has_tuple_grad) and v is not None:
gx = self.grad_x if has_grad else self.tuple_grad_x
try:
success &= action_hess_check(
gx, self.action_hess_x, x, v,
fd_dx=fd_dx,
title='action_hess_x from grad_x', verbose=verbose)
except NotImplementedError:
pass
else:
raise NotImplementedError("Testing method not implemented")
return success