TimeSeries.pm 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. # $Id: TimeSeries.pm 10907 2016-02-21 17:38:02Z borisneubert $
  2. ##############################################################################
  3. #
  4. # TimeSeries.pm
  5. # Copyright by Dr. Boris Neubert
  6. # e-mail: omega at online dot de
  7. #
  8. # This file is part of fhem.
  9. #
  10. # Fhem is free software: you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License as published by
  12. # the Free Software Foundation, either version 2 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # Fhem is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License
  21. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  22. #
  23. ##############################################################################
  24. #
  25. # CHANGES
  26. #
  27. # 27.06.2015 Jens Beyer (jensb at forum dot fhem dot de)
  28. # new: properties holdTime (in), integral (out) and tSeries/vSeries (data buffer)
  29. # new: defining holdTime will enable data buffer and calculation of moving stat values instead of block stat values
  30. # modified: method _updatestat requires only one parameter apart from self
  31. # modified: when property 'method' is set to 'none' _updatestat() will be called with new data value instead of const 1
  32. #
  33. # 19.07.2015 Jens Beyer (jensb at forum dot fhem dot de)
  34. # new: static method selftest
  35. #
  36. # 23.07.2015 Jens Beyer (jensb at forum dot fhem dot de)
  37. # new: method getValue
  38. #
  39. # 24.01.2016 knxhm at forum dot fhem dot de & Jens Beyer (jensb at forum dot fhem dot de)
  40. # new: property median (out)
  41. #
  42. # 29.01.2016 Jens Beyer (jensb at forum dot fhem dot de)
  43. # modified: method elapsed reverted to version from 2015-01-31 to provide downsampling and buffering through fhem.pl
  44. # modified: method _housekeeping does not reset time series if hold time is specified
  45. #
  46. ##############################################################################
  47. package TimeSeries;
  48. use warnings;
  49. use strict;
  50. #use Data::Dumper;
  51. no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  52. # If two subsequent points in the time series are less than
  53. # EPS seconds apart, the second value is ignored. This feature catches
  54. # - time running backwards,
  55. # - two points at the same time (within the resolution of time),
  56. # - precision loss due to points too close in time.
  57. # Ignored values are counted in the lost property.
  58. use constant EPS => 0.001; # 1 millisecond
  59. #
  60. # A time series is a sequence of points (t,v) with timestamp t and value v.
  61. #
  62. sub new() {
  63. my ($class, $args)= @_;
  64. my @METHODS= qw(none linear const);
  65. # none = no time weighting at all
  66. # we must have an assumption about how the value varies between
  67. # two data points in the time series (discretization method).
  68. # The const method assumes, that the value was constant
  69. # since the previous one.
  70. # The linear method assumes, that the value changed linearly
  71. # from the previous one to the current one.
  72. my $self= {
  73. method => $args->{method} || "none",
  74. autoreset => $args->{autoreset}, # if set, resets series every autoreset seconds
  75. holdTime => $args->{holdTime}, # if set, enables data buffer and limits series to holdTime seconds
  76. count => 0, # number of points successfully added
  77. lost => 0, # number of points rejected
  78. t0 => undef, # timestamp of first value added
  79. t => undef, # timestamp of last value added
  80. v0 => undef, # first value added
  81. v => undef, # last value added
  82. min => undef, # smallest value in the series
  83. max => undef, # largest value in the series
  84. tSeries => undef,# array of timestamps, used if holdTime is defined
  85. vSeries => undef,# array of values, used if holdTime is defined
  86. # statistics
  87. n => 0, # size of sample (non time weighted) or number of intervals (time weighted)
  88. mean => undef, # arithmetic mean of values
  89. sd => undef, # standard deviation of values
  90. integral => undef, # sum (holdTime undefined) or integral area (holdTime defined) of all values
  91. median => undef, # median of all values (method must be "none" and holdTime must be defined)
  92. _t0 => undef, # same as t0; moved to _t on reset
  93. _t => undef, # same as t but survives a reset
  94. _v => undef, # same as v but survives a reset
  95. _M => undef, # see below
  96. _S => undef, # see below
  97. }; # we are a hash reference
  98. $self->{method}= "none" unless($self->{method} ~~ @METHODS);
  99. return bless($self, $class); # make $self an object of class $class
  100. }
  101. #
  102. # reset the series
  103. #
  104. sub reset() {
  105. my $self= shift;
  106. # statistics
  107. # _t and _v is taken care of in new() and in add()
  108. $self->{n}= 0;
  109. $self->{mean}= undef;
  110. $self->{sd}= undef;
  111. $self->{integral}= 0;
  112. $self->{median}= undef;
  113. $self->{_M}= undef;
  114. $self->{_S}= undef;
  115. $self->{_t0}= $self->{_t};
  116. #
  117. $self->{count}= 0;
  118. $self->{lost}= 0;
  119. $self->{t0}= undef;
  120. $self->{t}= undef;
  121. $self->{v0}= undef;
  122. $self->{v}= undef;
  123. $self->{min}= undef;
  124. $self->{max}= undef;
  125. #
  126. $self->{tSeries}= undef;
  127. $self->{vSeries}= undef;
  128. if (!defined($self->{autoreset})) {
  129. $self->{_t0}= undef;
  130. $self->{_t}= undef;
  131. $self->{_v}= undef;
  132. }
  133. }
  134. #
  135. # trim series depth to holdTime relative to now
  136. #
  137. sub trimToHoldTime() {
  138. my $self= shift;
  139. my $n = @{$self->{tSeries}};
  140. #main::Debug("TimeSeries::trimToHoldTime: old count=$n\n");
  141. if (defined($self->{holdTime}) && defined($self->{tSeries})) {
  142. # trim series cache depth to holdTime relative to now
  143. my $keepTime = time() - $self->{holdTime};
  144. my $trimCount = 0;
  145. foreach (@{$self->{tSeries}}) {
  146. if ($_ >= $keepTime) {
  147. last;
  148. }
  149. $trimCount++;
  150. }
  151. if ($trimCount > 0) {
  152. # remove aged out samples
  153. splice(@{$self->{tSeries}}, 0, $trimCount);
  154. splice(@{$self->{vSeries}}, 0, $trimCount);
  155. # update properties
  156. # - lost is kept untouched because it cannot be consistently manipulated
  157. $self->{count} = @{$self->{tSeries}};
  158. #main::Debug("TimeSeries::trimToHoldTime: new count=$count before\n");
  159. if ($self->{count} > 0) {
  160. $self->{t0} = $self->{tSeries}[0];
  161. $self->{t} = $self->{tSeries}[$#{$self->{tSeries}}];
  162. $self->{v0} = $self->{vSeries}[0];
  163. $self->{v} = $self->{vSeries}[$#{$self->{vSeries}}];
  164. $self->{_t0}= $self->{t0};
  165. $self->{_t} = $self->{t};
  166. $self->{_v} = $self->{v};
  167. } else {
  168. $self->{t0} = undef;
  169. $self->{t} = undef;
  170. $self->{v0} = undef;
  171. $self->{v} = undef;
  172. $self->{_t0}= undef;
  173. $self->{_t} = undef;
  174. $self->{_v} = undef;
  175. }
  176. # reset statistics
  177. $self->{n} = 0;
  178. $self->{min} = undef;
  179. $self->{max} = undef;
  180. $self->{mean} = undef;
  181. $self->{sd} = undef;
  182. $self->{integral}= 0;
  183. $self->{_M} = undef;
  184. $self->{_S} = undef;
  185. # rebuild statistic for remaining samples
  186. for my $i (0 .. $#{$self->{tSeries}}) {
  187. my $tn= $self->{tSeries}[$i];
  188. my $vn= $self->{vSeries}[$i];
  189. # min, max
  190. $self->{min}= $vn if(!defined($self->{min}) || $vn< $self->{min});
  191. $self->{max}= $vn if(!defined($self->{max}) || $vn> $self->{max});
  192. # statistics
  193. if($self->{method} eq "none") {
  194. # no time-weighting
  195. $self->_updatestat($vn);
  196. } else {
  197. # time-weighting
  198. if($i > 0) {
  199. my $to= $self->{tSeries}[$i-1];
  200. my $vo= $self->{vSeries}[$i-1];
  201. my $dt= $tn - $to;
  202. if($self->{method} eq "const") {
  203. # steps
  204. $self->_updatestat($vo * $dt);
  205. } else {
  206. # linear interpolation
  207. $self->_updatestat(0.5 * ($vo + $vn) * $dt);
  208. }
  209. }
  210. }
  211. }
  212. }
  213. }
  214. #my $count = @{$self->{tSeries}};
  215. #main::Debug("TimeSeries::trimToHoldTime: new count=$count\n");
  216. }
  217. sub _updatestat($$) {
  218. my ($self, $V)= @_;
  219. # see Donald Knuth, The Art of Computer Programming, ch. 4.2.2, formulas 14ff.
  220. my $n= ++$self->{n};
  221. if($n> 1) {
  222. my $M= $self->{_M};
  223. $self->{_M}= $M + ($V - $M) / $n;
  224. $self->{_S}= $self->{_S} + ($V - $M) * ($V - $M);
  225. $self->{integral}+= $V;
  226. #main::Debug("V= $V M= $M _M= ".$self->{_M}." _S= " .$self->{_S}." int= ".$self->{integral});
  227. } else {
  228. $self->{_M}= $V;
  229. $self->{_S}= 0;
  230. $self->{integral}= $V;
  231. }
  232. #main::Debug("STAT UPD n=$n");
  233. }
  234. #
  235. # has autoreset period elapsed?
  236. # used by fhem.pl for downsampling
  237. #
  238. sub elapsed($$) {
  239. my ($self, $t)= @_;
  240. return defined($self->{autoreset}) && defined($self->{_t0}) && ($t - $self->{_t0} >= $self->{autoreset});
  241. }
  242. #
  243. # reset or trim series
  244. #
  245. sub _housekeeping($) {
  246. my ($self, $t)= @_;
  247. if($self->elapsed($t) && !defined($self->{holdTime})) {
  248. #main::Debug("TimeSeries::_housekeeping: reset\n");
  249. $self->reset();
  250. } elsif(defined($self->{holdTime}) && defined($self->{_t0}) && ($t - $self->{_t0} >= $self->{holdTime})) {
  251. #main::Debug("TimeSeries::_housekeeping: trimToHoldTime\n");
  252. $self->trimToHoldTime();
  253. }
  254. }
  255. #
  256. # add a point to the series
  257. #
  258. sub add($$$) {
  259. my ($self, $t, $v)= @_;
  260. # reject values if time resolution is insufficient
  261. if(defined($self->{_t}) && $t - $self->{_t} < EPS) {
  262. $self->{lost}++;
  263. return; # note: for consistency, the value is not considered at all
  264. }
  265. # reset or trim series
  266. $self->_housekeeping($t);
  267. #main::Debug("ADD ($t,$v)"); ###
  268. # add point to data buffer
  269. if(defined($self->{holdTime})) {
  270. $self->{tSeries}[$self->{count}] = $t;
  271. $self->{vSeries}[$self->{count}] = $v;
  272. }
  273. # count
  274. $self->{count}++;
  275. # statistics
  276. if($self->{method} eq "none") {
  277. # no time-weighting
  278. $self->_updatestat($v);
  279. # median
  280. if(defined($self->{holdTime})) {
  281. my @sortedVSeries = sort {$TimeSeries::a <=> $TimeSeries::b} @{$self->{vSeries}};
  282. my $center = int($self->{count} / 2);
  283. if($self->{count} % 2 == 0) {
  284. $self->{median} = ($sortedVSeries[$center - 1] + $sortedVSeries[$center]) / 2;
  285. } else {
  286. $self->{median} = $sortedVSeries[$center];
  287. }
  288. }
  289. } else {
  290. # time-weighting
  291. if(defined($self->{_t})) {
  292. my $dt= $t - $self->{_t};
  293. if($self->{method} eq "const") {
  294. # steps
  295. $self->_updatestat($self->{_v} * $dt);
  296. } else {
  297. # linear interpolation
  298. $self->_updatestat(0.5 * ($self->{_v} + $v) * $dt);
  299. }
  300. }
  301. }
  302. $self->{_t}= $t;
  303. $self->{_v}= $v;
  304. # first point
  305. if(!defined($self->{t0})) {
  306. $self->{t0}= $t;
  307. $self->{v0}= $v;
  308. }
  309. if(!defined($self->{_t0})) {
  310. $self->{_t0}= $t;
  311. }
  312. # last point
  313. $self->{t}= $t;
  314. $self->{v}= $v;
  315. # min, max
  316. $self->{min}= $v if(!defined($self->{min}) || $v< $self->{min});
  317. $self->{max}= $v if(!defined($self->{max}) || $v> $self->{max});
  318. # mean, standard deviation
  319. my $n= $self->{n};
  320. if($n) {
  321. my $T= $self->{method} eq "none" ? 1 : ( $self->{t} - $self->{_t0} ) / $n;
  322. if($T> 0) {
  323. #main::Debug("T= $T _M= " . $self->{_M} );
  324. $self->{mean}= $self->{_M} / $T;
  325. # in the time-weighted methods, this is just a measure for the variation of the values
  326. $self->{sd}= sqrt($self->{_S}/ ($n-1)) / $T if($n> 1);
  327. }
  328. }
  329. #main::Debug(Dumper($self)); ###
  330. }
  331. #
  332. # get corresponding value for given timestamp (data buffer must be enabled by setting holdTime)
  333. #
  334. # - if there is no exact match found for timestamp,
  335. # the value of the next smallest timestamp available is returned
  336. # - if timestamp is not inside the current time range undef is returned
  337. #
  338. sub getValue($$) {
  339. my ($self, $t)= @_;
  340. my $v = undef;
  341. if (defined($self->{tSeries}) && $t >= $self->{t0} && $t <= $self->{t}) {
  342. my $index = 0;
  343. for my $i (0 .. $#{$self->{tSeries}}) {
  344. my $ti= $self->{tSeries}[$i];
  345. if ($ti > $t) {
  346. last;
  347. }
  348. $index++;
  349. }
  350. $v = $self->{vSeries}[--$index];
  351. }
  352. return $v;
  353. }
  354. #
  355. # static class selftest performs unit test and logs validation errors
  356. #
  357. sub selftest() {
  358. my ($self, @params) = @_;
  359. die "static sub selftest may not be called as object method" if ref($self);
  360. my $success = 1;
  361. # block operation tests
  362. my $tsb = TimeSeries->new( { method => "none", autoreset => 3 } );
  363. $tsb->add(0, 0.8);
  364. $tsb->add(1, 1.0);
  365. $tsb->add(2, 1.2);
  366. if ($tsb->{count} != 3) { $success = 0; main::Debug("unweighed block add test failed: count mismatch $tsb->{count}/3\n"); }
  367. if ($tsb->{lost} != 0) { $success = 0; main::Debug("unweighed block add test failed: lost mismatch $tsb->{lost}/0\n"); }
  368. if ($tsb->{n} != 3) { $success = 0; main::Debug("unweighed block add test failed: n mismatch $tsb->{n}/3\n"); }
  369. if ($tsb->{t0} != 0) { $success = 0; main::Debug("unweighed block add test failed: first time mismatch $tsb->{t0}/0\n"); }
  370. if ($tsb->{t} != 2) { $success = 0; main::Debug("unweighed block add test failed: last time mismatch $tsb->{t}/2\n"); }
  371. if ($tsb->{v0} != 0.8) { $success = 0; main::Debug("unweighed block add test failed: first value mismatch $tsb->{v0}/0.8\n"); }
  372. if ($tsb->{v} != 1.2) { $success = 0; main::Debug("unweighed block add test failed: last value mismatch $tsb->{v}/1.2\n"); }
  373. if ($tsb->{min} != 0.8) { $success = 0; main::Debug("unweighed block add test failed: min mismatch $tsb->{min}/0.8\n"); }
  374. if ($tsb->{max} != 1.2) { $success = 0; main::Debug("unweighed block add test failed: max mismatch $tsb->{max}/1.2\n"); }
  375. if ($tsb->{mean} != 1.0) { $success = 0; main::Debug("unweighed block add test failed: mean mismatch $tsb->{mean}/1.0\n"); }
  376. if (!defined($tsb->{sd}) || $tsb->{sd} ne sqrt(0.13/2)) { $success = 0; main::Debug("unweighed block add test failed: sd mismatch $tsb->{sd}/0.254950975679639\n"); }
  377. if ($tsb->{integral} != 3.0) { $success = 0; main::Debug("unweighed block add test failed: sum mismatch $tsb->{integral}/3.0\n"); }
  378. $tsb->add(3, 0.8);
  379. $tsb->add(4, 1.2);
  380. if ($tsb->{count} != 2) { $success = 0; main::Debug("unweighed block autoreset test failed: count mismatch $tsb->{count}/2\n"); }
  381. if ($tsb->{lost} != 0) { $success = 0; main::Debug("unweighed block autoreset test failed: lost mismatch $tsb->{lost}/0\n"); }
  382. if ($tsb->{n} != 2) { $success = 0; main::Debug("unweighed block autoreset test failed: n mismatch $tsb->{n}/2\n"); }
  383. if ($tsb->{t0} != 3) { $success = 0; main::Debug("unweighed block autoreset test failed: first time mismatch $tsb->{t0}/3\n"); }
  384. if ($tsb->{t} != 4) { $success = 0; main::Debug("unweighed block autoreset test failed: last time mismatch $tsb->{t}/4\n"); }
  385. if ($tsb->{v0} != 0.8) { $success = 0; main::Debug("unweighed block autoreset test failed: first value mismatch $tsb->{v0}/0.8\n"); }
  386. if ($tsb->{v} != 1.2) { $success = 0; main::Debug("unweighed block autoreset test failed: last value mismatch $tsb->{v}/1.2\n"); }
  387. if ($tsb->{min} != 0.8) { $success = 0; main::Debug("unweighed block autoreset test failed: min mismatch $tsb->{min}/0.8\n"); }
  388. if ($tsb->{max} != 1.2) { $success = 0; main::Debug("unweighed block autoreset test failed: max mismatch $tsb->{max}/1.2\n"); }
  389. if ($tsb->{mean} != 1.0) { $success = 0; main::Debug("unweighed block autoreset test failed: mean mismatch $tsb->{mean}/1.0\n"); }
  390. if (!defined($tsb->{sd}) || $tsb->{sd} ne "0.4") { $success = 0; main::Debug("unweighed block autoreset test failed: sd mismatch $tsb->{sd}/0.4\n"); }
  391. if ($tsb->{integral} != 2.0) { $success = 0; main::Debug("unweighed block autoreset test failed: sum mismatch $tsb->{integral}/2.0\n"); }
  392. $tsb->reset();
  393. $tsb->{_t0} = undef;
  394. $tsb->{_t} = undef;
  395. $tsb->{_v} = undef;
  396. $tsb->{method} = 'const';
  397. $tsb->{autoreset} = 4;
  398. $tsb->add(0, 1.0);
  399. $tsb->add(1, 2.0);
  400. $tsb->add(3, 0.5);
  401. if ($tsb->{count} != 3) { $success = 0; main::Debug("const weighed block add test failed: count mismatch $tsb->{count}/3\n"); }
  402. if ($tsb->{lost} != 0) { $success = 0; main::Debug("const weighed block add test failed: lost mismatch $tsb->{lost}/0\n"); }
  403. if ($tsb->{n} != 2) { $success = 0; main::Debug("const weighed block add test failed: n mismatch $tsb->{n}/2\n"); }
  404. if ($tsb->{t0} != 0) { $success = 0; main::Debug("const weighed block add test failed: first time mismatch $tsb->{t0}/0\n"); }
  405. if ($tsb->{t} != 3) { $success = 0; main::Debug("const weighed block add test failed: last time mismatch $tsb->{t}/3\n"); }
  406. if ($tsb->{v0} != 1.0) { $success = 0; main::Debug("const weighed block add test failed: first value mismatch $tsb->{v0}/1.0\n"); }
  407. if ($tsb->{v} != 0.5) { $success = 0; main::Debug("const weighed block add test failed: last value mismatch $tsb->{v}/0.5\n"); }
  408. if ($tsb->{min} != 0.5) { $success = 0; main::Debug("const weighed block add test failed: min mismatch $tsb->{min}/0.5\n"); }
  409. if ($tsb->{max} != 2.0) { $success = 0; main::Debug("const weighed block add test failed: max mismatch $tsb->{max}/2.0\n"); }
  410. if ($tsb->{mean} ne (2.5/1.5)) { $success = 0; main::Debug("const weighed block add test failed: mean mismatch $tsb->{mean}/1.66666666666667\n"); }
  411. if (!defined($tsb->{sd}) || $tsb->{sd} ne 2) { $success = 0; main::Debug("const weighed block add test failed: sd mismatch $tsb->{sd}/2\n"); }
  412. if ($tsb->{integral} != 5.0) { $success = 0; main::Debug("const weighed block add test failed: sum mismatch $tsb->{integral}/5.0\n"); }
  413. # moving operation tests
  414. my $now = time();
  415. my $tsm = TimeSeries->new( { method => "none", holdTime => 3 } );
  416. $tsm->add($now-2, 0.8);
  417. $tsm->add($now-1, 1.0);
  418. $tsm->add($now, 1.2);
  419. if ($tsm->{count} != 3) { $success = 0; main::Debug("unweighed moving add test failed: count mismatch $tsm->{count}/3\n"); }
  420. if ($tsm->{lost} != 0) { $success = 0; main::Debug("unweighed moving add test failed: lost mismatch $tsm->{lost}/0\n"); }
  421. if ($tsm->{n} != 3) { $success = 0; main::Debug("unweighed moving add test failed: n mismatch $tsm->{n}/3\n"); }
  422. if ($tsm->{t0} != ($now-2)) { $success = 0; main::Debug("unweighed moving add test failed: first time mismatch $tsm->{t0}\n"); }
  423. if ($tsm->{t} != $now) { $success = 0; main::Debug("unweighed moving add test failed: last time mismatch $tsm->{t}\n"); }
  424. if ($tsm->{v0} != 0.8) { $success = 0; main::Debug("unweighed moving add test failed: first value mismatch $tsm->{v0}/0.8\n"); }
  425. if ($tsm->{v} != 1.2) { $success = 0; main::Debug("unweighed moving add test failed: last value mismatch $tsm->{v}/1.2\n"); }
  426. if ($tsm->{min} != 0.8) { $success = 0; main::Debug("unweighed moving add test failed: min mismatch $tsm->{min}/0.8\n"); }
  427. if ($tsm->{max} != 1.2) { $success = 0; main::Debug("unweighed moving add test failed: max mismatch $tsm->{max}/1.2\n"); }
  428. if ($tsm->{mean} != 1.0) { $success = 0; main::Debug("unweighed moving add test failed: mean mismatch $tsm->{mean}/1.0\n"); }
  429. if (!defined($tsm->{sd}) || $tsm->{sd} ne sqrt(0.13/2)) { $success = 0; main::Debug("unweighed moving add test failed: sd mismatch $tsm->{sd}/0.254950975679639\n"); }
  430. if ($tsm->{integral} != 3.0) { $success = 0; main::Debug("unweighed moving add test failed: sum mismatch $tsm->{integral}/3.0\n"); }
  431. if ($tsm->{median} != 1.0) { $success = 0; main::Debug("unweighed moving add test failed: median mismatch $tsm->{median}/1.0\n"); }
  432. sleep(3);
  433. $tsm->add($now+1, 1.0);
  434. $tsm->add($now+2, 0.8);
  435. if ($tsm->{count} != 3) { $success = 0; main::Debug("unweighed moving holdTime test failed: count mismatch $tsm->{count}/3\n"); }
  436. if ($tsm->{lost} != 0) { $success = 0; main::Debug("unweighed moving holdTime test failed: lost mismatch $tsm->{lost}/0\n"); }
  437. if ($tsm->{n} != 3) { $success = 0; main::Debug("unweighed moving holdTime test failed: n mismatch $tsm->{n}/3\n"); }
  438. if ($tsm->{t0} != $now) { $success = 0; main::Debug("unweighed moving holdTime test failed: first time mismatch $tsm->{t0}\n"); }
  439. if ($tsm->{t} != ($now+2)) { $success = 0; main::Debug("unweighed moving holdTime test failed: last time mismatch $tsm->{t}\n"); }
  440. if ($tsm->{v0} != 1.2) { $success = 0; main::Debug("unweighed moving holdTime test failed: first value mismatch $tsm->{v0}/1.2\n"); }
  441. if ($tsm->{v} != 0.8) { $success = 0; main::Debug("unweighed moving holdTime test failed: last value mismatch $tsm->{v}/0.8\n"); }
  442. if ($tsm->{min} != 0.8) { $success = 0; main::Debug("unweighed moving holdTime test failed: min mismatch $tsm->{min}/0.8\n"); }
  443. if ($tsm->{max} != 1.2) { $success = 0; main::Debug("unweighed moving holdTime test failed: max mismatch $tsm->{max}/1.2\n"); }
  444. if ($tsm->{mean} != 1.0) { $success = 0; main::Debug("unweighed moving holdTime test failed: mean mismatch $tsm->{mean}/1.0\n"); }
  445. if (!defined($tsm->{sd}) || $tsm->{sd} ne sqrt(0.13/2)) { $success = 0; main::Debug("unweighed moving holdTime test failed: sd mismatch $tsm->{sd}/0.254950975679639\n"); }
  446. if ($tsm->{integral} != 3.0) { $success = 0; main::Debug("unweighed moving holdTime test failed: sum mismatch $tsm->{integral}/3.0\n"); }
  447. if ($tsm->{median} != 1.0) { $success = 0; main::Debug("unweighed block autoreset test failed: median mismatch $tsm->{median}/1.0\n"); }
  448. $tsm->reset();
  449. $tsm->{method} = 'const';
  450. $tsm->{holdTime} = 5;
  451. $now = time();
  452. $tsm->add($now-4, 1.0);
  453. $tsm->add($now-3, 2.0);
  454. $tsm->add($now-1, -1.0);
  455. if ($tsm->{count} != 3) { $success = 0; main::Debug("const weighed moving add test 1 failed: count mismatch $tsm->{count}/3\n"); }
  456. if ($tsm->{lost} != 0) { $success = 0; main::Debug("const weighed moving add test 1 failed: lost mismatch $tsm->{lost}/0\n"); }
  457. if ($tsm->{n} != 2) { $success = 0; main::Debug("const weighed moving add test 1 failed: n mismatch $tsm->{n}/2\n"); }
  458. if ($tsm->{t0} != ($now-4)) { $success = 0; main::Debug("const weighed moving add test 1 failed: first time mismatch $tsm->{t0}\n"); }
  459. if ($tsm->{t} != ($now-1)) { $success = 0; main::Debug("const weighed moving add test 1 failed: last time mismatch $tsm->{t}\n"); }
  460. if ($tsm->{v0} != 1.0) { $success = 0; main::Debug("const weighed moving add test 1 failed: first value mismatch $tsm->{v0}/1.0\n"); }
  461. if ($tsm->{v} != -1.0) { $success = 0; main::Debug("const weighed moving add test 1 failed: last value mismatch $tsm->{v}/-1.0\n"); }
  462. if ($tsm->{min} != -1.0) { $success = 0; main::Debug("const weighed moving add test 1 failed: min mismatch $tsm->{min}/-1.0\n"); }
  463. if ($tsm->{max} != 2.0) { $success = 0; main::Debug("const weighed moving add test 1 failed: max mismatch $tsm->{max}/2.0\n"); }
  464. if ($tsm->{mean} ne (2.5/1.5)) { $success = 0; main::Debug("const weighed moving add test 1 failed: mean mismatch $tsm->{mean}/1.66666666666667\n"); }
  465. if (!defined($tsm->{sd}) || $tsm->{sd} ne 2) { $success = 0; main::Debug("const weighed moving add test 1 failed: sd mismatch $tsm->{sd}/2\n"); }
  466. if ($tsm->{integral} != 5.0) { $success = 0; main::Debug("const weighed moving add test 1 failed: sum mismatch $tsm->{integral}/5.0\n"); }
  467. $tsm->add($now, 0.5);
  468. if ($tsm->{count} != 4) { $success = 0; main::Debug("const weighed moving add test 2 failed: count mismatch $tsm->{count}/4\n"); }
  469. if ($tsm->{lost} != 0) { $success = 0; main::Debug("const weighed moving add test 2 failed: lost mismatch $tsm->{lost}/0\n"); }
  470. if ($tsm->{n} != 3) { $success = 0; main::Debug("const weighed moving add test 2 failed: n mismatch $tsm->{n}/3\n"); }
  471. if ($tsm->{t0} != ($now-4)) { $success = 0; main::Debug("const weighed moving add test 2 failed: first time mismatch $tsm->{t0}\n"); }
  472. if ($tsm->{t} != ($now)) { $success = 0; main::Debug("const weighed moving add test 2 failed: last time mismatch $tsm->{t}\n"); }
  473. if ($tsm->{v0} != 1.0) { $success = 0; main::Debug("const weighed moving add test 2 failed: first value mismatch $tsm->{v0}/1.0\n"); }
  474. if ($tsm->{v} != 0.5) { $success = 0; main::Debug("const weighed moving add test 2 failed: last value mismatch $tsm->{v}/0.5\n"); }
  475. if ($tsm->{min} != -1.0) { $success = 0; main::Debug("const weighed moving add test 2 failed: min mismatch $tsm->{min}/-1.0\n"); }
  476. if ($tsm->{max} != 2.0) { $success = 0; main::Debug("const weighed moving add test 2 failed: max mismatch $tsm->{max}/2.0\n"); }
  477. if ($tsm->{mean} != 1) { $success = 0; main::Debug("const weighed moving add test 2 failed: mean mismatch $tsm->{mean}/1\n"); }
  478. if (!defined($tsm->{sd}) || $tsm->{sd} ne sqrt(21.25/2)*3/4) { $success = 0; main::Debug("const weighed moving add test 2 failed: sd mismatch $tsm->{sd}/2.44470090195099\n"); }
  479. if ($tsm->{integral} != 4.0) { $success = 0; main::Debug("const weighed moving add test 2 failed: sum mismatch $tsm->{integral}/4.0\n"); }
  480. # get value tests
  481. if ($tsm->getValue($now-4) ne 1.0) { $success = 0; main::Debug("getValue test failed: first value mismatch ".$tsm->getValue($now-4)."/1.0\n"); }
  482. if ($tsm->getValue($now-3) ne 2.0) { $success = 0; main::Debug("getValue test failed: exact value mismatch ".$tsm->getValue($now-3)."/2.0\n"); }
  483. if ($tsm->getValue($now-2) ne 2.0) { $success = 0; main::Debug("getValue test failed: before value mismatch ".$tsm->getValue($now-2)."/2.0\n"); }
  484. if ($tsm->getValue($now) ne 0.5) { $success = 0; main::Debug("getValue test failed: last value mismatch ".$tsm->getValue($now)."/0.5\n"); }
  485. if (defined($tsm->getValue($now+1))) { $success = 0; main::Debug("getValue test failed: out of range value mismatch ".$tsm->getValue($now+1)."/undef\n"); }
  486. if ($success) {
  487. return "selftest passed";
  488. } else {
  489. return "selftest failed, see log for details";
  490. }
  491. }
  492. 1;
  493. =pod
  494. B<TimeSeries> is a perl module to feed time/value data points and get some statistics on them as you go:
  495. my $ts= TimeSeries->new( { method => "const" } );
  496. $ts->add(3.3, 2.1);
  497. $ts->add(5.1, 1.8);
  498. $ts->add(8.8, 2.4);
  499. printf("count= %d, n= %d, lost= %d, first= %f, last= %f, min= %f, max= %f, mean= %f, sd= %f\n",
  500. $ts->{count}, $ts->{n}, $ts->{lost}, $ts->{v0}, $ts->{v},
  501. $ts->{min}, $ts->{max},
  502. $ts->{mean}, $ts->{sd}
  503. );
  504. Mean, standard deviation and integral calculation also depends on the property method. You may choose from
  505. none (no time weighting), const (time weighted, step) or linear (time weighted, linear interpolation).
  506. The statistics may be reset manually using
  507. $ts->reset();
  508. By defining autoreset, the reset will occur automatically when the specified duration (seconds)
  509. is accumulated.
  510. If alternatively holdTime is defined, all data points are kept in a time limited data buffer that is
  511. re-evaluated each time a data point is added. Note that this may require significant amounts
  512. of memory depending on the sample rate and the holdTime.
  513. If method is none and holdtime is defined then the median of the values will be calculated additionally.
  514. It is also possible to define autoreset and holdtime at the same time. In this case the data buffer
  515. is enabled and will be cleared each time an autoreset occurs, independent of the value of holdtime.
  516. =cut