Source code for fpmlib.projections

#!/usr/bin/env python3
"""
Metric projections
------------------

There are some convex sets onto which the metric projections can be computed explicitly.
``fpmlib.projections`` module provides them.
The following items are automatically loaded when ``fpmlib`` package is imported.
"""

import numpy as np
from typing import Optional, Union
from .typing import MetricProjection
__all__ = ['Box', 'HalfSpace', 'Ball']


[docs]class Box(MetricProjection): r""" The metric projection onto the orthotope defined with its lower and upper bound of each dimension. For given :math:`(\mathbf{lb}_i)_{i=1}^N\in\mathbb{R}^N` and :math:`(\mathbf{ub}_i)_{i=1}^N\in\mathbb{R}^N``, the fixed point set of the created mapping :math:`T` is .. math:: \mathrm{Fix}(T)=\{(x_i)_{i=1}^N\in\mathbb{R}^N:\mathbf{lb}_i\le x_i\le\mathbf{ub}_i\ (i=1,2,\ldots,N)\}. For any ``ndarray`` vector :math:`x`, :math:`T(x)` is equivalent to :math:`\mathtt{np.clip}(x, \mathbf{lb}, \mathbf{ub})`. :param lb: An ``ndarray`` vector whose element expresses the lower bound corresponding to each dimension. If a ``float`` value is specified, it is dealt with as the vector whose all elements are set as the given value. If ``None`` is specified, the fixed point set of the created mapping is unbounded below. :param ub: An ``ndarray`` vector whose element expresses the upper bound corresponding to each dimension. If a ``float`` value is specified, it is dealt with as the vector whose all elements are set as the given value. If ``None`` is specified, the fixed point set of the created mapping is unbounded above. """ @property def ndim(self): if isinstance(self._lb, np.ndarray): return self._lb.shape[0] if isinstance(self._ub, np.ndarray): return self._ub.shape[0] return None def __init__(self, lb: Optional[Union[np.ndarray, float]] = None, ub: Optional[Union[np.ndarray, float]] = None): if isinstance(lb, np.ndarray) and isinstance(ub, np.ndarray) and lb.shape != ub.shape: raise ValueError('Vectors lb and ub must have the same number of dimensions') if isinstance(lb, np.ndarray): lb = lb.copy() if isinstance(ub, np.ndarray): ub = ub.copy() self._lb = lb self._ub = ub def __call__(self, x): return np.clip(x, self._lb, self._ub) def __contains__(self, x): if not isinstance(x, np.ndarray): return False if self._lb is not None: if isinstance(self._lb, np.ndarray) and self._lb.shape != x.shape: return False if not (self._lb <= x).all(): return False if self._ub is not None: if isinstance(self._ub, np.ndarray) and self._ub.shape != x.shape: return False if not (x <= self._ub).all(): return False return True
[docs]class HalfSpace(MetricProjection): r""" The metric projection :math:`P_H` onto the closed half-space .. math:: H:=\{x\in\mathbb{R}^N:\langle w, x\rangle\le d\}, where :math:`w\in\mathbb{R}^N\setminus\{0\}` and :math:`d\in\mathbb{R}`. :param w: An ``ndarray`` vector which defines the half-space as its parameter :math:`w`. :param d: A ``float`` value which defines the half-space as its parameter :math:`d`. """ @property def ndim(self): return self._w.shape[0] def __init__(self, w: np.ndarray, d: float): if not isinstance(w, np.ndarray) or len(w.shape) != 1: raise ValueError('Parameter w must be a vector.') l = np.linalg.norm(w) if l == 0: raise ValueError('Parameter w must be a nonzero vector.') self._w = w / l self._d = d / l def __call__(self, x): det = self._d - np.inner(self._w, x) if det >= 0: y = x.copy() else: y = det * self._w y += x return y def __contains__(self, x): if not isinstance(x, np.ndarray) or x.shape != self._w.shape: return False return (self._d - np.inner(self._w, x)) >= 0
[docs]class Ball(MetricProjection): r""" The metric projection :math:`P_B` onto the closed ball with center :math:`c\in\mathbb{R}^N` and radius :math:`r\in\mathbb{R}`, that is .. math:: B:=\{x\in\mathbb{R}^N:\|x-c\|\le r\}. :param c: An ``ndarray`` vector which expresses the center of the closed ball. :param r: A ``float`` value which expresses the radius of the closed ball. """ @property def ndim(self) -> int: return self._c.shape[0] def __init__(self, c: np.ndarray, r: float): if r < 0: raise ValueError('Parameter `r` must be a positive real.') if not isinstance(c, np.ndarray) or len(c.shape) != 1: raise ValueError('Parameter `c` must be a vector.') self._c = c.copy() self._r = r def __call__(self, x): v = x - self._c d = np.linalg.norm(v) if d <= self._r: return x.copy() else: v *= self._r / d v += self._c return v def __contains__(self, x): if not isinstance(x, np.ndarray) or x.shape != self._c.shape: return False return np.linalg.norm(x - self._c) <= self._r