glucose-get-last.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. function getDateFromEntry(entry) {
  2. return entry.date || Date.parse(entry.display_time) || Date.parse(entry.dateString);
  3. }
  4. function round(value, digits)
  5. {
  6. if (! digits) { digits = 0; }
  7. var scale = Math.pow(10, digits);
  8. return Math.round(value * scale) / scale;
  9. }
  10. var getLastGlucose = function (data) {
  11. data = data.filter(function(obj) {
  12. return obj.glucose || obj.sgv;
  13. }).map(function prepGlucose (obj) {
  14. //Support the NS sgv field to avoid having to convert in a custom way
  15. obj.glucose = obj.glucose || obj.sgv;
  16. if ( obj.glucose !== null ) {
  17. return obj;
  18. }
  19. });
  20. var now = data[0];
  21. var now_date = getDateFromEntry(now);
  22. var change;
  23. var last_deltas = [];
  24. var short_deltas = [];
  25. var long_deltas = [];
  26. var last_cal = 0;
  27. //console.error(now.glucose);
  28. for (var i=1; i < data.length; i++) {
  29. // if we come across a cal record, don't process any older SGVs
  30. if (typeof data[i] !== 'undefined' && data[i].type === "cal") {
  31. last_cal = i;
  32. break;
  33. }
  34. // only use data from the same device as the most recent BG data point
  35. if (typeof data[i] !== 'undefined' && data[i].glucose > 38 && data[i].device === now.device) {
  36. var then = data[i];
  37. var then_date = getDateFromEntry(then);
  38. var avgdelta = 0;
  39. var minutesago;
  40. if (typeof then_date !== 'undefined' && typeof now_date !== 'undefined') {
  41. minutesago = Math.round( (now_date - then_date) / (1000 * 60) );
  42. // multiply by 5 to get the same units as delta, i.e. mg/dL/5m
  43. change = now.glucose - then.glucose;
  44. avgdelta = change/minutesago * 5;
  45. } else { console.error("Error: date field not found: cannot calculate avgdelta"); }
  46. //if (i < 5) {
  47. //console.error(then.glucose, minutesago, avgdelta);
  48. //}
  49. // use the average of all data points in the last 2.5m for all further "now" calculations
  50. if (-2 < minutesago && minutesago < 2.5) {
  51. now.glucose = ( now.glucose + then.glucose ) / 2;
  52. now_date = ( now_date + then_date ) / 2;
  53. //console.error(then.glucose, now.glucose);
  54. // short_deltas are calculated from everything ~5-15 minutes ago
  55. } else if (2.5 < minutesago && minutesago < 17.5) {
  56. //console.error(minutesago, avgdelta);
  57. short_deltas.push(avgdelta);
  58. // last_deltas are calculated from everything ~5 minutes ago
  59. if (2.5 < minutesago && minutesago < 7.5) {
  60. last_deltas.push(avgdelta);
  61. }
  62. //console.error(then.glucose, minutesago, avgdelta, last_deltas, short_deltas);
  63. // long_deltas are calculated from everything ~20-40 minutes ago
  64. } else if (17.5 < minutesago && minutesago < 42.5) {
  65. long_deltas.push(avgdelta);
  66. }
  67. }
  68. }
  69. var last_delta = 0;
  70. var short_avgdelta = 0;
  71. var long_avgdelta = 0;
  72. // start autoISF by https://github.com/ga-zelle/autoISF , relevant variables and functions
  73. // mod 7: append 2 variables for 5% range
  74. var autoISF_duration = 0;
  75. var autoISF_average = 0;
  76. // mod 8: append 3 variables for deltas based on regression analysis
  77. var slope05 = 0;
  78. var slope15 = 0;
  79. var slope40 = 0;
  80. // mod 14f: append results from best fitting parabola
  81. var dura_p = 0;
  82. var delta_pl = 0;
  83. var delta_pn = 0;
  84. var r_squ = 0;
  85. var bg_acceleration = 0;
  86. var a_0 = 0;
  87. var a_1 = 0;
  88. var a_2 = 0;
  89. var pp_debug = "autoISF Mod14-Debug: ";
  90. if (last_deltas.length > 0) {
  91. last_delta = last_deltas.reduce(function(a, b) { return a + b; }) / last_deltas.length;
  92. }
  93. if (short_deltas.length > 0) {
  94. short_avgdelta = short_deltas.reduce(function(a, b) { return a + b; }) / short_deltas.length;
  95. }
  96. if (long_deltas.length > 0) {
  97. long_avgdelta = long_deltas.reduce(function(a, b) { return a + b; }) / long_deltas.length;
  98. }
  99. var bw = 0.05;
  100. var sumBG = now.glucose;
  101. var oldavg = now.glucose;
  102. var minutesdur = 0;
  103. for (var i = 1; i < data.length; i++) {
  104. var then = data[i];
  105. var then_date = getDateFromEntry(then);
  106. // mod 7c: stop the series if there was a CGM gap greater than 13 minutes, i.e. 2 regular readings
  107. if (Math.round((now_date - then_date) / (1000 * 60)) - minutesdur > 13) {
  108. break;
  109. }
  110. if (then.glucose > oldavg*(1-bw) && then.glucose < oldavg*(1+bw)) {
  111. sumBG += then.glucose;
  112. oldavg = sumBG / (i+1);
  113. minutesdur = Math.round((now_date - then_date) / (1000 * 60));
  114. } else {
  115. break;
  116. }
  117. }
  118. autoISF_average = oldavg;
  119. autoISF_duration = minutesdur;
  120. // mod 8: calculate 3 variables for deltas based on linear regression
  121. // initially just test the handling of arguments
  122. var slope05 = 1.05;
  123. var slope15 = 1.15;
  124. var slope40 = 1.40;
  125. // mod 8a: now do the real maths based on
  126. // http://www.carl-engler-schule.de/culm/culm/culm2/th_messdaten/mdv2/auszug_ausgleichsgerade.pdf
  127. var sumBG = 0; // y
  128. var sumt = 0; // x
  129. var sumBG2 = 0; // y^2
  130. var sumt2 = 0; // x^2
  131. var sumxy = 0; // x*y
  132. //double a;
  133. var b; // y = a + b * x
  134. var level = 7.5;
  135. var minutesL;
  136. // here, longer deltas include all values from 0 up the related limit
  137. for (var i = 0; i < data.length; i++) {
  138. var then = data[i];
  139. var then_date = getDateFromEntry(then);
  140. minutesL = (now_date - then_date) / (1000 * 60);
  141. // watch out: the scan goes backwards in time, so delta has wrong sign
  142. if(i * sumt2 == sumt * sumt) {
  143. b = 0.0;
  144. }
  145. else {
  146. b = (i * sumxy - sumt * sumBG) / (i * sumt2 - sumt * sumt);
  147. }
  148. if (minutesL > level && level == 7.5) {
  149. slope05 = -b * 5;
  150. level = 17.5;
  151. }
  152. if (minutesL > level && level == 17.5) {
  153. slope15 = -b * 5;
  154. level = 42.5;
  155. }
  156. if (minutesL > level && level == 42.5) {
  157. slope40 = -b * 5;
  158. break;
  159. }
  160. sumt += minutesL;
  161. sumt2 += minutesL * minutesL;
  162. sumBG += then.glucose;
  163. sumBG2 += then.glucose * then.glucose;
  164. sumxy += then.glucose * minutesL;
  165. }
  166. // mod 14f: calculate best parabola and determine delta by extending it 5 minutes into the future
  167. // nach https://www.codeproject.com/Articles/63170/Least-Squares-Regression-for-Quadratic-Curve-Fitti
  168. //
  169. // y = a2*x^2 + a1*x + a0 or
  170. // y = a*x^2 + b*x + c respectively
  171. // initially just test the handling of arguments
  172. var dura_p = 0;
  173. var delta_pl = 0;
  174. var delta_pn = 0;
  175. var bg_acceleration = 0;
  176. var r_squ = 0;
  177. var best_a = 0;
  178. var best_b = 0;
  179. var best_c = 0;
  180. var a_0 = 0;
  181. var a_1 = 0;
  182. var a_2 = 0;
  183. if (data.length <= 3) { // last 3 points make a trivial parabola
  184. dura_p = 0;
  185. delta_pl = 0;
  186. delta_pn = 0;
  187. bg_acceleration = 0;
  188. r_squ = 0;
  189. a_0 = 0;
  190. a_1 = 0;
  191. a_2 = 0;
  192. } else {
  193. //double corrMin = 0.90; // go backwards until the correlation coefficient goes below
  194. var sy = 0; // y
  195. var sx = 0; // x
  196. var sx2 = 0; // x^2
  197. var sx3 = 0; // x^3
  198. var sx4 = 0; // x^4
  199. var sxy = 0; // x*y
  200. var sx2y = 0; // x^2*y
  201. var corrMax = 0;
  202. var iframe = data[0];
  203. var time_0 = getDateFromEntry(iframe);
  204. var ti_last = 0;
  205. //# for best numerical accurarcy time and bg must be of same order of magnitude
  206. var scaleTime = 300; //# in 5m; values are 0, 1, 2, 3, 4, ...
  207. var scaleBg = 50; //# TIR range is now 1.4 - 3.6
  208. for (var i = 0; i < data.length; i++) {
  209. var then = data[i];
  210. var then_date = getDateFromEntry(then);
  211. // skip records older than 47.5 minutes
  212. var ti = (then_date - time_0) / 1000 / scaleTime;
  213. if (-ti *scaleTime > 47 * 60) { // skip records older than 47.5 minutes
  214. break;
  215. } else if (ti < ti_last - 7.5 * 60 / scaleTime) { // stop scan if a CGM gap > 7.5 minutes is detected
  216. if ( i<3) { // history too short for fit
  217. dura_p = -ti_last / 60;
  218. delta_pl = 0;
  219. delta_pn = 0;
  220. bg_acceleration= 0;
  221. r_squ = 0;
  222. a_0 = 0;
  223. a_1 = 0;
  224. a_2 = 0;
  225. }
  226. break;
  227. }
  228. ti_last = ti;
  229. var bg = then.glucose/scaleBg;
  230. sx += ti;
  231. sx2 += Math.pow(ti, 2);
  232. sx3 += Math.pow(ti, 3);
  233. sx4 += Math.pow(ti, 4);
  234. sy += bg;
  235. sxy += ti * bg;
  236. sx2y += Math.pow(ti, 2) * bg;
  237. var n = i + 1;
  238. var D = 0;
  239. var Da = 0;
  240. var Db = 0;
  241. var Dc = 0;
  242. if (n > 3) {
  243. D = sx4 * (sx2 * n - sx * sx) - sx3 * (sx3 * n - sx * sx2) + sx2 * (sx3 * sx - sx2 * sx2);
  244. Da = sx2y* (sx2 * n - sx * sx) - sxy * (sx3 * n - sx * sx2) + sy * (sx3 * sx - sx2 * sx2);
  245. Db = sx4 * (sxy * n - sy * sx) - sx3 * (sx2y* n - sy * sx2) + sx2 * (sx2y* sx - sxy * sx2);
  246. Dc = sx4 * (sx2 *sy - sx *sxy) - sx3 * (sx3 *sy - sx *sx2y) + sx2 * (sx3 *sxy - sx2 * sx2y);
  247. }
  248. if (D != 0) {
  249. var a = Da / D;
  250. b = Db / D; // b initialised in linear fit !
  251. var c = Dc / D;
  252. var y_mean = sy / n;
  253. var s_squares = 0;
  254. var s_residual_squares = 0;
  255. for (var j = 0; j <= i; j++) {
  256. var before = data[j];
  257. var before_date = getDateFromEntry(before);
  258. s_squares += Math.pow(before.glucose / scaleBg - y_mean, 2);
  259. var delta_t = (before_date - time_0) / 1000 / scaleTime;
  260. var bg_j = a * Math.pow(delta_t, 2) + b * delta_t + c;
  261. s_residual_squares += Math.pow(before.glucose / scaleBg - bg_j, 2);
  262. }
  263. var r_squ = 0.64;
  264. if (s_squares != 0) {
  265. r_squ = 1 - s_residual_squares / s_squares;
  266. }
  267. if (n > 3) {
  268. if (r_squ >= corrMax) {
  269. corrMax = r_squ;
  270. // double delta_t = (then_date - time_0) / 1000;
  271. dura_p = -ti * scaleTime / 60; // remember we are going backwards in time
  272. var delta5Min = 5 * 60 / scaleTime;
  273. delta_pl =-scaleBg * (a * Math.pow(- delta5Min, 2) - b * delta5Min); // 5 minute slope from last fitted bg starting from last bg, i.e. t=0
  274. delta_pn = scaleBg * (a * Math.pow( delta5Min, 2) + b * delta5Min); // 5 minute slope to next fitted bg starting from last bg, i.e. t=0
  275. bg_acceleration = 2 * a * scaleBg; // 2nd derivative of parabola per (5min)^2
  276. a_0 = c * scaleBg;
  277. a_1 = b * scaleBg;
  278. a_2 = a * scaleBg;
  279. //r_squ = corrMax;
  280. best_a = a * scaleBg;
  281. best_b = b * scaleBg;
  282. best_c = c * scaleBg;
  283. }
  284. }
  285. }
  286. }
  287. pp_debug += "coeffs a/b/c=(" + round(best_a,2) + " / " + round(best_b,2) + " / " + round(best_c,2) + "); bg date=" + time_0 + "; ";
  288. pp_debug += "Parabola Fits a0/a1/a2=(" + round(a_0,2) + " / " + round(a_1,2) + " / " + round(a_2,2) + "); ";
  289. }
  290. pp_debug += "Slopes 05/15/40=(" + round(slope05,2) + " / " + round(slope15,2) + " / " + round(slope40,2) + "); "
  291. return {
  292. delta: Math.round( last_delta * 10000 ) / 10000
  293. , glucose: Math.round( now.glucose * 10000 ) / 10000
  294. , noise: Math.round(now.noise)
  295. , short_avgdelta: Math.round( short_avgdelta * 10000 ) / 10000
  296. , long_avgdelta: Math.round( long_avgdelta * 10000 ) / 10000
  297. // autoISF values to return to determineBasal.js
  298. , autoISF_average: Math.round( autoISF_average * 10000) / 10000
  299. , autoISF_duration: Math.round(autoISF_duration * 10000) / 10000
  300. , dura_p: Math.round( dura_p * 10000) / 10000
  301. , delta_pl: Math.round( delta_pl * 10000) / 10000
  302. , delta_pn: Math.round( delta_pn * 10000) / 10000
  303. , bg_acceleration: bg_acceleration
  304. , r_squ: Math.round( corrMax * 10000) / 10000
  305. , parabola_fit_a0: Math.round( a_0 * 10000) / 10000
  306. , parabola_fit_a1: Math.round( a_1 * 10000) / 10000
  307. , parabola_fit_a2: Math.round( a_2 * 10000) / 10000
  308. , pp_debug
  309. // end autoISF values
  310. , date: now_date
  311. , last_cal: last_cal
  312. , device: now.device
  313. };
  314. };
  315. module.exports = getLastGlucose;