Source code for cofi.utils._reg_base

from abc import abstractmethod, ABCMeta
from numbers import Number
from functools import reduce
import numpy as np

from .._exceptions import DimensionMismatchError


[docs] class BaseRegularization(metaclass=ABCMeta): r"""Base class for a regularization term Check :class:`QuadraticReg` for a concrete example. .. rubric:: Basic interface The basic properties / methods for a regularization term in ``cofi.utils`` include the following: .. autosummary:: BaseRegularization.model_size BaseRegularization.reg BaseRegularization.gradient BaseRegularization.hessian BaseRegularization.__call__ .. rubric:: Adding two terms Two instances of ``BaseRegularization`` can also be added together using the ``+`` operator: .. autosummary:: BaseRegularization.__add__ """ def __init__( self, ): pass @property @abstractmethod def model_shape(self) -> tuple: """the shape of models that current regularization function accepts""" raise NotImplementedError @property def model_size(self) -> Number: """the number of unknowns that current regularization function accepts""" return reduce(lambda a, b: a * b, np.array(self.model_shape), 1)
[docs] def __call__(self, model: np.ndarray) -> Number: r"""a class instance itself can also be called as a function It works exactly the same as :meth:`reg`. In other words, the following two usages are exactly the same:: >>> my_reg = QuadraticReg(factor=1, model_size=3) >>> my_reg_value = my_reg(np.array([1,2,3])) # usage 1 >>> my_reg_value = my_reg.reg(np.array([1,2,3])) # usage 2 """ return self.reg(model)
[docs] @abstractmethod def reg(self, model: np.ndarray) -> Number: """the regularization function value given a model to evaluate""" raise NotImplementedError
[docs] @abstractmethod def gradient(self, model: np.ndarray) -> np.ndarray: """the gradient of regularization function with respect to model given a model The usual size for the gradient is :math:`(M,)` where :math:`M` is the number of model parameters """ raise NotImplementedError
[docs] @abstractmethod def hessian(self, model: np.ndarray) -> np.ndarray: """the hessian of regularization function with respect to model given a model The usual size for the Hessian is :math:`(M,M)` where :math:`M` is the number of model parameters """ raise NotImplementedError
[docs] def __add__(self, other_reg): r"""Adds two regularization terms Parameters ---------- other_reg : BaseRegularization the second argument of "+" operator; must also be a :class:`BaseRegularization` instance Returns ------- BaseRegularization a regularization term ``resRegularization`` such that: - :math:`\text{resRegularization.reg}(m)=\text{self.reg}(m)+\text{other_reg.reg}(m)` - :math:`\text{resRegularization.gradient}(m)=\text{self.gradient}(m)+\text{other_reg.gradient}(m)` - :math:`\text{resRegularization.hessian}(m)=\text{self.hessian}(m)+\text{other_reg.hessian}(m)` Raises ------ TypeError when the ``other_reg`` is not a regularization term generated by CoFI Utils DimensionMismatchError when the ``other_reg`` doesn't accept model_size that matches the one of ``self`` Examples -------- >>> from cofi import BaseProblem >>> from cofi.utils import QuadraticReg >>> reg1 = QuadraticReg(model_shape=(3,), weighting_matrix="damping") >>> reg2 = QuadraticReg(model_shape=(3,), weighting_matrix="smoothing") >>> my_problem = BaseProblem() >>> my_problem.set_regularization(reg1 + reg2) """ if not isinstance(other_reg, BaseRegularization): raise TypeError( f"unsupported operand type(s) for +: '{self.__class__.__name__}' " f"and '{other_reg.__class__.__name__}" ) if self.model_size != other_reg.model_size: raise DimensionMismatchError( entered_name="the second regularization term", entered_dimension=other_reg.model_size, expected_source="the first regularization term", expected_dimension=self.model_size, ) return CompositeRegularization(self, other=other_reg)
[docs] def __rmul__(self, coefficient): r"""Multiply a regularization term with a constant number Parameters ---------- coefficient : Number the first argument of "*" operator; must be a Number Returns ------- BaseRegularization a regularization term ``resRegularization`` such that: - :math:`\text{resRegularization.reg}(m)=\text{coefficient}\times\text{self.reg}(m)` - :math:`\text{resRegularization.gradient}(m)=\text{coefficient}\times\text{self.gradient}(m)` - :math:`\text{resRegularization.hessian}(m)=\text{coefficient}\times\text{self.hessian}(m)` Raises ------ TypeError when the ``coefficient`` is not of a python Number type Examples -------- >>> from cofi import BaseProblem >>> from cofi.utils import QuadraticReg >>> reg = QuadraticReg(model_shape=(3,), weighting_matrix="damping") >>> my_problem = BaseProblem() >>> my_problem.set_regularization(10 * reg) """ if not isinstance(coefficient, Number): raise TypeError( f"unsupported operand type(s) for *: '{coefficient.__class__.__name__}'" f" and '{self.__class__.__name__}" ) return CompositeRegularization(self, coefficient=coefficient)
class CompositeRegularization(BaseRegularization): def __init__(self, base, other=None, coefficient=None): self.base = base self.other = other self.coefficient = coefficient @property def model_shape(self): return self.base.model_shape def reg(self, model): if self.other: return self.base.reg(model) + self.other.reg(model) elif self.coefficient is not None: return self.coefficient * self.base.reg(model) def gradient(self, model): if self.other: return self.base.gradient(model) + self.other.gradient(model) elif self.coefficient is not None: return self.coefficient * self.base.gradient(model) def hessian(self, model): if self.other: return self.base.hessian(model) + self.other.hessian(model) elif self.coefficient is not None: return self.coefficient * self.base.hessian(model)