Source code for epyt_flow.uncertainty.uncertainties

  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
[docs] 276class UniformUncertainty(Uncertainty): 277 """ 278 Base class implementing uniform uncertainty. 279 280 Parameters 281 ---------- 282 low : `float`, optional 283 Lower bound of the uniform noise. 284 285 The default is zero. 286 high : `float`, optional 287 Upper bound of the uniform noise. 288 289 The default is one. 290 """ 291 def __init__(self, low: float = 0., high: float = 1., **kwds): 292 super().__init__(**kwds) 293 294 self.__low = low 295 self.__high = high 296 297 @property 298 def low(self) -> float: 299 """ 300 Gets the lower bound of the uniform noise. 301 302 Returns 303 ------- 304 `float` 305 Lower bound of the uniform noise. 306 """ 307 return self.__low 308 309 @property 310 def high(self) -> float: 311 """ 312 Gets the upper bound of the uniform noise. 313 314 Returns 315 ------- 316 `float` 317 Upper bound of the uniform noise. 318 """ 319 return self.__high 320
[docs] 321 def get_attributes(self) -> dict: 322 return super().get_attributes() | {"low": self.__low, "high": self.__high}
323 324 def __eq__(self, other) -> bool: 325 if not isinstance(other, (UniformUncertainty, type(None))): 326 raise TypeError("Can not compare 'UniformUncertainty' instance " + 327 f"with '{type(other)}' instance") 328 329 return super().__eq__(other) and self.__low == other.low and self.__high == other.high 330 331 def __str__(self) -> str: 332 return super().__str__() + f" low: {self.__low} high: {self.__high}"
333 334
[docs] 335@serializable(ABSOLUTE_UNIFORM_UNCERTAINTY_ID, ".epytflow_uncertainty_absolute_uniform") 336class AbsoluteUniformUncertainty(UniformUncertainty, JsonSerializable): 337 """ 338 Class implementing absolute uniform uncertainty -- i.e. uniform noise is added to the data. 339 """
[docs] 340 def apply(self, data: float) -> float: 341 data += self._random_generator.uniform(low=self.low, high=self.high) 342 343 return self.clip(data)
344 345
[docs] 346@serializable(RELATIVE_UNIFORM_UNCERTAINTY_ID, ".epytflow_uncertainty_relative_uniform") 347class RelativeUniformUncertainty(UniformUncertainty, JsonSerializable): 348 """ 349 Class implementing relative uniform uncertainty -- i.e. data is multiplied by uniform noise. 350 """
[docs] 351 def apply(self, data: float) -> float: 352 data *= self._random_generator.uniform(low=self.low, high=self.high) 353 354 return self.clip(data)
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
[docs] 391class DeepUniformUncertainty(Uncertainty): 392 """ 393 Base class implementing deep uniform uncertainty. 394 """ 395 def __init__(self, **kwds): 396 super().__init__(**kwds) 397 398 self.__create_uncertainties() 399 400 def __create_uncertainties(self, n_samples: int = 500): 401 self._uncertainties_idx = 0 402 rand_low = create_deep_random_pattern(n_samples, np_rand_gen=self._random_generator) 403 rand_high = create_deep_random_pattern(n_samples, np_rand_gen=self._random_generator) 404 rand_low = np.minimum(rand_low, rand_high) 405 rand_high = np.maximum(rand_low, rand_high) 406 self._uncertainties = [self._random_generator.uniform(low, high) 407 for low, high in zip(rand_low, rand_high)] 408
[docs] 409 def set_random_generator(self, np_rand_generator) -> None: 410 super().set_random_generator(np_rand_generator) 411 412 self.__create_uncertainties()
413
[docs] 414 @abstractmethod 415 def apply(self, data: float) -> float: 416 self._uncertainties_idx += 1 417 if self._uncertainties_idx >= len(self._uncertainties): 418 self.__create_uncertainties() 419 420 return self.clip(data)
421 422
[docs] 423@serializable(ABSOLUTE_DEEP_UNIFORM_UNCERTAINTY_ID, ".epytflow_uncertainty_absolute_deep_uniform") 424class AbsoluteDeepUniformUncertainty(DeepUniformUncertainty, JsonSerializable): 425 """ 426 Class implementing absolute deep uniform uncertainty -- i.e. random uniform noise 427 (shape of the noise is changing over time) is added to the data. 428 """
[docs] 429 def apply(self, data: float) -> float: 430 data += self._uncertainties[self._uncertainties_idx] 431 432 return super().apply(data)
433 434
[docs] 435@serializable(RELATIVE_DEEP_UNIFORM_UNCERTAINTY_ID, ".epytflow_uncertainty_relative_deep_uniform") 436class RelativeDeepUniformUncertainty(DeepUniformUncertainty, JsonSerializable): 437 """ 438 Class implementing relative deep uniform uncertainty -- i.e. data is multiplied by 439 random uniform noise (shape of the noise is changing over time). 440 """
[docs] 441 def apply(self, data: float) -> float: 442 data *= self._uncertainties[self._uncertainties_idx] 443 444 return super().apply(data)
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)