categorize.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. var tz = require('moment-timezone');
  2. var basal = require('../profile/basal');
  3. var getIOB = require('../iob');
  4. var ISF = require('../profile/isf');
  5. var find_insulin = require('../iob/history');
  6. var dosed = require('./dosed');
  7. // main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals.
  8. function categorizeBGDatums(opts) {
  9. var treatments = opts.treatments;
  10. // this sorts the treatments collection in order.
  11. treatments.sort(function (a, b) {
  12. var aDate = new Date(tz(a.timestamp));
  13. var bDate = new Date(tz(b.timestamp));
  14. //console.error(aDate);
  15. return bDate.getTime() - aDate.getTime();
  16. });
  17. var profileData = opts.profile;
  18. var glucoseData = [ ];
  19. if (typeof(opts.glucose) !== 'undefined') {
  20. //var glucoseData = opts.glucose;
  21. glucoseData = opts.glucose.map(function prepGlucose (obj) {
  22. //Support the NS sgv field to avoid having to convert in a custom way
  23. obj.glucose = obj.glucose || obj.sgv;
  24. if (obj.date) {
  25. //obj.BGTime = new Date(obj.date);
  26. } else if (obj.displayTime) {
  27. // Attempt to get date from displayTime
  28. obj.date = new Date(obj.displayTime.replace('T', ' ')).getTime();
  29. } else if (obj.dateString) {
  30. // Attempt to get date from dateString
  31. obj.date = new Date(obj.dateString).getTime();
  32. }// else { console.error("Could not determine BG time"); }
  33. if (!obj.dateString)
  34. {
  35. obj.dateString = new Date(tz(obj.date)).toISOString();
  36. }
  37. return obj;
  38. }).filter(function filterRecords(obj) {
  39. // Only take records with a valid date record
  40. // and a glucose value, which is also above 39
  41. return (obj.date && obj.glucose && obj.glucose >=39);
  42. }).sort(function (a, b) {
  43. // sort the collection in order
  44. return b.date - a.date;
  45. });
  46. }
  47. // if (typeof(opts.preppedGlucose) !== 'undefined') {
  48. // var preppedGlucoseData = opts.preppedGlucose;
  49. // }
  50. //starting variable at 0
  51. var boluses = 0;
  52. var maxCarbs = 0;
  53. //console.error(treatments);
  54. if (!treatments) return {};
  55. //console.error(glucoseData);
  56. var IOBInputs = {
  57. profile: profileData
  58. , history: opts.pumpHistory
  59. };
  60. var CSFGlucoseData = [];
  61. var ISFGlucoseData = [];
  62. var basalGlucoseData = [];
  63. var UAMGlucoseData = [];
  64. var CRData = [];
  65. var bucketedData = [];
  66. bucketedData[0] = JSON.parse(JSON.stringify(glucoseData[0]));
  67. var j=0;
  68. var k=0; // index of first value used by bucket
  69. //for loop to validate and bucket the data
  70. for (var i=1; i < glucoseData.length; ++i) {
  71. var BGTime = glucoseData[i].date;
  72. var lastBGTime = glucoseData[k].date;
  73. var elapsedMinutes = (BGTime - lastBGTime)/(60*1000);
  74. if(Math.abs(elapsedMinutes) >= 2) {
  75. j++; // move to next bucket
  76. k=i; // store index of first value used by bucket
  77. bucketedData[j]=JSON.parse(JSON.stringify(glucoseData[i]));
  78. } else {
  79. // average all readings within time deadband
  80. var glucoseTotal = glucoseData.slice(k, i+1).reduce(function(total, entry) {
  81. return total + entry.glucose;
  82. }, 0);
  83. bucketedData[j].glucose = glucoseTotal / (i-k+1);
  84. }
  85. }
  86. //console.error(bucketedData);
  87. //console.error(bucketedData[bucketedData.length-1]);
  88. // go through the treatments and remove any that are older than the oldest glucose value
  89. //console.error(treatments);
  90. for (i=treatments.length-1; i>0; --i) {
  91. var treatment = treatments[i];
  92. //console.error(treatment);
  93. if (treatment) {
  94. var treatmentDate = new Date(tz(treatment.timestamp));
  95. var treatmentTime = treatmentDate.getTime();
  96. var glucoseDatum = bucketedData[bucketedData.length-1];
  97. //console.error(glucoseDatum);
  98. if (glucoseDatum) {
  99. var BGDate = new Date(glucoseDatum.date);
  100. BGTime = BGDate.getTime();
  101. if ( treatmentTime < BGTime ) {
  102. treatments.splice(i,1);
  103. }
  104. }
  105. }
  106. }
  107. //console.error(treatments);
  108. var calculatingCR = false;
  109. var absorbing = 0;
  110. var uam = 0; // unannounced meal
  111. var mealCOB = 0;
  112. var mealCarbs = 0;
  113. var CRCarbs = 0;
  114. var type="";
  115. // main for loop
  116. var fullHistory = IOBInputs.history;
  117. for (i=bucketedData.length-5; i > 0; --i) {
  118. glucoseDatum = bucketedData[i];
  119. //console.error(glucoseDatum);
  120. BGDate = new Date(glucoseDatum.date);
  121. BGTime = BGDate.getTime();
  122. // As we're processing each data point, go through the treatment.carbs and see if any of them are older than
  123. // the current BG data point. If so, add those carbs to COB.
  124. treatment = treatments[treatments.length-1];
  125. var myCarbs = 0;
  126. if (treatment) {
  127. treatmentDate = new Date(tz(treatment.timestamp));
  128. treatmentTime = treatmentDate.getTime();
  129. //console.error(treatmentDate);
  130. if ( treatmentTime < BGTime ) {
  131. if (treatment.carbs >= 1) {
  132. mealCOB += parseFloat(treatment.carbs);
  133. mealCarbs += parseFloat(treatment.carbs);
  134. myCarbs = treatment.carbs;
  135. }
  136. treatments.pop();
  137. }
  138. }
  139. var BG;
  140. var delta;
  141. var avgDelta;
  142. // TODO: re-implement interpolation to avoid issues here with gaps
  143. // calculate avgDelta as last 4 datapoints to better catch more rises after COB hits zero
  144. if (typeof(bucketedData[i].glucose) !== 'undefined' && typeof(bucketedData[i+4].glucose) !== 'undefined') {
  145. //console.error(bucketedData[i]);
  146. BG = bucketedData[i].glucose;
  147. if ( BG < 40 || bucketedData[i+4].glucose < 40) {
  148. //process.stderr.write("!");
  149. continue;
  150. }
  151. delta = (BG - bucketedData[i+1].glucose);
  152. avgDelta = (BG - bucketedData[i+4].glucose)/4;
  153. } else { console.error("Could not find glucose data"); }
  154. avgDelta = avgDelta.toFixed(2);
  155. glucoseDatum.avgDelta = avgDelta;
  156. //sens = ISF
  157. var sens = ISF.isfLookup(IOBInputs.profile.isfProfile,BGDate);
  158. IOBInputs.clock=BGDate.toISOString();
  159. // trim down IOBInputs.history to just the data for 6h prior to BGDate
  160. //console.error(IOBInputs.history[0].created_at);
  161. var newHistory = [];
  162. for (var h=0; h<fullHistory.length; h++) {
  163. var hDate = new Date(fullHistory[h].created_at)
  164. //console.error(fullHistory[i].created_at, hDate, BGDate, BGDate-hDate);
  165. //if (h == 0 || h == fullHistory.length - 1) {
  166. //console.error(hDate, BGDate, hDate-BGDate)
  167. //}
  168. if (BGDate-hDate < 6*60*60*1000 && BGDate-hDate > 0) {
  169. //process.stderr.write("i");
  170. //console.error(hDate);
  171. newHistory.push(fullHistory[h]);
  172. }
  173. }
  174. IOBInputs.history = newHistory;
  175. // process.stderr.write("" + newHistory.length + " ");
  176. //console.error(newHistory[0].created_at,newHistory[newHistory.length-1].created_at,newHistory.length);
  177. // for IOB calculations, use the average of the last 4 hours' basals to help convergence;
  178. // this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge.
  179. // use the pumpbasalprofile to properly calculate IOB during periods where no temp basal is set
  180. var currentPumpBasal = basal.basalLookup(opts.pumpbasalprofile, BGDate);
  181. var BGDate1hAgo = new Date(BGTime-1*60*60*1000);
  182. var BGDate2hAgo = new Date(BGTime-2*60*60*1000);
  183. var BGDate3hAgo = new Date(BGTime-3*60*60*1000);
  184. var basal1hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate1hAgo);
  185. var basal2hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate2hAgo);
  186. var basal3hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate3hAgo);
  187. var sum = [currentPumpBasal,basal1hAgo,basal2hAgo,basal3hAgo].reduce(function(a, b) { return a + b; });
  188. IOBInputs.profile.currentBasal = Math.round((sum/4)*1000)/1000;
  189. // this is the current autotuned basal, used for everything else besides IOB calculations
  190. var currentBasal = basal.basalLookup(opts.basalprofile, BGDate);
  191. //console.error(currentBasal,basal1hAgo,basal2hAgo,basal3hAgo,IOBInputs.profile.currentBasal);
  192. // basalBGI is BGI of basal insulin activity.
  193. var basalBGI = Math.round(( currentBasal * sens / 60 * 5 )*100)/100; // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m
  194. //console.log(JSON.stringify(IOBInputs.profile));
  195. // call iob since calculated elsewhere
  196. var iob = getIOB(IOBInputs)[0];
  197. //console.error(JSON.stringify(iob));
  198. // activity times ISF times 5 minutes is BGI
  199. var BGI = Math.round(( -iob.activity * sens * 5 )*100)/100;
  200. // datum = one glucose data point (being prepped to store in output)
  201. glucoseDatum.BGI = BGI;
  202. // calculating deviation
  203. var deviation = avgDelta-BGI;
  204. var dev5m = delta-BGI;
  205. //console.error(deviation,avgDelta,BG,bucketedData[i].glucose);
  206. // set positive deviations to zero if BG is below 80
  207. if ( BG < 80 && deviation > 0 ) {
  208. deviation = 0;
  209. }
  210. // rounding and storing deviation
  211. deviation = deviation.toFixed(2);
  212. dev5m = dev5m.toFixed(2);
  213. glucoseDatum.deviation = deviation;
  214. // Then, calculate carb absorption for that 5m interval using the deviation.
  215. if ( mealCOB > 0 ) {
  216. var profile = profileData;
  217. var ci = Math.max(deviation, profile.min_5m_carbimpact);
  218. var absorbed = ci * profile.carb_ratio / sens;
  219. // Store the COB, and use it as the starting point for the next data point.
  220. mealCOB = Math.max(0, mealCOB-absorbed);
  221. }
  222. // Calculate carb ratio (CR) independently of CSF and ISF
  223. // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
  224. // For now, if another meal IOB/COB stacks on top of it, consider them together
  225. // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
  226. // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
  227. if (mealCOB > 0 || calculatingCR ) {
  228. // set initial values when we first see COB
  229. CRCarbs += myCarbs;
  230. if (!calculatingCR) {
  231. var CRInitialIOB = iob.iob;
  232. var CRInitialBG = glucoseDatum.glucose;
  233. var CRInitialCarbTime = new Date(glucoseDatum.date);
  234. console.error("CRInitialIOB:",CRInitialIOB,"CRInitialBG:",CRInitialBG,"CRInitialCarbTime:",CRInitialCarbTime);
  235. }
  236. // keep calculatingCR as long as we have COB or enough IOB
  237. if ( mealCOB > 0 && i>1 ) {
  238. calculatingCR = true;
  239. } else if ( iob.iob > currentBasal/2 && i>1 ) {
  240. calculatingCR = true;
  241. // when COB=0 and IOB drops low enough, record end values and be done calculatingCR
  242. } else {
  243. var CREndIOB = iob.iob;
  244. var CREndBG = glucoseDatum.glucose;
  245. var CREndTime = new Date(glucoseDatum.date);
  246. console.error("CREndIOB:",CREndIOB,"CREndBG:",CREndBG,"CREndTime:",CREndTime);
  247. var CRDatum = {
  248. CRInitialIOB: CRInitialIOB
  249. , CRInitialBG: CRInitialBG
  250. , CRInitialCarbTime: CRInitialCarbTime
  251. , CREndIOB: CREndIOB
  252. , CREndBG: CREndBG
  253. , CREndTime: CREndTime
  254. , CRCarbs: CRCarbs
  255. };
  256. //console.error(CRDatum);
  257. var CRElapsedMinutes = Math.round((CREndTime - CRInitialCarbTime) / 1000 / 60);
  258. //console.error(CREndTime - CRInitialCarbTime, CRElapsedMinutes);
  259. if ( CRElapsedMinutes < 60 || ( i===1 && mealCOB > 0 ) ) {
  260. console.error("Ignoring",CRElapsedMinutes,"m CR period.");
  261. } else {
  262. CRData.push(CRDatum);
  263. }
  264. CRCarbs = 0;
  265. calculatingCR = false;
  266. }
  267. }
  268. // If mealCOB is zero but all deviations since hitting COB=0 are positive, assign those data points to CSFGlucoseData
  269. // Once deviations go negative for at least one data point after COB=0, we can use the rest of the data to tune ISF or basals
  270. if (mealCOB > 0 || absorbing || mealCarbs > 0) {
  271. // if meal IOB has decayed, then end absorption after this data point unless COB > 0
  272. if ( iob.iob < currentBasal/2 ) {
  273. absorbing = 0;
  274. // otherwise, as long as deviations are positive, keep tracking carb deviations
  275. } else if (deviation > 0) {
  276. absorbing = 1;
  277. } else {
  278. absorbing = 0;
  279. }
  280. if ( ! absorbing && ! mealCOB ) {
  281. mealCarbs = 0;
  282. }
  283. // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag
  284. //console.error(type);
  285. if ( type !== "csf" ) {
  286. glucoseDatum.mealAbsorption = "start";
  287. console.error(glucoseDatum.mealAbsorption,"carb absorption");
  288. }
  289. type="csf";
  290. glucoseDatum.mealCarbs = mealCarbs;
  291. //if (i == 0) { glucoseDatum.mealAbsorption = "end"; }
  292. CSFGlucoseData.push(glucoseDatum);
  293. } else {
  294. // check previous "type" value, and if it was csf, set a mealAbsorption end flag
  295. if ( type === "csf" ) {
  296. CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end";
  297. console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption");
  298. }
  299. if ((iob.iob > 2 * currentBasal || deviation > 6 || uam) ) {
  300. if (deviation > 0) {
  301. uam = 1;
  302. } else {
  303. uam = 0;
  304. }
  305. if ( type !== "uam" ) {
  306. glucoseDatum.uamAbsorption = "start";
  307. console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption");
  308. }
  309. type="uam";
  310. UAMGlucoseData.push(glucoseDatum);
  311. } else {
  312. if ( type === "uam" ) {
  313. console.error("end unannounced meal absorption");
  314. }
  315. // Go through the remaining time periods and divide them into periods where scheduled basal insulin activity dominates. This would be determined by calculating the BG impact of scheduled basal insulin (for example 1U/hr * 48 mg/dL/U ISF = 48 mg/dL/hr = 5 mg/dL/5m), and comparing that to BGI from bolus and net basal insulin activity.
  316. // When BGI is positive (insulin activity is negative), we want to use that data to tune basals
  317. // When BGI is smaller than about 1/4 of basalBGI, we want to use that data to tune basals
  318. // When BGI is negative and more than about 1/4 of basalBGI, we can use that data to tune ISF,
  319. // unless avgDelta is positive: then that's some sort of unexplained rise we don't want to use for ISF, so that means basals
  320. if (basalBGI > -4 * BGI) {
  321. type="basal";
  322. basalGlucoseData.push(glucoseDatum);
  323. } else {
  324. if ( avgDelta > 0 && avgDelta > -2*BGI ) {
  325. //type="unknown"
  326. type="basal"
  327. basalGlucoseData.push(glucoseDatum);
  328. } else {
  329. type="ISF";
  330. ISFGlucoseData.push(glucoseDatum);
  331. }
  332. }
  333. }
  334. }
  335. // debug line to print out all the things
  336. var BGDateArray = BGDate.toString().split(" ");
  337. BGTime = BGDateArray[4];
  338. // console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"basalBGI:",basalBGI.toFixed(1),"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",deviation,"avgDelta:",avgDelta,type);
  339. console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",dev5m,"avgDev:",deviation,"avgDelta:",avgDelta,type,BG,myCarbs);
  340. }
  341. IOBInputs = {
  342. profile: profileData
  343. , history: opts.pumpHistory
  344. };
  345. treatments = find_insulin(IOBInputs);
  346. CRData.forEach(function(CRDatum) {
  347. var dosedOpts = {
  348. treatments: treatments
  349. , profile: opts.profile
  350. , start: CRDatum.CRInitialCarbTime
  351. , end: CRDatum.CREndTime
  352. };
  353. var insulinDosed = dosed(dosedOpts);
  354. CRDatum.CRInsulin = insulinDosed.insulin;
  355. //console.error(CRDatum);
  356. });
  357. var CSFLength = CSFGlucoseData.length;
  358. var ISFLength = ISFGlucoseData.length;
  359. var UAMLength = UAMGlucoseData.length;
  360. var basalLength = basalGlucoseData.length;
  361. if (opts.categorize_uam_as_basal) {
  362. console.error("--categorize-uam-as-basal=true set: categorizing all UAM data as basal.");
  363. basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData);
  364. } else if (CSFLength > 12) {
  365. console.error("Found at least 1h of carb absorption: assuming all meals were announced, and categorizing UAM data as basal.");
  366. basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData);
  367. } else {
  368. if (2*basalLength < UAMLength) {
  369. //console.error(basalGlucoseData, UAMGlucoseData);
  370. console.error("Warning: too many deviations categorized as UnAnnounced Meals");
  371. console.error("Adding",UAMLength,"UAM deviations to",basalLength,"basal ones");
  372. basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData);
  373. //console.error(basalGlucoseData);
  374. // if too much data is excluded as UAM, add in the UAM deviations to basal, but then discard the highest 50%
  375. basalGlucoseData.sort(function (a, b) {
  376. return a.deviation - b.deviation;
  377. });
  378. var newBasalGlucose = basalGlucoseData.slice(0,basalGlucoseData.length/2);
  379. //console.error(newBasalGlucose);
  380. basalGlucoseData = newBasalGlucose;
  381. console.error("and selecting the lowest 50%, leaving", basalGlucoseData.length, "basal+UAM ones");
  382. }
  383. if (2*ISFLength < UAMLength && ISFLength < 10) {
  384. console.error("Adding",UAMLength,"UAM deviations to",ISFLength,"ISF ones");
  385. ISFGlucoseData = ISFGlucoseData.concat(UAMGlucoseData);
  386. // if too much data is excluded as UAM, add in the UAM deviations to ISF, but then discard the highest 50%
  387. ISFGlucoseData.sort(function (a, b) {
  388. return a.deviation - b.deviation;
  389. });
  390. var newISFGlucose = ISFGlucoseData.slice(0,ISFGlucoseData.length/2);
  391. //console.error(newISFGlucose);
  392. ISFGlucoseData = newISFGlucose;
  393. console.error("and selecting the lowest 50%, leaving", ISFGlucoseData.length, "ISF+UAM ones");
  394. //console.error(ISFGlucoseData.length, UAMLength);
  395. }
  396. }
  397. basalLength = basalGlucoseData.length;
  398. ISFLength = ISFGlucoseData.length;
  399. if ( 4*basalLength + ISFLength < CSFLength && ISFLength < 10 ) {
  400. console.error("Warning: too many deviations categorized as meals");
  401. //console.error("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones");
  402. //var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData);
  403. console.error("Adding",CSFLength,"CSF deviations to",ISFLength,"ISF ones");
  404. ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData);
  405. CSFGlucoseData = [];
  406. }
  407. return {
  408. CRData: CRData,
  409. CSFGlucoseData: CSFGlucoseData,
  410. ISFGlucoseData: ISFGlucoseData,
  411. basalGlucoseData: basalGlucoseData
  412. };
  413. }
  414. exports = module.exports = categorizeBGDatums;