index.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. var percentile = require('../percentile');
  2. //rounding of basals for pumps
  3. //20 for omnipod to 0.05
  4. //40 for medtronic to 0.025
  5. var basal_scale = 20;
  6. // does three things - tunes basals, ISF, and CSF
  7. function tuneAllTheThings (inputs) {
  8. var previousAutotune = inputs.previousAutotune;
  9. //console.error(previousAutotune);
  10. var pumpProfile = inputs.pumpProfile;
  11. var pumpBasalProfile = pumpProfile.basalprofile;
  12. //console.error(pumpBasalProfile);
  13. var basalProfile = previousAutotune.basalprofile;
  14. //console.error(basalProfile);
  15. var isfProfile = previousAutotune.isfProfile;
  16. //console.error(isfProfile);
  17. var ISF = isfProfile.sensitivities[0].sensitivity;
  18. //console.error(ISF);
  19. var carbRatio = previousAutotune.carb_ratio;
  20. //console.error(carbRatio);
  21. var CSF = ISF / carbRatio;
  22. var DIA = previousAutotune.dia;
  23. var peak = previousAutotune.insulinPeakTime;
  24. if (! previousAutotune.useCustomPeakTime === true) {
  25. if ( previousAutotune.curve === "ultra-rapid" ) {
  26. peak = 55;
  27. } else {
  28. peak = 75;
  29. }
  30. }
  31. //console.error(DIA, peak);
  32. // conditional on there being a pump profile; if not then skip
  33. if (pumpProfile) { var pumpISFProfile = pumpProfile.isfProfile; }
  34. if (pumpISFProfile && pumpISFProfile.sensitivities[0]) {
  35. var pumpISF = pumpISFProfile.sensitivities[0].sensitivity;
  36. var pumpCarbRatio = pumpProfile.carb_ratio;
  37. var pumpCSF = pumpISF / pumpCarbRatio;
  38. }
  39. if (! carbRatio) { carbRatio = pumpCarbRatio; }
  40. if (! CSF) { CSF = pumpCSF; }
  41. if (! ISF) { ISF = pumpISF; }
  42. //console.error(CSF);
  43. var preppedGlucose = inputs.preppedGlucose;
  44. var CSFGlucose = preppedGlucose.CSFGlucoseData;
  45. //console.error(CSFGlucose[0]);
  46. var ISFGlucose = preppedGlucose.ISFGlucoseData;
  47. //console.error(ISFGlucose[0]);
  48. var basalGlucose = preppedGlucose.basalGlucoseData;
  49. //console.error(basalGlucose[0]);
  50. var CRData = preppedGlucose.CRData;
  51. //console.error(CRData);
  52. var diaDeviations = preppedGlucose.diaDeviations;
  53. //console.error(diaDeviations);
  54. var peakDeviations = preppedGlucose.peakDeviations;
  55. //console.error(peakDeviations);
  56. // tune DIA
  57. var newDIA = DIA;
  58. if (diaDeviations) {
  59. var currentDIAMeanDev = diaDeviations[2].meanDeviation;
  60. var currentDIARMSDev = diaDeviations[2].RMSDeviation;
  61. //console.error(DIA,currentDIAMeanDev,currentDIARMSDev);
  62. var minMeanDeviations = 1000000;
  63. var minRMSDeviations = 1000000;
  64. var meanBest = 2;
  65. var RMSBest = 2;
  66. for (var i=0; i < diaDeviations.length; i++) {
  67. var meanDeviations = diaDeviations[i].meanDeviation;
  68. var RMSDeviations = diaDeviations[i].RMSDeviation;
  69. if (meanDeviations < minMeanDeviations) {
  70. minMeanDeviations = Math.round(meanDeviations*1000)/1000;
  71. meanBest = i;
  72. }
  73. if (RMSDeviations < minRMSDeviations) {
  74. minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
  75. RMSBest = i;
  76. }
  77. }
  78. console.error("Best insulinEndTime for meanDeviations:",diaDeviations[meanBest].dia,"hours");
  79. console.error("Best insulinEndTime for RMSDeviations:",diaDeviations[RMSBest].dia,"hours");
  80. if ( meanBest < 2 && RMSBest < 2 ) {
  81. if ( diaDeviations[1].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[1].RMSDeviation < currentDIARMSDev * 0.99 ) {
  82. newDIA = diaDeviations[1].dia;
  83. }
  84. } else if ( meanBest > 2 && RMSBest > 2 ) {
  85. if ( diaDeviations[3].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[3].RMSDeviation < currentDIARMSDev * 0.99 ) {
  86. newDIA = diaDeviations[3].dia;
  87. }
  88. }
  89. if ( newDIA > 12 ) {
  90. console.error("insulinEndTime maximum is 12h: not raising further");
  91. newDIA=12;
  92. }
  93. if ( newDIA !== DIA ) {
  94. console.error("Adjusting insulinEndTime from",DIA,"to",newDIA,"hours");
  95. } else {
  96. console.error("Leaving insulinEndTime unchanged at",DIA,"hours");
  97. }
  98. }
  99. // tune insulinPeakTime
  100. var newPeak = peak;
  101. if (peakDeviations && peakDeviations[2]) {
  102. var currentPeakMeanDev = peakDeviations[2].meanDeviation;
  103. var currentPeakRMSDev = peakDeviations[2].RMSDeviation;
  104. //console.error(currentPeakMeanDev);
  105. minMeanDeviations = 1000000;
  106. minRMSDeviations = 1000000;
  107. meanBest = 2;
  108. RMSBest = 2;
  109. for (i=0; i < peakDeviations.length; i++) {
  110. meanDeviations = peakDeviations[i].meanDeviation;
  111. RMSDeviations = peakDeviations[i].RMSDeviation;
  112. if (meanDeviations < minMeanDeviations) {
  113. minMeanDeviations = Math.round(meanDeviations*1000)/1000;
  114. meanBest = i;
  115. }
  116. if (RMSDeviations < minRMSDeviations) {
  117. minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
  118. RMSBest = i;
  119. }
  120. }
  121. console.error("Best insulinPeakTime for meanDeviations:",peakDeviations[meanBest].peak,"minutes");
  122. console.error("Best insulinPeakTime for RMSDeviations:",peakDeviations[RMSBest].peak,"minutes");
  123. if ( meanBest < 2 && RMSBest < 2 ) {
  124. if ( peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[1].RMSDeviation < currentPeakRMSDev * 0.99 ) {
  125. newPeak = peakDeviations[1].peak;
  126. }
  127. } else if ( meanBest > 2 && RMSBest > 2 ) {
  128. if ( peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[3].RMSDeviation < currentPeakRMSDev * 0.99 ) {
  129. newPeak = peakDeviations[3].peak;
  130. }
  131. }
  132. if ( newPeak !== peak ) {
  133. console.error("Adjusting insulinPeakTime from",peak,"to",newPeak,"minutes");
  134. } else {
  135. console.error("Leaving insulinPeakTime unchanged at",peak);
  136. }
  137. }
  138. // Calculate carb ratio (CR) independently of CSF and ISF
  139. // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
  140. // For now, if another meal IOB/COB stacks on top of it, consider them together
  141. // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
  142. // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
  143. var CRTotalCarbs = 0;
  144. var CRTotalInsulin = 0;
  145. CRData.forEach(function(CRDatum) {
  146. var CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG;
  147. var CRInsulinReq = CRBGChange / ISF;
  148. var CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB;
  149. CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq;
  150. //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRDatum.CRInsulinTotal);
  151. var CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000;
  152. //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRDatum.CRInsulinTotal);
  153. //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulin,"CRDatum.CRInsulinTotal:",CRDatum.CRInsulinTotal,"CR:",CR);
  154. if (CRDatum.CRInsulinTotal > 0) {
  155. CRTotalCarbs += CRDatum.CRCarbs;
  156. CRTotalInsulin += CRDatum.CRInsulinTotal;
  157. //console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin);
  158. }
  159. });
  160. CRTotalInsulin = Math.round(CRTotalInsulin*1000)/1000;
  161. var totalCR = Math.round( CRTotalCarbs / CRTotalInsulin * 1000 )/1000;
  162. console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin,"totalCR:",totalCR);
  163. // convert the basal profile to hourly if it isn't already
  164. var hourlyBasalProfile = [];
  165. var hourlyPumpProfile = [];
  166. for (i=0; i < 24; i++) {
  167. // autotuned basal profile
  168. for (var j=0; j < basalProfile.length; ++j) {
  169. if (basalProfile[j].minutes <= i * 60) {
  170. if (basalProfile[j].rate === 0) {
  171. console.error("ERROR: bad basalProfile",basalProfile[j]);
  172. return;
  173. }
  174. hourlyBasalProfile[i] = JSON.parse(JSON.stringify(basalProfile[j]));
  175. }
  176. }
  177. hourlyBasalProfile[i].i=i;
  178. hourlyBasalProfile[i].minutes=i*60;
  179. var zeroPadHour = ("000"+i).slice(-2);
  180. hourlyBasalProfile[i].start=zeroPadHour + ":00:00";
  181. hourlyBasalProfile[i].rate=Math.round(hourlyBasalProfile[i].rate*1000)/1000
  182. // pump basal profile
  183. if (pumpBasalProfile && pumpBasalProfile[0]) {
  184. for (j=0; j < pumpBasalProfile.length; ++j) {
  185. //console.error(pumpBasalProfile[j]);
  186. if (pumpBasalProfile[j].rate === 0) {
  187. console.error("ERROR: bad pumpBasalProfile",pumpBasalProfile[j]);
  188. return;
  189. }
  190. if (pumpBasalProfile[j].minutes <= i * 60) {
  191. hourlyPumpProfile[i] = JSON.parse(JSON.stringify(pumpBasalProfile[j]));
  192. }
  193. }
  194. hourlyPumpProfile[i].i=i;
  195. hourlyPumpProfile[i].minutes=i*60;
  196. hourlyPumpProfile[i].rate=Math.round(hourlyPumpProfile[i].rate*1000)/1000
  197. }
  198. }
  199. //console.error(hourlyPumpProfile);
  200. //console.error(hourlyBasalProfile);
  201. var newHourlyBasalProfile = JSON.parse(JSON.stringify(hourlyBasalProfile));
  202. // look at net deviations for each hour
  203. for (var hour=0; hour < 24; hour++) {
  204. var deviations = 0;
  205. for (i=0; i < basalGlucose.length; ++i) {
  206. var BGTime;
  207. if (basalGlucose[i].date) {
  208. BGTime = new Date(basalGlucose[i].date);
  209. } else if (basalGlucose[i].displayTime) {
  210. BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
  211. } else if (basalGlucose[i].dateString) {
  212. BGTime = new Date(basalGlucose[i].dateString);
  213. } else {
  214. console.error("Could not determine last BG time");
  215. }
  216. var myHour = BGTime.getHours();
  217. if (hour === myHour) {
  218. //console.error(basalGlucose[i].deviation);
  219. deviations += parseFloat(basalGlucose[i].deviation);
  220. }
  221. }
  222. deviations = Math.round( deviations * 1000 ) / 1000
  223. console.error("Hour",hour.toString(),"total deviations:",deviations,"mg/dL");
  224. // calculate how much less or additional basal insulin would have been required to eliminate the deviations
  225. // only apply 20% of the needed adjustment to keep things relatively stable
  226. var basalNeeded = 0.2 * deviations / ISF;
  227. basalNeeded = Math.round( basalNeeded * 100 ) / 100
  228. // if basalNeeded is positive, adjust each of the 1-3 hour prior basals by 10% of the needed adjustment
  229. console.error("Hour",hour,"basal adjustment needed:",basalNeeded,"U/hr");
  230. if (basalNeeded > 0 ) {
  231. for (var offset=-3; offset < 0; offset++) {
  232. var offsetHour = hour + offset;
  233. if (offsetHour < 0) { offsetHour += 24; }
  234. //console.error(offsetHour);
  235. newHourlyBasalProfile[offsetHour].rate += basalNeeded / 3;
  236. newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000
  237. }
  238. // otherwise, figure out the percentage reduction required to the 1-3 hour prior basals
  239. // and adjust all of them downward proportionally
  240. } else if (basalNeeded < 0) {
  241. var threeHourBasal = 0;
  242. for (offset=-3; offset < 0; offset++) {
  243. offsetHour = hour + offset;
  244. if (offsetHour < 0) { offsetHour += 24; }
  245. threeHourBasal += newHourlyBasalProfile[offsetHour].rate;
  246. }
  247. var adjustmentRatio = 1.0 + basalNeeded / threeHourBasal;
  248. //console.error(adjustmentRatio);
  249. for (offset=-3; offset < 0; offset++) {
  250. offsetHour = hour + offset;
  251. if (offsetHour < 0) { offsetHour += 24; }
  252. newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio;
  253. newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000
  254. }
  255. }
  256. }
  257. if (pumpBasalProfile && pumpBasalProfile[0]) {
  258. for (hour=0; hour < 24; hour++) {
  259. //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2);
  260. // cap adjustments at autosens_max and autosens_min
  261. if (typeof pumpProfile.autosens_max !== 'undefined') {
  262. var autotuneMax = pumpProfile.autosens_max;
  263. } else {
  264. var autotuneMax = 1.2;
  265. }
  266. if (typeof pumpProfile.autosens_min !== 'undefined') {
  267. var autotuneMin = pumpProfile.autosens_min;
  268. } else {
  269. var autotuneMin = 0.7;
  270. }
  271. var maxRate = hourlyPumpProfile[hour].rate * autotuneMax;
  272. var minRate = hourlyPumpProfile[hour].rate * autotuneMin;
  273. if (newHourlyBasalProfile[hour].rate > maxRate ) {
  274. console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is",autotuneMax,"* pump basal of",hourlyPumpProfile[hour].rate,")");
  275. //console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is 20% above pump basal of",hourlyPumpProfile[hour].rate,")");
  276. newHourlyBasalProfile[hour].rate = maxRate;
  277. } else if (newHourlyBasalProfile[hour].rate < minRate ) {
  278. console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is",autotuneMin,"* pump basal of",hourlyPumpProfile[hour].rate,")");
  279. //console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is 20% below pump basal of",hourlyPumpProfile[hour].rate,")");
  280. newHourlyBasalProfile[hour].rate = minRate;
  281. }
  282. newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate*basal_scale)/basal_scale;
  283. }
  284. }
  285. // some hours of the day rarely have data to tune basals due to meals.
  286. // when no adjustments are needed to a particular hour, we should adjust it toward the average of the
  287. // periods before and after it that do have data to be tuned
  288. var lastAdjustedHour = 0;
  289. // scan through newHourlyBasalProfile and find hours where the rate is unchanged
  290. for (hour=0; hour < 24; hour++) {
  291. if (hourlyBasalProfile[hour].rate === newHourlyBasalProfile[hour].rate) {
  292. var nextAdjustedHour = 23;
  293. for (var nextHour = hour; nextHour < 24; nextHour++) {
  294. if (! (hourlyBasalProfile[nextHour].rate === newHourlyBasalProfile[nextHour].rate)) {
  295. nextAdjustedHour = nextHour;
  296. break;
  297. //} else {
  298. //console.error(nextHour, hourlyBasalProfile[nextHour].rate, newHourlyBasalProfile[nextHour].rate);
  299. }
  300. }
  301. //console.error(hour, newHourlyBasalProfile);
  302. newHourlyBasalProfile[hour].rate = Math.round( (0.8*hourlyBasalProfile[hour].rate + 0.1*newHourlyBasalProfile[lastAdjustedHour].rate + 0.1*newHourlyBasalProfile[nextAdjustedHour].rate)*basal_scale)/basal_scale;
  303. if (newHourlyBasalProfile[hour].untuned)
  304. newHourlyBasalProfile[hour].untuned++;
  305. else
  306. newHourlyBasalProfile[hour].untuned = 1;
  307. console.error("Adjusting hour",hour,"basal from",hourlyBasalProfile[hour].rate,"to",newHourlyBasalProfile[hour].rate,"based on hour",lastAdjustedHour,"=",newHourlyBasalProfile[lastAdjustedHour].rate,"and hour",nextAdjustedHour,"=",newHourlyBasalProfile[nextAdjustedHour].rate);
  308. } else {
  309. lastAdjustedHour = hour;
  310. }
  311. }
  312. console.error(newHourlyBasalProfile);
  313. basalProfile = newHourlyBasalProfile;
  314. // Calculate carb ratio (CR) independently of CSF and ISF
  315. // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
  316. // For now, if another meal IOB/COB stacks on top of it, consider them together
  317. // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
  318. // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
  319. // calculate net deviations while carbs are absorbing
  320. // measured from carb entry until COB and deviations both drop to zero
  321. var deviations = 0;
  322. var mealCarbs = 0;
  323. var totalMealCarbs = 0;
  324. var totalDeviations = 0;
  325. var fullNewCSF;
  326. //console.error(CSFGlucose[0].mealAbsorption);
  327. //console.error(CSFGlucose[0]);
  328. for (i=0; i < CSFGlucose.length; ++i) {
  329. //console.error(CSFGlucose[i].mealAbsorption, i);
  330. if ( CSFGlucose[i].mealAbsorption === "start" ) {
  331. deviations = 0;
  332. mealCarbs = parseInt(CSFGlucose[i].mealCarbs);
  333. } else if (CSFGlucose[i].mealAbsorption === "end") {
  334. deviations += parseFloat(CSFGlucose[i].deviation);
  335. // compare the sum of deviations from start to end vs. current CSF * mealCarbs
  336. //console.error(CSF,mealCarbs);
  337. var csfRise = CSF * mealCarbs;
  338. //console.error(deviations,ISF);
  339. //console.error("csfRise:",csfRise,"deviations:",deviations);
  340. totalMealCarbs += mealCarbs;
  341. totalDeviations += deviations;
  342. } else {
  343. deviations += Math.max(0*previousAutotune.min_5m_carbimpact,parseFloat(CSFGlucose[i].deviation));
  344. mealCarbs = Math.max(mealCarbs, parseInt(CSFGlucose[i].mealCarbs));
  345. }
  346. }
  347. // at midnight, write down the mealcarbs as total meal carbs (to prevent special case of when only one meal and it not finishing absorbing by midnight)
  348. // TODO: figure out what to do with dinner carbs that don't finish absorbing by midnight
  349. if (totalMealCarbs === 0) { totalMealCarbs += mealCarbs; }
  350. if (totalDeviations === 0) { totalDeviations += deviations; }
  351. //console.error(totalDeviations, totalMealCarbs);
  352. if (totalMealCarbs === 0) {
  353. // if no meals today, CSF is unchanged
  354. fullNewCSF = CSF;
  355. } else {
  356. // how much change would be required to account for all of the deviations
  357. fullNewCSF = Math.round( (totalDeviations / totalMealCarbs)*100 )/100;
  358. }
  359. // only adjust by 20%
  360. var newCSF = ( 0.8 * CSF ) + ( 0.2 * fullNewCSF );
  361. // safety cap CSF
  362. if (typeof(pumpCSF) !== 'undefined') {
  363. var maxCSF = pumpCSF * autotuneMax;
  364. var minCSF = pumpCSF * autotuneMin;
  365. if (newCSF > maxCSF) {
  366. console.error("Limiting CSF to",maxCSF.toFixed(2),"(which is",autotuneMax,"* pump CSF of",pumpCSF,")");
  367. newCSF = maxCSF;
  368. } else if (newCSF < minCSF) {
  369. console.error("Limiting CSF to",minCSF.toFixed(2),"(which is",autotuneMin,"* pump CSF of",pumpCSF,")");
  370. newCSF = minCSF;
  371. } //else { console.error("newCSF",newCSF,"is close enough to",pumpCSF); }
  372. }
  373. var oldCSF = Math.round( CSF * 1000 ) / 1000;
  374. newCSF = Math.round( newCSF * 1000 ) / 1000;
  375. totalDeviations = Math.round ( totalDeviations * 1000 )/1000;
  376. console.error("totalMealCarbs:",totalMealCarbs,"totalDeviations:",totalDeviations,"oldCSF",oldCSF,"fullNewCSF:",fullNewCSF,"newCSF:",newCSF);
  377. // this is where CSF is set based on the outputs
  378. if (newCSF) {
  379. CSF = newCSF;
  380. }
  381. if (totalCR === 0) {
  382. // if no meals today, CR is unchanged
  383. var fullNewCR = carbRatio;
  384. } else {
  385. // how much change would be required to account for all of the deviations
  386. fullNewCR = totalCR;
  387. }
  388. // don't tune CR out of bounds
  389. var maxCR = pumpCarbRatio * autotuneMax;
  390. if (maxCR > 150) { maxCR = 150 }
  391. var minCR = pumpCarbRatio * autotuneMin;
  392. if (minCR < 1) { minCR = 1 }
  393. // safety cap fullNewCR
  394. if (typeof(pumpCarbRatio) !== 'undefined') {
  395. if (fullNewCR > maxCR) {
  396. console.error("Limiting fullNewCR from",fullNewCR,"to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")");
  397. fullNewCR = maxCR;
  398. } else if (fullNewCR < minCR) {
  399. console.error("Limiting fullNewCR from",fullNewCR,"to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")");
  400. fullNewCR = minCR;
  401. } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); }
  402. }
  403. // only adjust by 20%
  404. var newCR = ( 0.8 * carbRatio ) + ( 0.2 * fullNewCR );
  405. // safety cap newCR
  406. if (typeof(pumpCarbRatio) !== 'undefined') {
  407. if (newCR > maxCR) {
  408. console.error("Limiting CR to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")");
  409. newCR = maxCR;
  410. } else if (newCR < minCR) {
  411. console.error("Limiting CR to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")");
  412. newCR = minCR;
  413. } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); }
  414. }
  415. newCR = Math.round( newCR * 1000 ) / 1000;
  416. console.error("oldCR:",carbRatio,"fullNewCR:",fullNewCR,"newCR:",newCR);
  417. // this is where CR is set based on the outputs
  418. //var ISFFromCRAndCSF = ISF;
  419. if (newCR) {
  420. carbRatio = newCR;
  421. //ISFFromCRAndCSF = Math.round( carbRatio * CSF * 1000)/1000;
  422. }
  423. // calculate median deviation and bgi in data attributable to ISF
  424. var deviations = [];
  425. var BGIs = [];
  426. var avgDeltas = [];
  427. var ratios = [];
  428. for (i=0; i < ISFGlucose.length; ++i) {
  429. deviation = parseFloat(ISFGlucose[i].deviation);
  430. deviations.push(deviation);
  431. var BGI = parseFloat(ISFGlucose[i].BGI);
  432. BGIs.push(BGI);
  433. var avgDelta = parseFloat(ISFGlucose[i].avgDelta);
  434. avgDeltas.push(avgDelta);
  435. var ratio = 1 + deviation / BGI;
  436. //console.error("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio);
  437. ratios.push(ratio);
  438. }
  439. avgDeltas.sort(function(a, b){return a-b});
  440. BGIs.sort(function(a, b){return a-b});
  441. deviations.sort(function(a, b){return a-b});
  442. ratios.sort(function(a, b){return a-b});
  443. var p50deviation = percentile(deviations, 0.50);
  444. var p50BGI = percentile(BGIs, 0.50);
  445. var p50ratios = Math.round( percentile(ratios, 0.50) * 1000)/1000;
  446. var fullNewISF = ISF;
  447. if (ISFGlucose.length < 10) {
  448. // leave ISF unchanged if fewer than 10 ISF data points
  449. console.error ("Only found",ISFGlucose.length,"ISF data points, leaving ISF unchanged at",ISF);
  450. } else {
  451. // calculate what adjustments to ISF would have been necessary to bring median deviation to zero
  452. fullNewISF = ISF * p50ratios;
  453. }
  454. fullNewISF = Math.round( fullNewISF * 1000 ) / 1000;
  455. //console.error("p50ratios:",p50ratios,"fullNewISF:",fullNewISF,ratios[Math.floor(ratios.length/2)]);
  456. // adjust the target ISF to be a weighted average of fullNewISF and pumpISF
  457. var adjustmentFraction;
  458. if (typeof(pumpProfile.autotune_isf_adjustmentFraction) !== 'undefined') {
  459. adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction;
  460. } else {
  461. adjustmentFraction = 1.0;
  462. }
  463. // low autosens ratio = high ISF
  464. var maxISF = pumpISF / autotuneMin;
  465. // high autosens ratio = low ISF
  466. var minISF = pumpISF / autotuneMax;
  467. if (typeof(pumpISF) !== 'undefined') {
  468. if ( fullNewISF < 0 ) {
  469. var adjustedISF = ISF;
  470. } else {
  471. adjustedISF = adjustmentFraction*fullNewISF + (1-adjustmentFraction)*pumpISF;
  472. }
  473. // cap adjustedISF before applying 10%
  474. //console.error(adjustedISF, maxISF, minISF);
  475. if (adjustedISF > maxISF) {
  476. console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")");
  477. adjustedISF = maxISF;
  478. } else if (adjustedISF < minISF) {
  479. console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")");
  480. adjustedISF = minISF;
  481. }
  482. // and apply 20% of that adjustment
  483. var newISF = ( 0.8 * ISF ) + ( 0.2 * adjustedISF );
  484. if (newISF > maxISF) {
  485. console.error("Limiting ISF of",newISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")");
  486. newISF = maxISF;
  487. } else if (newISF < minISF) {
  488. console.error("Limiting ISF of",newISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")");
  489. newISF = minISF;
  490. }
  491. }
  492. newISF = Math.round( newISF * 1000 ) / 1000;
  493. //console.error(avgRatio);
  494. //console.error(newISF);
  495. p50deviation = Math.round( p50deviation * 1000 ) / 1000;
  496. p50BGI = Math.round( p50BGI * 1000 ) / 1000;
  497. adjustedISF = Math.round( adjustedISF * 1000 ) / 1000;
  498. console.error("p50deviation:",p50deviation,"p50BGI",p50BGI,"p50ratios:",p50ratios,"Old ISF:",ISF,"fullNewISF:",fullNewISF,"adjustedISF:",adjustedISF,"newISF:",newISF,"newDIA:",newDIA,"newPeak:",newPeak);
  499. if (newISF) {
  500. ISF = newISF;
  501. }
  502. // reconstruct updated version of previousAutotune as autotuneOutput
  503. var autotuneOutput = previousAutotune;
  504. autotuneOutput.basalprofile = basalProfile;
  505. isfProfile.sensitivities[0].sensitivity = ISF;
  506. autotuneOutput.isfProfile = isfProfile;
  507. autotuneOutput.sens = ISF;
  508. autotuneOutput.csf = CSF;
  509. //carbRatio = ISF / CSF;
  510. carbRatio = Math.round( carbRatio * 1000 ) / 1000;
  511. autotuneOutput.carb_ratio = carbRatio;
  512. autotuneOutput.dia = newDIA;
  513. autotuneOutput.insulinPeakTime = newPeak;
  514. if (diaDeviations || peakDeviations) {
  515. autotuneOutput.useCustomPeakTime = true;
  516. }
  517. return autotuneOutput;
  518. }
  519. exports = module.exports = tuneAllTheThings;