@@ -0,0 +1,6 @@
 1 + """Callables with in- and output shape information supporting algebraic operations.""" 2 + 3 + from . import _algebra 4 + from ._algebra_fallbacks import ScaledFunction, SumFunction 5 + from ._function import Function, LambdaFunction 6 + from ._zero import Zero

@@ -7,7 +7,7 @@
 7 7 8 8 import numpy as np 9 9 10 - from probnum import _function, randvars, utils as _utils 10 + from probnum import functions, randvars, utils as _utils 11 11 from probnum.randprocs import kernels 12 12 from probnum.typing import DTypeLike, ShapeLike, ShapeType 13 13
@@ -56,7 +56,7 @@
 56 56  input_shape: ShapeLike, 57 57  output_shape: ShapeLike, 58 58  dtype: DTypeLike, 59 -  mean: Optional[_function.Function] = None, 59 +  mean: Optional[functions.Function] = None, 60 60  cov: Optional[kernels.Kernel] = None, 61 61  ): 62 62  self._input_shape = _utils.as_shape(input_shape)
@@ -75,7 +75,7 @@
 75 75 76 76  # Mean function 77 77  if mean is not None: 78 -  if not isinstance(mean, _function.Function): 78 +  if not isinstance(mean, functions.Function): 79 79  raise TypeError("The mean function must have type probnum.Function.") 80 80 81 81  if mean.input_shape != self._input_shape:
@@ -177,7 +177,7 @@
 177 177  raise NotImplementedError 178 178 179 179  @property 180 -  def mean(self) -> _function.Function: 180 +  def mean(self) -> functions.Function: 181 181  r"""Mean function :math:m(x) := \mathbb{E}[f(x)] of the random process.""" 182 182  if self._mean is None: 183 183  raise NotImplementedError

@@ -0,0 +1,69 @@
 1 + r"""Algebraic operations on :class:Function\ s.""" 2 + 3 + from ._algebra_fallbacks import SumFunction 4 + from ._function import Function 5 + from ._zero import Zero 6 + 7 + ############ 8 + # Function # 9 + ############ 10 + 11 + 12 + @Function.__add__.register # pylint: disable=no-member 13 + def _(self, other: Function) -> SumFunction: 14 +  return SumFunction(self, other) 15 + 16 + 17 + @Function.__add__.register # pylint: disable=no-member 18 + def _(self, other: SumFunction) -> SumFunction: 19 +  return SumFunction(self, *other.summands) 20 + 21 + 22 + @Function.__add__.register # pylint: disable=no-member 23 + def _(self, other: Zero) -> Function: # pylint: disable=unused-argument 24 +  return self 25 + 26 + 27 + @Function.__sub__.register # pylint: disable=no-member 28 + def _(self, other: Function) -> SumFunction: 29 +  return SumFunction(self, -other) 30 + 31 + 32 + @Function.__sub__.register # pylint: disable=no-member 33 + def _(self, other: Zero) -> Function: # pylint: disable=unused-argument 34 +  return self 35 + 36 + 37 + ############### 38 + # SumFunction # 39 + ############### 40 + 41 + 42 + @SumFunction.__add__.register # pylint: disable=no-member 43 + def _(self, other: Function) -> SumFunction: 44 +  return SumFunction(*self.summands, other) 45 + 46 + 47 + @SumFunction.__add__.register # pylint: disable=no-member 48 + def _(self, other: SumFunction) -> SumFunction: 49 +  return SumFunction(*self.summands, *other.summands) 50 + 51 + 52 + @SumFunction.__sub__.register # pylint: disable=no-member 53 + def _(self, other: Function) -> SumFunction: 54 +  return SumFunction(*self.summands, -other) 55 + 56 + 57 + ######## 58 + # Zero # 59 + ######## 60 + 61 + 62 + @Zero.__add__.register # pylint: disable=no-member 63 + def _(self, other: Function) -> Function: # pylint: disable=unused-argument 64 +  return other 65 + 66 + 67 + @Zero.__sub__.register # pylint: disable=no-member 68 + def _(self, other: Function) -> Function: # pylint: disable=unused-argument 69 +  return -other

@@ -6,7 +6,7 @@
 6 6 functions with stochastic output. 7 7 """ 8 8 9 - from . import kernels, mean_fns 9 + from . import kernels 10 10 from ._gaussian_process import GaussianProcess 11 11 from ._random_process import RandomProcess 12 12

@@ -5,7 +5,7 @@
 5 5 import numpy as np 6 6 import scipy.stats 7 7 8 - from probnum import _function, randvars, utils 8 + from probnum import functions, randvars, utils 9 9 from probnum.randprocs import _random_process, kernels 10 10 from probnum.randprocs.markov import _transition, continuous, discrete 11 11 from probnum.typing import ShapeLike
@@ -31,7 +31,7 @@
 31 31  input_shape=input_shape, 32 32  output_shape=output_shape, 33 33  dtype=np.dtype(np.float_), 34 -  mean=_function.LambdaFunction( 34 +  mean=functions.LambdaFunction( 35 35  lambda x: self.__call__(args=x).mean, 36 36  input_shape=input_shape, 37 37  output_shape=output_shape,

@@ -3,12 +3,13 @@
 3 3 from __future__ import annotations 4 4 5 5 import abc 6 + import functools 6 7 from typing import Callable 7 8 8 9 import numpy as np 9 10 10 - from . import utils 11 - from .typing import ArrayLike, ShapeLike, ShapeType 11 + from probnum import utils 12 + from probnum.typing import ArrayLike, ShapeLike, ShapeType 12 13 13 14 14 15 class Function(abc.ABC):
@@ -17,6 +18,8 @@
 17 18  This class represents a, uni- or multivariate, scalar- or tensor-valued, 18 19  mathematical function. Hence, the call method should not have any observable 19 20  side-effects. 21 +  Instances of this class can be added and multiplied by a scalar, which means that 22 +  they are elements of a vector space. 20 23 21 24  Parameters 22 25  ----------
@@ -29,7 +32,7 @@
 29 32  See Also 30 33  -------- 31 34  LambdaFunction : Define a :class:Function from an anonymous function. 32 -  ~probnum.randprocs.mean_fns.Zero : Zero mean function of a random process. 35 +  ~probnum.functions.Zero : Zero function. 33 36  """ 34 37 35 38  def __init__(self, input_shape: ShapeLike, output_shape: ShapeLike = ()) -> None:
@@ -112,6 +115,39 @@
 112 115  def _evaluate(self, x: np.ndarray) -> np.ndarray: 113 116  pass 114 117 118 +  def __neg__(self): 119 +  return -1.0 * self 120 + 121 +  @functools.singledispatchmethod 122 +  def __add__(self, other): 123 +  return NotImplemented 124 + 125 +  @functools.singledispatchmethod 126 +  def __sub__(self, other): 127 +  return NotImplemented 128 + 129 +  @functools.singledispatchmethod 130 +  def __mul__(self, other): 131 +  if np.ndim(other) == 0: 132 +  from ._algebra_fallbacks import ( # pylint: disable=import-outside-toplevel 133 +  ScaledFunction, 134 +  ) 135 + 136 +  return ScaledFunction(function=self, scalar=other) 137 + 138 +  return NotImplemented 139 + 140 +  @functools.singledispatchmethod 141 +  def __rmul__(self, other): 142 +  if np.ndim(other) == 0: 143 +  from ._algebra_fallbacks import ( # pylint: disable=import-outside-toplevel 144 +  ScaledFunction, 145 +  ) 146 + 147 +  return ScaledFunction(function=self, scalar=other) 148 + 149 +  return NotImplemented 150 + 115 151 116 152 class LambdaFunction(Function): 117 153  """Define a :class:Function from a given :class:callable.
@@ -131,7 +167,7 @@
 131 167  Examples 132 168  -------- 133 169  >>> import numpy as np 134 -  >>> from probnum import LambdaFunction 170 +  >>> from probnum.functions import LambdaFunction 135 171  >>> fn = LambdaFunction(fn=lambda x: 2 * x + 1, input_shape=(2,), output_shape=(2,)) 136 172  >>> fn(np.array([[1, 2], [4, 5]])) 137 173  array([[ 3, 5], 138 174 imilarity index 53% 139 175 ename from src/probnum/randprocs/mean_fns.py 140 176 ename to src/probnum/functions/_zero.py

@@ -1,10 +1,10 @@
 1 - """Mean functions of random processes.""" 1 + """The zero function.""" 2 2 3 - import numpy as np 3 + import functools 4 4 5 - from .. import _function 5 + import numpy as np 6 6 7 - __all__ = ["Zero"] 7 + from . import _function 8 8 9 9 10 10 class Zero(_function.Function):
@@ -15,3 +15,11 @@
 15 15  x, 16 16  shape=x.shape[: x.ndim - self._input_ndim] + self._output_shape, 17 17  ) 18 + 19 +  @functools.singledispatchmethod 20 +  def __add__(self, other): 21 +  return super().__add__(other) 22 + 23 +  @functools.singledispatchmethod 24 +  def __sub__(self, other): 25 +  return super().__sub__(other)

@@ -8,7 +8,7 @@
 8 8 from probnum.typing import ArrayLike 9 9 10 10 from . import _random_process, kernels 11 - from .. import _function 11 + from .. import functions 12 12 13 13 14 14 class GaussianProcess(_random_process.RandomProcess[ArrayLike, np.ndarray]):
@@ -35,7 +35,7 @@
 35 35  Define a Gaussian process with a zero mean function and RBF kernel. 36 36 37 37  >>> import numpy as np 38 -  >>> from probnum.randprocs.mean_fns import Zero 38 +  >>> from probnum.functions import Zero 39 39  >>> from probnum.randprocs.kernels import ExpQuad 40 40  >>> from probnum.randprocs import GaussianProcess 41 41  >>> mu = Zero(input_shape=()) # zero-mean function
@@ -58,10 +58,10 @@
 58 58 59 59  def __init__( 60 60  self, 61 -  mean: _function.Function, 61 +  mean: functions.Function, 62 62  cov: kernels.Kernel, 63 63  ): 64 -  if not isinstance(mean, _function.Function): 64 +  if not isinstance(mean, functions.Function): 65 65  raise TypeError("The mean function must have type probnum.Function.") 66 66 67 67  super().__init__(

@@ -26,6 +26,7 @@
 26 26 from . import ( 27 27  diffeq, 28 28  filtsmooth, 29 +  functions, 29 30  linalg, 30 31  linops, 31 32  problems,
@@ -34,23 +35,18 @@
 34 35  randvars, 35 36  utils, 36 37 ) 37 - from ._function import Function, LambdaFunction 38 38 from ._version import version as __version__ 39 39 from .randvars import asrandvar 40 40 41 41 # Public classes and functions. Order is reflected in documentation. 42 42 __all__ = [ 43 43  "asrandvar", 44 -  "Function", 45 -  "LambdaFunction", 46 44  "ProbabilisticNumericalMethod", 47 45  "StoppingCriterion", 48 46  "LambdaStoppingCriterion", 49 47 ] 50 48 51 49 # Set correct module paths. Corrects links and module paths in documentation. 52 - Function.__module__ = "probnum" 53 - LambdaFunction.__module__ = "probnum" 54 50 ProbabilisticNumericalMethod.__module__ = "probnum" 55 51 StoppingCriterion.__module__ = "probnum" 56 52 LambdaStoppingCriterion.__module__ = "probnum"

@@ -0,0 +1,141 @@
 1 + r"""Fallback implementation for algebraic operations on :class:Function\ s.""" 2 + 3 + from __future__ import annotations 4 + 5 + import functools 6 + import operator 7 + 8 + import numpy as np 9 + 10 + from probnum import utils 11 + from probnum.typing import ScalarLike, ScalarType 12 + 13 + from ._function import Function 14 + 15 + 16 + class SumFunction(Function): 17 +  r"""Pointwise sum of :class:Function\ s. 18 + 19 +  Given functions :math:f_1, \dotsc, f_n \colon \mathbb{R}^n \to \mathbb{R}^m, this 20 +  defines a new function 21 + 22 +  .. math:: 23 +  \sum_{i = 1}^n f_i \colon \mathbb{R}^n \to \mathbb{R}^m, 24 +  x \mapsto \sum_{i = 1}^n f_i(x). 25 + 26 +  Parameters 27 +  ---------- 28 +  *summands 29 +  The functions :math:f_1, \dotsc, f_n. 30 +  """ 31 + 32 +  def __init__(self, *summands: Function) -> None: 33 +  if not all(isinstance(summand, Function) for summand in summands): 34 +  raise TypeError( 35 +  "The functions to be added must be objects of type Function." 36 +  ) 37 + 38 +  if not all( 39 +  summand.input_shape == summands[0].input_shape for summand in summands 40 +  ): 41 +  raise ValueError( 42 +  "The functions to be added must all have the same input shape." 43 +  ) 44 + 45 +  if not all( 46 +  summand.output_shape == summands[0].output_shape for summand in summands 47 +  ): 48 +  raise ValueError( 49 +  "The functions to be added must all have the same output shape." 50 +  ) 51 + 52 +  self._summands = summands 53 + 54 +  super().__init__( 55 +  input_shape=summands[0].input_shape, 56 +  output_shape=summands[0].output_shape, 57 +  ) 58 + 59 +  @property 60 +  def summands(self) -> tuple[SumFunction, ...]: 61 +  r"""The functions :math:f_1, \dotsc, f_n to be added.""" 62 +  return self._summands 63 + 64 +  def _evaluate(self, x: np.ndarray) -> np.ndarray: 65 +  return functools.reduce( 66 +  operator.add, (summand(x) for summand in self._summands) 67 +  ) 68 + 69 +  @functools.singledispatchmethod 70 +  def __add__(self, other): 71 +  return super().__add__(other) 72 + 73 +  @functools.singledispatchmethod 74 +  def __sub__(self, other): 75 +  return super().__sub__(other) 76 + 77 + 78 + class ScaledFunction(Function): 79 +  r"""Function multiplied pointwise with a scalar. 80 + 81 +  Given a function :math:f \colon \mathbb{R}^n \to \mathbb{R}^m and a scalar 82 +  :math:\alpha \in \mathbb{R}, this defines a new function 83 + 84 +  .. math:: 85 +  \alpha f \colon \mathbb{R}^n \to \mathbb{R}^m, 86 +  x \mapsto (\alpha f)(x) = \alpha f(x). 87 + 88 +  Parameters 89 +  ---------- 90 +  function 91 +  The function :math:f. 92 +  scalar 93 +  The scalar :math:\alpha. 94 +  """ 95 + 96 +  def __init__(self, function: Function, scalar: ScalarLike): 97 +  if not isinstance(function, Function): 98 +  raise TypeError( 99 +  "The function to be scaled must be an object of type Function." 100 +  ) 101 + 102 +  self._function = function 103 +  self._scalar = utils.as_numpy_scalar(scalar) 104 + 105 +  super().__init__( 106 +  input_shape=self._function.input_shape, 107 +  output_shape=self._function.output_shape, 108 +  ) 109 + 110 +  @property 111 +  def function(self) -> Function: 112 +  r"""The function :math:f.""" 113 +  return self._function 114 + 115 +  @property 116 +  def scalar(self) -> ScalarType: 117 +  r"""The scalar :math:\alpha.""" 118 +  return self._scalar 119 + 120 +  def _evaluate(self, x: np.ndarray) -> np.ndarray: 121 +  return self._scalar * self._function(x) 122 + 123 +  @functools.singledispatchmethod 124 +  def __mul__(self, other): 125 +  if np.ndim(other) == 0: 126 +  return ScaledFunction( 127 +  function=self._function, 128 +  scalar=self._scalar * np.asarray(other), 129 +  ) 130 + 131 +  return super().__mul__(other) 132 + 133 +  @functools.singledispatchmethod 134 +  def __rmul__(self, other): 135 +  if np.ndim(other) == 0: 136 +  return ScaledFunction( 137 +  function=self._function, 138 +  scalar=np.asarray(other) * self._scalar, 139 +  ) 140 + 141 +  return super().__rmul__(other) 0 142 imilarity index 76% 1 143 ename from src/probnum/_function.py 2 144 ename to src/probnum/functions/_function.py
 1 coverage:  2  precision: 2  3  status:  4  project:  5  default:  6  target: auto  7  threshold: 1%  8  patch:  9  default:  10  target: 90%  11  threshold: 1%  12 13 comment:  14  layout: "reach, diff, files"  15  behavior: default  16  require_changes: true