autosens.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. var basal = require('../profile/basal');
  2. var get_iob = require('../iob');
  3. var find_insulin = require('../iob/history');
  4. var isf = require('../profile/isf');
  5. var find_meals = require('../meal/history');
  6. var tz = require('moment-timezone');
  7. var percentile = require('../percentile');
  8. function detectSensitivity(inputs) {
  9. //console.error(inputs.glucose_data[0]);
  10. var glucose_data = inputs.glucose_data.map(function prepGlucose (obj) {
  11. //Support the NS sgv field to avoid having to convert in a custom way
  12. obj.glucose = obj.glucose || obj.sgv;
  13. return obj;
  14. });
  15. //console.error(glucose_data[0]);
  16. var iob_inputs = inputs.iob_inputs;
  17. var basalprofile = inputs.basalprofile;
  18. var profile = inputs.iob_inputs.profile;
  19. // use last 24h worth of data by default
  20. if (inputs.retrospective) {
  21. //console.error(glucose_data[0]);
  22. var lastSiteChange = new Date(new Date(glucose_data[0].date).getTime() - (24 * 60 * 60 * 1000));
  23. } else {
  24. lastSiteChange = new Date(new Date().getTime() - (24 * 60 * 60 * 1000));
  25. }
  26. if (inputs.iob_inputs.profile.rewind_resets_autosens === true ) {
  27. // scan through pumphistory and set lastSiteChange to the time of the last pump rewind event
  28. // if not present, leave lastSiteChange unchanged at 24h ago.
  29. var history = inputs.iob_inputs.history;
  30. for (var h=1; h < history.length; ++h) {
  31. if ( ! history[h]._type || history[h]._type !== "Rewind" ) {
  32. //process.stderr.write("-");
  33. continue;
  34. }
  35. if ( history[h].timestamp ) {
  36. lastSiteChange = new Date( history[h].timestamp );
  37. console.error("Setting lastSiteChange to",lastSiteChange,"using timestamp",history[h].timestamp);
  38. break;
  39. }
  40. }
  41. }
  42. // get treatments from pumphistory once, not every time we get_iob()
  43. var treatments = find_insulin(inputs.iob_inputs);
  44. var mealinputs = {
  45. history: inputs.iob_inputs.history
  46. , profile: profile
  47. , carbs: inputs.carbs
  48. , glucose: inputs.glucose_data
  49. //, prepped_glucose: prepped_glucose_data
  50. };
  51. var meals = find_meals(mealinputs);
  52. meals.sort(function (a, b) {
  53. var aDate = new Date(tz(a.timestamp));
  54. var bDate = new Date(tz(b.timestamp));
  55. //console.error(aDate);
  56. return bDate.getTime() - aDate.getTime();
  57. });
  58. //console.error(meals);
  59. var avgDeltas = [];
  60. var bgis = [];
  61. var deviations = [];
  62. var deviationSum = 0;
  63. var bucketed_data = [];
  64. glucose_data.reverse();
  65. bucketed_data[0] = glucose_data[0];
  66. //console.error(bucketed_data[0]);
  67. var j=0;
  68. // go through the meal treatments and remove any that are older than the oldest glucose value
  69. //console.error(meals);
  70. for (var i=1; i < glucose_data.length; ++i) {
  71. var bgTime;
  72. var lastbgTime;
  73. if (glucose_data[i].display_time) {
  74. bgTime = new Date(glucose_data[i].display_time.replace('T', ' '));
  75. } else if (glucose_data[i].dateString) {
  76. bgTime = new Date(glucose_data[i].dateString);
  77. } else if (glucose_data[i].xDrip_started_at) {
  78. continue;
  79. } else { console.error("Could not determine BG time"); }
  80. if (glucose_data[i-1].display_time) {
  81. lastbgTime = new Date(glucose_data[i-1].display_time.replace('T', ' '));
  82. } else if (glucose_data[i-1].dateString) {
  83. lastbgTime = new Date(glucose_data[i-1].dateString);
  84. } else if (bucketed_data[0].display_time) {
  85. lastbgTime = new Date(bucketed_data[0].display_time.replace('T', ' '));
  86. } else if (glucose_data[i-1].xDrip_started_at) {
  87. continue;
  88. } else { console.error("Could not determine last BG time"); }
  89. if (glucose_data[i].glucose < 39 || glucose_data[i-1].glucose < 39) {
  90. //console.error("skipping:",glucose_data[i].glucose,glucose_data[i-1].glucose);
  91. continue;
  92. }
  93. // only consider BGs since lastSiteChange
  94. if (lastSiteChange) {
  95. var hoursSinceSiteChange = (bgTime-lastSiteChange)/(60*60*1000);
  96. if (hoursSinceSiteChange < 0) {
  97. //console.error(hoursSinceSiteChange, bgTime, lastSiteChange);
  98. continue;
  99. }
  100. }
  101. var elapsed_minutes = (bgTime - lastbgTime)/(60*1000);
  102. if(Math.abs(elapsed_minutes) > 2) {
  103. j++;
  104. bucketed_data[j]=glucose_data[i];
  105. bucketed_data[j].date = bgTime.getTime();
  106. //console.error(elapsed_minutes, bucketed_data[j].glucose, glucose_data[i].glucose);
  107. } else {
  108. bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose)/2;
  109. //console.error(bucketed_data[j].glucose, glucose_data[i].glucose);
  110. }
  111. }
  112. bucketed_data.shift();
  113. //console.error(bucketed_data[0]);
  114. for (i=meals.length-1; i>0; --i) {
  115. var treatment = meals[i];
  116. //console.error(treatment);
  117. if (treatment) {
  118. var treatmentDate = new Date(tz(treatment.timestamp));
  119. var treatmentTime = treatmentDate.getTime();
  120. var glucoseDatum = bucketed_data[0];
  121. //console.error(glucoseDatum);
  122. if (! glucoseDatum || ! glucoseDatum.date) {
  123. //console.error("No date found on: ",glucoseDatum);
  124. continue;
  125. }
  126. var BGDate = new Date(glucoseDatum.date);
  127. var BGTime = BGDate.getTime();
  128. if ( treatmentTime < BGTime ) {
  129. //console.error("Removing old meal: ",treatmentDate);
  130. meals.splice(i,1);
  131. }
  132. }
  133. }
  134. var absorbing = 0;
  135. var uam = 0; // unannounced meal
  136. var mealCOB = 0;
  137. var mealCarbs = 0;
  138. var mealStartCounter = 999;
  139. var type="";
  140. //console.error(bucketed_data);
  141. for (i=3; i < bucketed_data.length; ++i) {
  142. bgTime = new Date(bucketed_data[i].date);
  143. var sens = isf.isfLookup(profile.isfProfile,bgTime);
  144. //console.error(bgTime , bucketed_data[i].glucose);
  145. var bg;
  146. var avgDelta;
  147. var delta;
  148. if (typeof(bucketed_data[i].glucose) !== 'undefined') {
  149. bg = bucketed_data[i].glucose;
  150. var last_bg = bucketed_data[i-1].glucose;
  151. var old_bg = bucketed_data[i-3].glucose;
  152. if ( isNaN(bg) || !bg || bg < 40 || isNaN(old_bg) || !old_bg || old_bg < 40 || isNaN(last_bg) || !last_bg || last_bg < 40) {
  153. process.stderr.write("!");
  154. continue;
  155. }
  156. avgDelta = (bg - old_bg)/3;
  157. delta = (bg - last_bg);
  158. } else {
  159. console.error("Could not find glucose data");
  160. continue;
  161. }
  162. avgDelta = avgDelta.toFixed(2);
  163. iob_inputs.clock=bgTime;
  164. iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime);
  165. // make sure autosens doesn't use temptarget-adjusted insulin calculations
  166. iob_inputs.profile.temptargetSet = false;
  167. //console.log(JSON.stringify(iob_inputs.profile));
  168. //console.error("Before: ", new Date().getTime());
  169. var iob = get_iob(iob_inputs, true, treatments)[0];
  170. //console.error("After: ", new Date().getTime());
  171. //console.log(JSON.stringify(iob));
  172. var bgi = Math.round(( -iob.activity * sens * 5 )*100)/100;
  173. bgi = bgi.toFixed(2);
  174. //console.error(delta);
  175. var deviation;
  176. if (isNaN(delta) ) {
  177. console.error("Bad delta: ",delta, bg, last_bg, old_bg);
  178. } else {
  179. deviation = delta-bgi;
  180. }
  181. //if (!deviation) { console.error(deviation, delta, bgi); }
  182. // set positive deviations to zero if BG is below 80
  183. if ( bg < 80 && deviation > 0 ) {
  184. deviation = 0;
  185. }
  186. deviation = deviation.toFixed(2);
  187. glucoseDatum = bucketed_data[i];
  188. //console.error(glucoseDatum);
  189. BGDate = new Date(glucoseDatum.date);
  190. BGTime = BGDate.getTime();
  191. // As we're processing each data point, go through the treatment.carbs and see if any of them are older than
  192. // the current BG data point. If so, add those carbs to COB.
  193. treatment = meals[meals.length-1];
  194. if (treatment) {
  195. treatmentDate = new Date(tz(treatment.timestamp));
  196. treatmentTime = treatmentDate.getTime();
  197. if ( treatmentTime < BGTime ) {
  198. if (treatment.carbs >= 1) {
  199. //console.error(treatmentDate, treatmentTime, BGTime, BGTime-treatmentTime);
  200. mealCOB += parseFloat(treatment.carbs);
  201. mealCarbs += parseFloat(treatment.carbs);
  202. var displayCOB = Math.round(mealCOB);
  203. //console.error(displayCOB, mealCOB, treatment.carbs);
  204. process.stderr.write(displayCOB.toString()+"g");
  205. }
  206. meals.pop();
  207. }
  208. }
  209. // calculate carb absorption for that 5m interval using the deviation.
  210. if ( mealCOB > 0 ) {
  211. //var profile = profileData;
  212. var ci = Math.max(deviation, profile.min_5m_carbimpact);
  213. var absorbed = ci * profile.carb_ratio / sens;
  214. if (absorbed) {
  215. mealCOB = Math.max(0, mealCOB-absorbed);
  216. } else {
  217. console.error(absorbed, ci, profile.carb_ratio, sens, deviation, profile.min_5m_carbimpact);
  218. }
  219. }
  220. // If mealCOB is zero but all deviations since hitting COB=0 are positive, exclude from autosens
  221. //console.error(mealCOB, absorbing, mealCarbs);
  222. if (mealCOB > 0 || absorbing || mealCarbs > 0) {
  223. if (deviation > 0 ) {
  224. absorbing = 1;
  225. } else {
  226. absorbing = 0;
  227. }
  228. // stop excluding positive deviations as soon as mealCOB=0 if meal has been absorbing for >5h
  229. if ( mealStartCounter > 60 && mealCOB < 0.5 ) {
  230. displayCOB = Math.round(mealCOB);
  231. process.stderr.write(displayCOB.toString()+"g");
  232. absorbing = 0;
  233. }
  234. if ( ! absorbing && mealCOB < 0.5 ) {
  235. mealCarbs = 0;
  236. }
  237. // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag
  238. //console.error(type);
  239. if ( type !== "csf" ) {
  240. process.stderr.write("(");
  241. mealStartCounter = 0;
  242. //glucoseDatum.mealAbsorption = "start";
  243. //console.error(glucoseDatum.mealAbsorption,"carb absorption");
  244. }
  245. mealStartCounter++;
  246. type="csf";
  247. glucoseDatum.mealCarbs = mealCarbs;
  248. //if (i == 0) { glucoseDatum.mealAbsorption = "end"; }
  249. //CSFGlucoseData.push(glucoseDatum);
  250. } else {
  251. // check previous "type" value, and if it was csf, set a mealAbsorption end flag
  252. if ( type === "csf" ) {
  253. process.stderr.write(")");
  254. //CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end";
  255. //console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption");
  256. }
  257. var currentBasal = iob_inputs.profile.current_basal;
  258. // always exclude the first 45m after each carb entry using mealStartCounter
  259. //if (iob.iob > currentBasal || uam ) {
  260. if ((!inputs.retrospective && iob.iob > 2 * currentBasal) || uam || mealStartCounter < 9 ) {
  261. mealStartCounter++;
  262. if (deviation > 0) {
  263. uam = 1;
  264. } else {
  265. uam = 0;
  266. }
  267. if ( type !== "uam" ) {
  268. process.stderr.write("u(");
  269. //glucoseDatum.uamAbsorption = "start";
  270. //console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption");
  271. }
  272. //console.error(mealStartCounter);
  273. type="uam";
  274. } else {
  275. if ( type === "uam" ) {
  276. process.stderr.write(")");
  277. //console.error("end unannounced meal absorption");
  278. }
  279. type = "non-meal"
  280. }
  281. }
  282. // Exclude meal-related deviations (carb absorption) from autosens
  283. if ( type === "non-meal" ) {
  284. if ( deviation > 0 ) {
  285. //process.stderr.write(" "+bg.toString());
  286. process.stderr.write("+");
  287. } else if ( deviation === 0 ) {
  288. process.stderr.write("=");
  289. } else {
  290. //process.stderr.write(" "+bg.toString());
  291. process.stderr.write("-");
  292. }
  293. avgDeltas.push(avgDelta);
  294. bgis.push(bgi);
  295. deviations.push(deviation);
  296. deviationSum += parseFloat(deviation);
  297. } else {
  298. process.stderr.write("x");
  299. }
  300. // add an extra negative deviation if a high temptarget is running and exercise mode is set
  301. if (profile.high_temptarget_raises_sensitivity === true || profile.exercise_mode === true) {
  302. var tempTarget = tempTargetRunning(inputs.temptargets, bgTime)
  303. if (tempTarget) {
  304. //console.error(tempTarget)
  305. }
  306. if ( tempTarget > 100 ) {
  307. // for a 110 temptarget, add a -0.5 deviation, for 160 add -3
  308. var tempDeviation=-(tempTarget-100)/20;
  309. process.stderr.write("-");
  310. //console.error(tempDeviation)
  311. deviations.push(tempDeviation);
  312. }
  313. }
  314. var minutes = bgTime.getMinutes();
  315. var hours = bgTime.getHours();
  316. if ( minutes >= 0 && minutes < 5 ) {
  317. //console.error(bgTime);
  318. process.stderr.write(hours.toString()+"h");
  319. // add one neutral deviation every 2 hours to help decay over long exclusion periods
  320. if ( hours % 2 === 0 ) {
  321. deviations.push(0);
  322. process.stderr.write("=");
  323. }
  324. }
  325. var lookback = inputs.deviations;
  326. if (!lookback) { lookback = 96; }
  327. // only keep the last 96 non-excluded data points (8h+ for any exclusions)
  328. if (deviations.length > lookback) {
  329. deviations.shift();
  330. }
  331. }
  332. //console.error("");
  333. process.stderr.write(" ");
  334. //console.log(JSON.stringify(avgDeltas));
  335. //console.log(JSON.stringify(bgis));
  336. // when we have less than 8h worth of deviation data, add up to 90m of zero deviations
  337. // this dampens any large sensitivity changes detected based on too little data, without ignoring them completely
  338. console.error("");
  339. console.error("Using most recent",deviations.length,"deviations since",lastSiteChange);
  340. if (deviations.length < 96) {
  341. var pad = Math.round((1 - deviations.length/96) * 18);
  342. console.error("Adding",pad,"more zero deviations");
  343. for (var d=0; d<pad; d++) {
  344. //process.stderr.write(".");
  345. deviations.push(0);
  346. }
  347. }
  348. avgDeltas.sort(function(a, b){return a-b});
  349. bgis.sort(function(a, b){return a-b});
  350. deviations.sort(function(a, b){return a-b});
  351. for (i=0.9; i > 0.1; i = i - 0.01) {
  352. //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2));
  353. if ( percentile(deviations, (i+0.01)) >= 0 && percentile(deviations, i) < 0 ) {
  354. //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2));
  355. var lessThanZero = Math.round(100*i);
  356. console.error(lessThanZero+"% of non-meal deviations negative (>50% = sensitivity)");
  357. }
  358. if ( percentile(deviations, (i+0.01)) > 0 && percentile(deviations, i) <= 0 ) {
  359. //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2));
  360. var greaterThanZero = 100-Math.round(100*i);
  361. console.error(greaterThanZero+"% of non-meal deviations positive (>50% = resistance)");
  362. }
  363. }
  364. var pSensitive = percentile(deviations, 0.50);
  365. var pResistant = percentile(deviations, 0.50);
  366. var average = deviationSum / deviations.length;
  367. //console.error("Mean deviation: "+average.toFixed(2));
  368. var squareDeviations = deviations.reduce(function(acc, dev){var dev_f = parseFloat(dev); return acc + dev_f * dev_f}, 0);
  369. var rmsDev = Math.sqrt(squareDeviations / deviations.length);
  370. console.error("RMS deviation: "+rmsDev.toFixed(2));
  371. var basalOff = 0;
  372. if(pSensitive < 0) { // sensitive
  373. basalOff = pSensitive * (60/5) / profile.sens;
  374. process.stderr.write("Insulin sensitivity detected: ");
  375. } else if (pResistant > 0) { // resistant
  376. basalOff = pResistant * (60/5) / profile.sens;
  377. process.stderr.write("Insulin resistance detected: ");
  378. } else {
  379. console.error("Sensitivity normal.");
  380. }
  381. ratio = 1 + (basalOff / profile.max_daily_basal);
  382. //console.error(basalOff, profile.max_daily_basal, ratio);
  383. // don't adjust more than 1.2x by default (set in preferences.json)
  384. var rawRatio = ratio;
  385. ratio = Math.max(ratio, profile.autosens_min);
  386. ratio = Math.min(ratio, profile.autosens_max);
  387. if (ratio !== rawRatio) {
  388. console.error('Ratio limited from ' + rawRatio + ' to ' + ratio);
  389. }
  390. ratio = Math.round(ratio*100)/100;
  391. newisf = Math.round(profile.sens / ratio);
  392. //console.error(profile, newisf, ratio);
  393. console.error("ISF adjusted from "+profile.sens+" to "+newisf);
  394. //console.error("Basal adjustment "+basalOff.toFixed(2)+"U/hr");
  395. //console.error("Ratio: "+ratio*100+"%: new ISF: "+newisf.toFixed(1)+"mg/dL/U");
  396. return {
  397. "ratio": ratio,
  398. "newisf": newisf
  399. }
  400. }
  401. module.exports = detectSensitivity;
  402. function tempTargetRunning(temptargets_data, time) {
  403. // sort tempTargets by date so we can process most recent first
  404. try {
  405. temptargets_data.sort(function (a, b) { return new Date(a.created_at) < new Date(b.created_at) });
  406. } catch (e) {
  407. //console.error("Could not sort temptargets_data. Optional feature temporary targets disabled.");
  408. }
  409. //console.error(temptargets_data);
  410. //console.error(time);
  411. for (var i = 0; i < temptargets_data.length; i++) {
  412. var start = new Date(temptargets_data[i].created_at);
  413. //console.error(start);
  414. var expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000);
  415. //console.error(expires);
  416. if (time >= new Date(temptargets_data[i].created_at) && temptargets_data[i].duration === 0) {
  417. // cancel temp targets
  418. //console.error(temptargets_data[i]);
  419. return 0;
  420. } else if (time >= new Date(temptargets_data[i].created_at) && time < expires ) {
  421. //console.error(temptargets_data[i]);
  422. var tempTarget = ( temptargets_data[i].targetTop + temptargets_data[i].targetBottom ) / 2;
  423. //console.error(tempTarget);
  424. return tempTarget;
  425. }
  426. }
  427. }