1"""
2Module provides classes for implementing different types of uncertainties.
3"""
4from abc import ABC, abstractmethod
5from copy import deepcopy
6import numpy as np
7from numpy.random import Generator, default_rng
8
9from .utils import generate_deep_random_gaussian_noise, create_deep_random_pattern
10from ..serialization import serializable, JsonSerializable, ABSOLUTE_GAUSSIAN_UNCERTAINTY_ID, \
11 RELATIVE_GAUSSIAN_UNCERTAINTY_ID, ABSOLUTE_UNIFORM_UNCERTAINTY_ID, \
12 RELATIVE_UNIFORM_UNCERTAINTY_ID, ABSOLUTE_DEEP_UNIFORM_UNCERTAINTY_ID, \
13 RELATIVE_DEEP_UNIFORM_UNCERTAINTY_ID, ABSOLUTE_DEEP_GAUSSIAN_UNCERTAINTY_ID, \
14 RELATIVE_DEEP_GAUSSIAN_UNCERTAINTY_ID, ABSOLUTE_DEEP_UNCERTAINTY_ID, \
15 RELATIVE_DEEP_UNCERTAINTY_ID, PERCENTAGE_DEVIATON_UNCERTAINTY_ID
16
17
[docs]
18class Uncertainty(ABC):
19 """
20 Base class for uncertainties -- i.e. perturbations of data/signals.
21
22 Parameters
23 ----------
24 min_value : `float`, optional
25 Lower bound on the data/signal that is perturbed by this uncertainty.
26
27 The default is None.
28 max_value : `float`, optional
29 Upper bound on the data/signal that is perturbed by this uncertainty.
30
31 The default is None.
32 """
33 def __init__(self, min_value: float = None, max_value: float = None, **kwds):
34 super().__init__(**kwds)
35
36 self.__min_value = min_value
37 self.__max_value = max_value
38
39 self._random_generator = default_rng()
40
41 @property
42 def min_value(self) -> float:
43 """
44 Gets the lower bound on the data/signal.
45
46 Returns
47 -------
48 `float`
49 Lower bound.
50 """
51 return self.__min_value
52
53 @property
54 def max_value(self) -> float:
55 """
56 Gets the upper bound on the data/signal.
57
58 Returns
59 -------
60 `float`
61 Upper bound.
62 """
63 return self.__max_value
64
65 @property
66 def random_generator(self) -> Generator:
67 """
68 Returns the random number generator that is used for generating the uncertainties.
69
70 Returns
71 -------
72 `numpy.random.Generator <https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.Generator>`_
73 The random number generator.
74 """
75 return deepcopy(self._random_generator)
76
[docs]
77 def set_random_generator(self, np_rand_generator: Generator) -> None:
78 """
79 Sets the random number generator that is going to be used for generating the uncertainties.
80
81 Parameters
82 ----------
83 np_rand_generator : `numpy.random.Generator <https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.Generator>`_, optional
84 The random number generator.
85
86 The default is the default BitGenerator (PCG64) as constructed by
87 `numpy.random.default_rng() <https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.default_rng>`_.
88 """
89 self._random_generator = np_rand_generator
90
[docs]
91 def get_attributes(self) -> dict:
92 """
93 Gets all attributes to be serialized -- these attributes are passed to the
94 constructor when the object is deserialized.
95
96 Returns
97 -------
98 `dict`
99 Dictionary of attributes -- i.e. pairs of attribute name + value.
100 """
101 return {"min_value": self.__min_value, "max_value": self.__max_value}
102
103 def __eq__(self, other) -> bool:
104 if not isinstance(other, (Uncertainty, type(None))):
105 raise TypeError("Can not compare 'Uncertainty' instance " +
106 f"with '{type(other)}' instance")
107 if other is None:
108 return False
109
110 return self.__min_value == other.min_value and self.__max_value == other.max_value and \
111 self._random_generator == other.random_generator
112
113 def __str__(self) -> str:
114 return f"min_value: {self.__min_value} max_value: {self.__max_value}"
115
[docs]
116 def clip(self, data: np.ndarray) -> np.ndarray:
117 """
118 Clips values in a given array -- i.e. every value must be in [min_value, max_value].
119
120 Parameters
121 ----------
122 data : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
123 Array to be clipped.
124
125 Returns
126 -------
127 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
128 Clipped data.
129 """
130 if self.__min_value is not None:
131 data = np.max([data, self.__min_value])
132 if self.__max_value is not None:
133 data = np.min([data, self.__max_value])
134
135 return data
136
[docs]
137 @abstractmethod
138 def apply(self, data: float):
139 """
140 Applies the uncertainty to a single value.
141
142 Parameters
143 ----------
144 data : `float`
145 The value to which the uncertainty is applied.
146
147 Returns
148 -------
149 `float`
150 Uncertainty applied to 'data'.
151 """
152 raise NotImplementedError()
153
[docs]
154 def apply_batch(self, data: np.ndarray) -> np.ndarray:
155 """
156 Applies the uncertainty to an array of values.
157
158 Parameters
159 ----------
160 data : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
161 Array of values to which the uncertainty is applied.
162
163 Returns
164 -------
165 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
166 Uncertainty applied to `data`.
167 """
168 for t in range(data.shape[0]):
169 data[t] = self.apply(data[t])
170 return data
171
172
[docs]
173class GaussianUncertainty(Uncertainty):
174 """
175 Base class implementing Gaussian uncertainty
176
177 Parameters
178 ----------
179 mean : `float`, optional
180 Mean of the Gaussian noise.
181
182 If None, mean will be assigned a random value between 0 and 1.
183
184 The default is None.
185 scale : `float`, optional
186 Scale (i.e. standard deviation) of the Gaussian noise.
187
188 If None, scale will be assigned a random value between 0 and 1.
189
190 The default is None.
191 """
192 def __init__(self, mean: float = None, scale: float = None, **kwds):
193 super().__init__(**kwds)
194
195 self._mean = mean
196 self._scale = scale
197
198 @property
199 def mean(self) -> float:
200 """
201 Gets the mean of the Gaussian noise.
202
203 Returns
204 -------
205 `float`
206 Mean of the Gaussian noise.
207 """
208 return self._mean
209
210 @property
211 def scale(self) -> float:
212 """
213 Gets the scale (i.e. standard deviation) of the Gaussian noise.
214
215 Returns
216 -------
217 `float`
218 Scale (i.e. standard deviation) of the Gaussian noise.
219 """
220 return self._scale
221
[docs]
222 def get_attributes(self) -> dict:
223 return super().get_attributes() | {"mean": self._mean, "scale": self._scale}
224
225 def __eq__(self, other) -> bool:
226 if not isinstance(other, (GaussianUncertainty, type(None))):
227 raise TypeError("Can not compare 'GaussianUncertainty' instance " +
228 f"with '{type(other)}' instance")
229
230 return super().__eq__(other) and self._mean == other.mean and self._scale == other.scale
231
232 def __str__(self) -> str:
233 return super().__str__() + f" mean: {self._mean} scale: {self._scale}"
234
235
[docs]
236@serializable(ABSOLUTE_GAUSSIAN_UNCERTAINTY_ID, ".epytflow_uncertainty_absolute_gaussian")
237class AbsoluteGaussianUncertainty(GaussianUncertainty, JsonSerializable):
238 """
239 Class implementing absolute Gaussian uncertainty -- i.e. Gaussian noise is added to the data.
240 """
[docs]
241 def apply(self, data: float) -> float:
242 self._mean = self._random_generator.rand() if self._mean is None else self._mean
243 self._scale = self._random_generator.rand() if self._scale is None else self._scale
244
245 data += self._random_generator.normal(loc=self._mean, scale=self._scale)
246
247 return self.clip(data)
248
249
[docs]
250@serializable(RELATIVE_GAUSSIAN_UNCERTAINTY_ID, ".epytflow_uncertainty_relative_gaussian")
251class RelativeGaussianUncertainty(GaussianUncertainty, JsonSerializable):
252 """
253 Class implementing relative Gaussian uncertainty -- i.e. data is perturbed by Gaussian noise
254 centered at zero.
255
256 Parameters
257 ----------
258 scale : `float`, optional
259 Scale (i.e. standard deviation) of the Gaussian noise.
260
261 If None, scale will be assigned a random value between 0 and 1.
262
263 The default is None.
264 """
265 def __init__(self, scale: float = None, **kwds):
266 super().__init__(mean=0., scale=scale, **kwds)
267
[docs]
268 def apply(self, data: float) -> float:
269 self._scale = self._random_generator.rand() if self._scale is None else self._scale
270
271 data += self._random_generator.normal(loc=0, scale=self._scale)
272
273 return self.clip(data)
274
275
333
334
344
345
355
356
[docs]
357@serializable(PERCENTAGE_DEVIATON_UNCERTAINTY_ID, ".epytflow_uncertainty_percentage_deviation")
358class PercentageDeviationUncertainty(UniformUncertainty, JsonSerializable):
359 """
360 Class implementing a uniform data deviation -- i.e. the data can deviate up to some percentage
361 from its original value.
362
363 Parameters
364 ----------
365 deviation_percentage : `float`
366 Percentage (0-1) the data can deviate from its original value.
367 """
368 def __init__(self, deviation_percentage: float, **kwds):
369 if not isinstance(deviation_percentage, float):
370 raise TypeError("'deviation_percentage' must be an instance of 'float' " +
371 f"but not of {type(deviation_percentage)}")
372 if not 0 < deviation_percentage < 1:
373 raise ValueError("'deviation_percentage' must be in (0,1)")
374
375 if "low" in kwds:
376 del kwds["low"]
377 if "high" in kwds:
378 del kwds["high"]
379
380 super().__init__(low=1. - deviation_percentage, high=1. + deviation_percentage, **kwds)
381
[docs]
382 def get_attributes(self) -> dict:
383 return super().get_attributes() | {"deviation_percentage": self.high - 1.}
384
[docs]
385 def apply(self, data: float) -> float:
386 data *= self._random_generator.uniform(low=self.low, high=self.high)
387
388 return self.clip(data)
389
390
421
422
433
434
445
446
[docs]
447class DeepGaussianUncertainty(Uncertainty, JsonSerializable):
448 """
449 Base class implementing deep Gaussian uncertainty.
450
451 Parameters
452 ----------
453 mean : `float`, optional
454 Fixed mean of Gaussian noise.
455 If None, random means are generated.
456
457 The default is None.
458 """
459 def __init__(self, mean: float = None, **kwds):
460 self.__mean = mean
461
462 super().__init__(**kwds)
463
464 self.__create_uncertainties()
465
466 def __create_uncertainties(self, n_samples: int = 500) -> None:
467 self._uncertainties_idx = 0
468 self._uncertainties = generate_deep_random_gaussian_noise(n_samples, self.__mean,
469 np_rand_gen=self._random_generator)
470
[docs]
471 def set_random_generator(self, np_rand_generator) -> None:
472 super().set_random_generator(np_rand_generator)
473
474 self.__create_uncertainties()
475
[docs]
476 @abstractmethod
477 def apply(self, data: float) -> float:
478 self._uncertainties_idx += 1
479 if self._uncertainties_idx >= len(self._uncertainties):
480 self.__create_uncertainties()
481
482 return self.clip(data)
483
484
[docs]
485@serializable(ABSOLUTE_DEEP_GAUSSIAN_UNCERTAINTY_ID, ".epytflow_uncertainty_absolute_deep_gaussian")
486class AbsoluteDeepGaussianUncertainty(DeepGaussianUncertainty, JsonSerializable):
487 """
488 Class implementing absolute deep Gaussian uncertainty -- i.e. random Gaussian noise
489 (mean and variance are changing over time) is added to the data.
490 """
[docs]
491 def apply(self, data: float) -> float:
492 data += self._uncertainties[self._uncertainties_idx]
493
494 return super().apply(data)
495
496
[docs]
497@serializable(RELATIVE_DEEP_GAUSSIAN_UNCERTAINTY_ID, ".epytflow_uncertainty_relative_deep_gaussian")
498class RelativeDeepGaussianUncertainty(DeepGaussianUncertainty, JsonSerializable):
499 """
500 Class implementing realtive deep Gaussian uncertainty -- i.e. data is multiplied by
501 random Gaussian noise (mean and variance are changing over time).
502 """
503 def __init__(self, **kwds):
504 super().__init__(mean=0., **kwds)
505
[docs]
506 def apply(self, data: float) -> float:
507 data += self._uncertainties[self._uncertainties_idx]
508
509 return super().apply(data)
510
511
[docs]
512class DeepUncertainty(Uncertainty):
513 """
514 Base class implementing deep uncertainty.
515
516 Parameters
517 ----------
518 min_noise_value : `float`
519 Lower bound on the noise.
520 max_noise_value : `float`
521 Upper bound on the noise.
522 """
523 def __init__(self, min_noise_value: float = 0., max_noise_value: float = 1., **kwds):
524 super().__init__(**kwds)
525
526 self.__min_noise_value = min_noise_value
527 self.__max_noise_value = max_noise_value
528
529 self._uncertainties_idx = None
530 self._uncertainties = None
531 self.__create_uncertainties()
532
533 @property
534 def min_noise_value(self) -> float:
535 """
536 Gets the lower bound on the noise.
537
538 Returns
539 -------
540 `float`
541 Lower bound on the noise.
542 """
543 return self.__min_noise_value
544
545 @property
546 def max_noise_value(self) -> float:
547 """
548 Gets the upper bound on the noise.
549
550 Returns
551 -------
552 `float`
553 Upper bound on the noise.
554 """
555 return self.__max_noise_value
556
[docs]
557 def set_random_generator(self, np_rand_generator) -> None:
558 super().set_random_generator(np_rand_generator)
559
560 self.__create_uncertainties()
561
[docs]
562 def get_attributes(self) -> dict:
563 return super().get_attributes() | {"min_noise_value": self.__min_noise_value,
564 "max_noise_value": self.__max_noise_value}
565
566 def __eq__(self, other) -> bool:
567 if not isinstance(other, (DeepUncertainty, type(None))):
568 raise TypeError("Can not compare 'DeepUncertainty' instance " +
569 f"with '{type(other)}' instance")
570
571 return super().__eq__(other) and self.__min_noise_value == other.min_noise_value and \
572 self.__max_noise_value == other.max_noise_value
573
574 def __str__(self) -> str:
575 return super().__str__() + f" min_noise_value: {self.__min_noise_value} " +\
576 f"max_noise_value: {self.__max_noise_value}"
577
578 def __create_uncertainties(self, n_samples: int = 500) -> None:
579 init_value = None
580 if self._uncertainties_idx is not None:
581 init_value = self._uncertainties[-1]
582
583 self._uncertainties_idx = 0
584 self._uncertainties = create_deep_random_pattern(n_samples, self.__min_noise_value,
585 self.__max_noise_value, init_value,
586 np_rand_gen=self._random_generator)
587
[docs]
588 @abstractmethod
589 def apply(self, data: float) -> float:
590 self._uncertainties_idx += 1
591 if self._uncertainties_idx >= len(self._uncertainties):
592 self.__create_uncertainties()
593
594 return self.clip(data)
595
596
[docs]
597@serializable(ABSOLUTE_DEEP_UNCERTAINTY_ID, ".epytflow_uncertainty_absolute_deep")
598class AbsoluteDeepUncertainty(DeepUncertainty, JsonSerializable):
599 """
600 Class implementing absolute deep uncertainty -- i.e. completely random noise
601 is added to the data.
602 """
[docs]
603 def apply(self, data: float) -> float:
604 data += self._uncertainties[self._uncertainties_idx]
605
606 return super().apply(data)
607
608
[docs]
609@serializable(RELATIVE_DEEP_UNCERTAINTY_ID, ".epytflow_uncertainty_relative_deep")
610class RelativeDeepUncertainty(DeepUncertainty, JsonSerializable):
611 """
612 Class implementing relative deep uncertainty -- i.e. data is multiplied by
613 completely random noise.
614 """
[docs]
615 def apply(self, data: float) -> float:
616 data *= self._uncertainties[self._uncertainties_idx]
617
618 return super().apply(data)