@@ -0,0 +1,6 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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 @@
Loading
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
Files Coverage
src/probnum 90.28%
Project Totals (197 files) 90.28%
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
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading