Coverage for pygeodesy/fstats.py: 99%
303 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-07 07:28 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-07 07:28 -0400
2# -*- coding: utf-8 -*-
4u'''Classes for running statistics and regreesions based on
5L{pygeodesy.Fsum}, precision floating point summation.
6'''
7# make sure int/int division yields float quotient, see .basics
8from __future__ import division as _; del _ # PYCHOK semicolon
10from pygeodesy.basics import isodd, islistuple, _xinstanceof, \
11 _xsubclassof, _zip
12from pygeodesy.constants import _0_0, _1_5, _2_0, _3_0, _4_0, _6_0
13from pygeodesy.errors import _xError
14from pygeodesy.fmath import hypot2, sqrt
15from pygeodesy.fsums import _2float, Fmt, Fsum
16from pygeodesy.interns import NN, _iadd_op_, _invalid_, _other_, _SPACE_
17from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY
18from pygeodesy.named import _Named, _NotImplemented, notOverloaded, \
19 property_RO
20# from pygeodesy.props import property_RO # from .named
21# from pygeodesy.streprs import Fmt # from .fsums
23# from math import sqrt # pow from .fmath
25__all__ = _ALL_LAZY.fstats
26__version__ = '23.07.21'
28_Float = Fsum, float
29_Scalar = _Float + (int,) # XXX basics._Ints is ABCMeta
30try:
31 _Scalar += (long,)
32except NameError: # Python 3+
33 pass
36def _2Floats(xs, ys=False):
37 '''(INTERNAL) Yield each value as C{float} or L{Fsum}.
38 '''
39 for i, x in enumerate(xs):
40 yield x if isinstance(x, _Float) else (_2float(index=i, ys=x)
41 if ys else _2float(index=i, xs=x))
44def _sampled(n, sample):
45 '''(INTERNAL) Return the sample or the entire count.
46 '''
47 return (n - 1) if sample and n > 0 else n
50class _FstatsNamed(_Named):
51 '''(INTERNAL) Base class.
52 '''
53 _n = 0
55 def __add__(self, other):
56 '''Sum of this and a scalar, an L{Fsum} or an other instance.
57 '''
58 f = self.fcopy(name=self.__add__.__name__) # PYCHOK expected
59 f += other
60 return f
62 def __float__(self): # PYCHOK no cover
63 '''Not implemented.'''
64 return _NotImplemented(self)
66 def __int__(self): # PYCHOK no cover
67 '''Not implemented.'''
68 return _NotImplemented(self)
70 def __len__(self):
71 '''Return the I{total} number of accumulated values (C{int}).
72 '''
73 return self._n
75 def __neg__(self): # PYCHOK no cover
76 '''Not implemented.'''
77 return _NotImplemented(self)
79 def __radd__(self, other): # PYCHOK no cover
80 '''Not implemented.'''
81 return _NotImplemented(self, other)
83 def __str__(self):
84 return Fmt.SQUARE(self.named3, len(self))
86 def fcopy(self, deep=False, name=NN):
87 '''Copy this instance, C{shallow} or B{C{deep}}.
88 '''
89 n = name or self.fcopy.__name__
90 f = _Named.copy(self, deep=deep, name=n)
91 return self._copy(f, self) # PYCHOK expected
93 copy = fcopy
96class _FstatsBase(_FstatsNamed):
97 '''(INTERNAL) Base running stats class.
98 '''
99 _Ms = ()
101 def _copy(self, c, s):
102 '''(INTERNAL) Copy C{B{c} = B{s}}.
103 '''
104 _xinstanceof(self.__class__, c=c, s=s)
105 c._Ms = tuple(M.fcopy() for M in s._Ms) # deep=False
106 c._n = s._n
107 return c
109 def fadd(self, xs, sample=False): # PYCHOK no cover
110 '''(INTERNAL) I{Must be overloaded}, see function C{notOverloaded}.
111 '''
112 notOverloaded(self, xs, sample=sample)
114 def fadd_(self, *xs, **sample):
115 '''Accumulate and return the current count.
117 @see: Method C{fadd}.
118 '''
119 return self.fadd(xs, **sample)
121 def fmean(self, xs=None):
122 '''Accumulate and return the current mean.
124 @kwarg xs: Iterable with additional values (C{Scalar}s).
126 @return: Current, running mean (C{float}).
128 @see: Method C{fadd}.
129 '''
130 if xs:
131 self.fadd(xs)
132 return self._M1.fsum()
134 def fmean_(self, *xs):
135 '''Accumulate and return the current mean.
137 @see: Method C{fmean}.
138 '''
139 return self.fmean(xs)
141 def fstdev(self, xs=None, sample=False):
142 '''Accumulate and return the current standard deviation.
144 @kwarg xs: Iterable with additional values (C{Scalar}).
145 @kwarg sample: Return the I{sample} instead of the entire
146 I{population} value (C{bool}).
148 @return: Current, running (sample) standard deviation (C{float}).
150 @see: Method C{fadd}.
151 '''
152 v = self.fvariance(xs, sample=sample)
153 return sqrt(v) if v > 0 else _0_0
155 def fstdev_(self, *xs, **sample):
156 '''Accumulate and return the current standard deviation.
158 @see: Method C{fstdev}.
159 '''
160 return self.fstdev(xs, **sample)
162 def fvariance(self, xs=None, sample=False):
163 '''Accumulate and return the current variance.
165 @kwarg xs: Iterable with additional values (C{Scalar}s).
166 @kwarg sample: Return the I{sample} instead of the entire
167 I{population} value (C{bool}).
169 @return: Current, running (sample) variance (C{float}).
171 @see: Method C{fadd}.
172 '''
173 n = self.fadd(xs, sample=sample)
174 return float(self._M2 / float(n)) if n > 0 else _0_0
176 def fvariance_(self, *xs, **sample):
177 '''Accumulate and return the current variance.
179 @see: Method C{fvariance}.
180 '''
181 return self.fvariance(xs, **sample)
183 def _iadd_other(self, other):
184 '''(INTERNAL) Add Scalar or Scalars.
185 '''
186 if isinstance(other, _Scalar):
187 self.fadd_(other)
188 else:
189 try:
190 if not islistuple(other):
191 raise TypeError(_SPACE_(_invalid_, _other_))
192 self.fadd(other)
193 except Exception as x:
194 raise _xError(x, _SPACE_(self, _iadd_op_, repr(other)))
196 @property_RO
197 def _M1(self):
198 '''(INTERNAL) get the 1st Moment accumulator.'''
199 return self._Ms[0]
201 @property_RO
202 def _M2(self):
203 '''(INTERNAL) get the 2nd Moment accumulator.'''
204 return self._Ms[1]
207class Fcook(_FstatsBase):
208 '''U{Cook<https://www.JohnDCook.com/blog/skewness_kurtosis>}'s
209 C{RunningStats} computing the running mean, median and
210 (sample) kurtosis, skewness, variance, standard deviation
211 and Jarque-Bera normality.
213 @see: L{Fwelford} and U{Higher-order statistics<https://
214 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}.
215 '''
216 def __init__(self, xs=None, name=NN):
217 '''New L{Fcook} stats accumulator.
219 @kwarg xs: Iterable with initial values (C{Scalar}s).
220 @kwarg name: Optional name (C{str}).
222 @see: Method L{Fcook.fadd}.
223 '''
224 self._Ms = tuple(Fsum() for _ in range(4)) # 1st, 2nd ... Moment
225 if name:
226 self.name = name
227 if xs:
228 self.fadd(xs)
230 def __iadd__(self, other):
231 '''Add B{C{other}} to this L{Fcook} instance.
233 @arg other: An L{Fcook} instance or C{Scalar}s, meaning
234 one or more C{scalar} or L{Fsum} instances.
236 @return: This instance, updated (L{Fcook}).
238 @raise TypeError: Invalid B{C{other}} type.
240 @raise ValueError: Invalid B{C{other}}.
242 @see: Method L{Fcook.fadd}.
243 '''
244 if isinstance(other, Fcook):
245 nb = len(other)
246 if nb > 0:
247 na = len(self)
248 if na > 0:
249 A1, A2, A3, A4 = self._Ms
250 B1, B2, B3, B4 = other._Ms
252 n = na + nb
253 n_ = float(n)
254 D = A1 - B1 # b1 - a1
255 Dn = D / n_
256 Dn2 = Dn**2 # d**2 / n**2
257 nab = na * nb
258 Dn3 = Dn2 * (D * nab)
260 na2 = na**2
261 nb2 = nb**2
262 A4 += B4
263 A4 += (B3 * na - (A3 * nb)) * (Dn * _4_0)
264 A4 += (B2 * na2 + (A2 * nb2)) * (Dn2 * _6_0)
265 A4 += (Dn * Dn3) * (na2 - nab + nb2) # d**4 / n**3
267 A3 += B3
268 A3 += (A2 * na - (B2 * nb)) * (Dn * _3_0)
269 A3 += Dn3 * (na - nb)
271 A2 += B2
272 A2 += Dn2 * (nab / n_)
274 B1n = B1 * nb # if other is self
275 A1 *= na
276 A1 += B1n
277 A1 *= 1 / n_ # /= chokes PyChecker
279# self._Ms = A1, A2, A3, A4
280 self._n = n
281 else:
282 self._copy(self, other)
283 else:
284 self._iadd_other(other)
285 return self
287 def fadd(self, xs, sample=False):
288 '''Accumulate and return the current count.
290 @arg xs: Iterable with additional values (C{Scalar}s,
291 meaning C{scalar} or L{Fsum} instances).
292 @kwarg sample: Return the I{sample} instead of the entire
293 I{population} value (C{bool}).
295 @return: Current, running (sample) count (C{int}).
297 @raise OverflowError: Partial C{2sum} overflow.
299 @raise TypeError: Non-scalar B{C{xs}} value.
301 @raise ValueError: Invalid or non-finite B{C{xs}} value.
303 @see: U{online_kurtosis<https://WikiPedia.org/wiki/
304 Algorithms_for_calculating_variance>}.
305 '''
306 n = self._n
307 if xs:
308 M1, M2, M3, M4 = self._Ms
309 for x in _2Floats(xs):
310 n1 = n
311 n += 1
312 D = x - M1
313 Dn = D / n
314 if Dn:
315 Dn2 = Dn**2
316 if n1 > 1:
317 T1 = D * (Dn * n1)
318 T2 = T1 * (Dn * (n1 - 1))
319 T3 = T1 * (Dn2 * (n**2 - 3 * n1))
320 elif n1 > 0: # n1 == 1, n == 2
321 T1 = D * Dn
322 T2 = _0_0
323 T3 = T1 * Dn2
324 else:
325 T1 = T2 = T3 = _0_0
326 M4 += T3
327 M4 -= M3 * (Dn * _4_0)
328 M4 += M2 * (Dn2 * _6_0)
330 M3 += T2
331 M3 -= M2 * (Dn * _3_0)
333 M2 += T1
334 M1 += Dn
335# self._Ms = M1, M2, M3, M4
336 self._n = n
337 return _sampled(n, sample)
339 def fjb(self, xs=None, sample=True, excess=True):
340 '''Accumulate and compute the current U{Jarque-Bera
341 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality.
343 @kwarg xs: Iterable with additional values (C{Scalar}s).
344 @kwarg sample: Return the I{sample} value (C{bool}), default.
345 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default.
347 @return: Current, running (sample) Jarque-Bera normality (C{float}).
349 @see: Method L{Fcook.fadd}.
350 '''
351 n = self.fadd(xs, sample=sample)
352 k = self.fkurtosis(sample=sample, excess=excess) / _2_0
353 s = self.fskewness(sample=sample)
354 return n * hypot2(k, s) / _6_0
356 def fjb_(self, *xs, **sample_excess):
357 '''Accumulate and compute the current U{Jarque-Bera
358 <https://WikiPedia.org/wiki/Jarque–Bera_test>} normality.
360 @see: Method L{Fcook.fjb}.
361 '''
362 return self.fjb(xs, **sample_excess)
364 def fkurtosis(self, xs=None, sample=False, excess=True):
365 '''Accumulate and return the current kurtosis.
367 @kwarg xs: Iterable with additional values (C{Scalar}s).
368 @kwarg sample: Return the I{sample} instead of the entire
369 I{population} value (C{bool}).
370 @kwarg excess: Return the I{excess} kurtosis (C{bool}), default.
372 @return: Current, running (sample) kurtosis or I{excess} kurtosis (C{float}).
374 @see: U{Kurtosis Formula<https://www.Macroption.com/kurtosis-formula>}
375 and U{Mantalos<https://www.researchgate.net/publication/227440210>}.
377 @see: Method L{Fcook.fadd}.
378 '''
379 k, n = _0_0, self.fadd(xs, sample=sample)
380 if n > 0:
381 _, M2, _, M4 = self._Ms
382 m2 = float(M2 * M2)
383 if m2:
384 K, x = (M4 * (n / m2)), _3_0
385 if sample and 2 < n < len(self):
386 d = float((n - 1) * (n - 2))
387 K *= (n + 1) * (n + 2) / d
388 x *= n**2 / d
389 if excess:
390 K -= x
391 k = K.fsum()
392 return k
394 def fkurtosis_(self, *xs, **sample_excess):
395 '''Accumulate and return the current kurtosis.
397 @see: Method L{Fcook.fkurtosis}.
398 '''
399 return self.fkurtosis(xs, **sample_excess)
401 def fmedian(self, xs=None):
402 '''Accumulate and return the current median.
404 @kwarg xs: Iterable with additional values (C{Scalar}s).
406 @return: Current, running median (C{float}).
408 @see: U{Pearson's Skewness Coefficients<https://MathWorld.Wolfram.com/
409 PearsonsSkewnessCoefficients.html>}, U{Skewness & Kurtosis Simplified
410 https://TowardsDataScience.com/skewness-kurtosis-simplified-1338e094fc85>}
411 and method L{Fcook.fadd}.
412 '''
413 # skewness = 3 * (mean - median) / stdev, i.e.
414 # median = mean - skewness * stdef / 3
415 m = float(self._M1) if xs is None else self.fmean(xs)
416 return m - self.fskewness() * self.fstdev() / _3_0
418 def fmedian_(self, *xs):
419 '''Accumulate and return the current median.
421 @see: Method L{Fcook.fmedian}.
422 '''
423 return self.fmedian(xs)
425 def fskewness(self, xs=None, sample=False):
426 '''Accumulate and return the current skewness.
428 @kwarg xs: Iterable with additional values (C{Scalar}s).
429 @kwarg sample: Return the I{sample} instead of the entire
430 I{population} value (C{bool}).
432 @return: Current, running (sample) skewness (C{float}).
434 @see: U{Skewness Formula<https://www.Macroption.com/skewness-formula/>}
435 and U{Mantalos<https://www.researchgate.net/publication/227440210>}.
437 @see: Method L{Fcook.fadd}.
438 '''
439 s, n = _0_0, self.fadd(xs, sample=sample)
440 if n > 0:
441 _, M2, M3, _ = self._Ms
442 m2 = pow(float(M2), _1_5)
443 if m2:
444 S = M3 * (sqrt(float(n)) / m2)
445 if sample and 1 < n < len(self):
446 S *= (n + 1) / float(n - 1)
447 s = S.fsum()
448 return s
450 def fskewness_(self, *xs, **sample):
451 '''Accumulate and return the current skewness.
453 @see: Method L{Fcook.fskewness}.
454 '''
455 return self.fskewness(xs, **sample)
457 def toFwelford(self, name=NN):
458 '''Return an L{Fwelford} equivalent.
459 '''
460 f = Fwelford(name=name or self.name)
461 f._Ms = self._M1.fcopy(), self._M2.fcopy() # deep=False
462 f._n = self._n
463 return f
466class Fwelford(_FstatsBase):
467 '''U{Welford<https://WikiPedia.org/wiki/Algorithms_for_calculating_variance>}'s
468 accumulator computing the running mean, (sample) variance and standard deviation.
470 @see: U{Cook<https://www.JohnDCook.com/blog/standard_deviation/>} and L{Fcook}.
471 '''
472 def __init__(self, xs=None, name=NN):
473 '''New L{Fwelford} stats accumulator.
475 @kwarg xs: Iterable with initial values (C{Scalar}s).
476 @kwarg name: Optional name (C{str}).
478 @see: Method L{Fwelford.fadd}.
479 '''
480 self._Ms = Fsum(), Fsum() # 1st and 2nd Moment
481 if name:
482 self.name = name
483 if xs:
484 self.fadd(xs)
486 def __iadd__(self, other):
487 '''Add B{C{other}} to this L{Fwelford} instance.
489 @arg other: An L{Fwelford} or L{Fcook} instance or C{Scalar}s,
490 meaning one or more C{scalar} or L{Fsum} instances.
492 @return: This instance, updated (L{Fwelford}).
494 @raise TypeError: Invalid B{C{other}} type.
496 @raise ValueError: Invalid B{C{other}}.
498 @see: Method L{Fwelford.fadd} and U{Parallel algorithm<https//
499 WikiPedia.org/wiki/Algorithms_for_calculating_variance>}.
500 '''
501 if isinstance(other, Fwelford):
502 nb = len(other)
503 if nb > 0:
504 na = len(self)
505 if na > 0:
506 M, S = self._Ms
507 M_, S_ = other._Ms
509 n = na + nb
510 n_ = float(n)
512 D = M_ - M
513 D *= D # D**2
514 D *= na * nb / n_
515 S += D
516 S += S_
518 Mn = M_ * nb # if other is self
519 M *= na
520 M += Mn
521 M *= 1 / n_ # /= chokes PyChecker
523# self._Ms = M, S
524 self._n = n
525 else:
526 self._copy(self, other)
528 elif isinstance(other, Fcook):
529 self += other.toFwelford()
530 else:
531 self._iadd_other(other)
532 return self
534 def fadd(self, xs, sample=False):
535 '''Accumulate and return the current count.
537 @arg xs: Iterable with additional values (C{Scalar}s,
538 meaning C{scalar} or L{Fsum} instances).
539 @kwarg sample: Return the I{sample} instead of the entire
540 I{population} value (C{bool}).
542 @return: Current, running (sample) count (C{int}).
544 @raise OverflowError: Partial C{2sum} overflow.
546 @raise TypeError: Non-scalar B{C{xs}} value.
548 @raise ValueError: Invalid or non-finite B{C{xs}} value.
549 '''
550 n = self._n
551 if xs:
552 M, S = self._Ms
553 for x in _2Floats(xs):
554 n += 1
555 D = x - M
556 M += D / n
557 D *= x - M
558 S += D
559# self._Ms = M, S
560 self._n = n
561 return _sampled(n, sample)
564class Flinear(_FstatsNamed):
565 '''U{Cook<https://www.JohnDCook.com/blog/running_regression>}'s
566 C{RunningRegression} computing the running slope, intercept
567 and correlation of a linear regression.
568 '''
569 def __init__(self, xs=None, ys=None, Fstats=Fwelford, name=NN):
570 '''New L{Flinear} regression accumulator.
572 @kwarg xs: Iterable with initial C{x} values (C{Scalar}s).
573 @kwarg ys: Iterable with initial C{y} values (C{Scalar}s).
574 @kwarg Fstats: Stats class for C{x} and C{y} values (L{Fcook}
575 or L{Fwelford}).
576 @kwarg name: Optional name (C{str}).
578 @raise TypeError: Invalid B{C{Fs}}, not L{Fcook} or
579 L{Fwelford}.
580 @see: Method L{Flinear.fadd}.
581 '''
582 _xsubclassof(Fcook, Fwelford, Fstats=Fstats)
583 if name:
584 self.name = name
586 self._S = Fsum(name=name)
587 self._X = Fstats(name=name)
588 self._Y = Fstats(name=name)
589 if xs and ys:
590 self.fadd(xs, ys)
592 def __iadd__(self, other):
593 '''Add B{C{other}} to this instance.
595 @arg other: An L{Flinear} instance or C{Scalar} pairs,
596 meaning C{scalar} or L{Fsum} instances.
598 @return: This instance, updated (L{Flinear}).
600 @raise TypeError: Invalid B{C{other}} or the B{C{other}}
601 and these C{x} and C{y} accumulators
602 are not compatible.
604 @raise ValueError: Invalid or odd-length B{C{other}}.
606 @see: Method L{Flinear.fadd_}.
607 '''
608 if isinstance(other, Flinear):
609 if len(other) > 0:
610 if len(self) > 0:
611 n = other._n
612 S = other._S
613 X = other._X
614 Y = other._Y
615 D = (X._M1 - self._X._M1) * \
616 (Y._M1 - self._Y._M1) * \
617 (n * self._n / float(n + self._n))
618 self._n += n
619 self._S += S + D
620 self._X += X
621 self._Y += Y
622 else:
623 self._copy(self, other)
624 else:
625 try:
626 if not islistuple(other):
627 raise TypeError(_SPACE_(_invalid_, _other_))
628 elif isodd(len(other)):
629 raise ValueError(Fmt.PAREN(isodd=Fmt.PAREN(len=_other_)))
630 self.fadd_(*other)
631 except Exception as x:
632 raise _xError(x, _SPACE_(self, _iadd_op_, repr(other)))
633 return self
635 def _copy(self, c, s):
636 '''(INTERNAL) Copy C{B{c} = B{s}}.
637 '''
638 _xinstanceof(Flinear, c=c, s=s)
639 c._n = s._n
640 c._S = s._S.fcopy(deep=False)
641 c._X = s._X.fcopy(deep=False)
642 c._Y = s._Y.fcopy(deep=False)
643 return c
645 def fadd(self, xs, ys, sample=False):
646 '''Accumulate and return the current count.
648 @arg xs: Iterable with additional C{x} values (C{Scalar}s),
649 meaning C{scalar} or L{Fsum} instances).
650 @arg ys: Iterable with additional C{y} values (C{Scalar}s,
651 meaning C{scalar} or L{Fsum} instances).
652 @kwarg sample: Return the I{sample} instead of the entire
653 I{population} value (C{bool}).
655 @return: Current, running (sample) count (C{int}).
657 @raise OverflowError: Partial C{2sum} overflow.
659 @raise TypeError: Non-scalar B{C{xs}} or B{C{ys}} value.
661 @raise ValueError: Invalid or non-finite B{C{xs}} or B{C{ys}} value.
662 '''
663 n = self._n
664 if xs and ys:
665 S = self._S
666 X = self._X
667 Y = self._Y
668 for x, y in _zip(_2Floats(xs), _2Floats(ys, ys=True)): # strict=True
669 n1 = n
670 n += 1
671 if n1 > 0:
672 S += (X._M1 - x) * (Y._M1 - y) * (n1 / float(n))
673 X += x
674 Y += y
675 self._n = n
676 return _sampled(n, sample)
678 def fadd_(self, *x_ys, **sample):
679 '''Accumulate and return the current count.
681 @arg x_ys: Individual, alternating C{x, y, x, y, ...}
682 positional values (C{Scalar}s).
684 @see: Method C{Flinear.fadd}.
685 '''
686 return self.fadd(x_ys[0::2], x_ys[1::2], **sample)
688 def fcorrelation(self, sample=False):
689 '''Return the current, running (sample) correlation (C{float}).
691 @kwarg sample: Return the I{sample} instead of the entire
692 I{population} value (C{bool}).
693 '''
694 return self._sampled(self.x.fstdev(sample=sample) *
695 self.y.fstdev(sample=sample), sample)
697 def fintercept(self, sample=False):
698 '''Return the current, running (sample) intercept (C{float}).
700 @kwarg sample: Return the I{sample} instead of the entire
701 I{population} value (C{bool}).
702 '''
703 return float(self.y._M1 -
704 (self.x._M1 * self.fslope(sample=sample)))
706 def fslope(self, sample=False):
707 '''Return the current, running (sample) slope (C{float}).
709 @kwarg sample: Return the I{sample} instead of the entire
710 I{population} value (C{bool}).
711 '''
712 return self._sampled(self.x.fvariance(sample=sample), sample)
714 def _sampled(self, t, sample):
715 '''(INTERNAL) Compute the sampled or entire population result.
716 '''
717 t *= float(_sampled(self._n, sample))
718 return float(self._S / t) if t else _0_0
720 @property_RO
721 def x(self):
722 '''Get the C{x} accumulator (L{Fcook} or L{Fwelford}).
723 '''
724 return self._X
726 @property_RO
727 def y(self):
728 '''Get the C{y} accumulator (L{Fcook} or L{Fwelford}).
729 '''
730 return self._Y
733__all__ += _ALL_DOCS(_FstatsBase, _FstatsNamed)
735# **) MIT License
736#
737# Copyright (C) 2021-2023 -- mrJean1 at Gmail -- All Rights Reserved.
738#
739# Permission is hereby granted, free of charge, to any person obtaining a
740# copy of this software and associated documentation files (the "Software"),
741# to deal in the Software without restriction, including without limitation
742# the rights to use, copy, modify, merge, publish, distribute, sublicense,
743# and/or sell copies of the Software, and to permit persons to whom the
744# Software is furnished to do so, subject to the following conditions:
745#
746# The above copyright notice and this permission notice shall be included
747# in all copies or substantial portions of the Software.
748#
749# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
750# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
751# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
752# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
753# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
754# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
755# OTHER DEALINGS IN THE SOFTWARE.