|
|
@@ -0,0 +1,554 @@
|
|
|
+var percentile = require('../percentile');
|
|
|
+//rounding of basals for pumps
|
|
|
+//20 for omnipod to 0.05
|
|
|
+//40 for medtronic to 0.025
|
|
|
+var basal_scale = 20;
|
|
|
+// does three things - tunes basals, ISF, and CSF
|
|
|
+
|
|
|
+function tuneAllTheThings (inputs) {
|
|
|
+
|
|
|
+ var previousAutotune = inputs.previousAutotune;
|
|
|
+ //console.error(previousAutotune);
|
|
|
+ var pumpProfile = inputs.pumpProfile;
|
|
|
+ var pumpBasalProfile = pumpProfile.basalprofile;
|
|
|
+ //console.error(pumpBasalProfile);
|
|
|
+ var basalProfile = previousAutotune.basalprofile;
|
|
|
+ //console.error(basalProfile);
|
|
|
+ var isfProfile = previousAutotune.isfProfile;
|
|
|
+ //console.error(isfProfile);
|
|
|
+ var ISF = isfProfile.sensitivities[0].sensitivity;
|
|
|
+ //console.error(ISF);
|
|
|
+ var carbRatio = previousAutotune.carb_ratio;
|
|
|
+ //console.error(carbRatio);
|
|
|
+ var CSF = ISF / carbRatio;
|
|
|
+ var DIA = previousAutotune.dia;
|
|
|
+ var peak = previousAutotune.insulinPeakTime;
|
|
|
+ if (! previousAutotune.useCustomPeakTime === true) {
|
|
|
+ if ( previousAutotune.curve === "ultra-rapid" ) {
|
|
|
+ peak = 55;
|
|
|
+ } else {
|
|
|
+ peak = 75;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //console.error(DIA, peak);
|
|
|
+
|
|
|
+ // conditional on there being a pump profile; if not then skip
|
|
|
+ if (pumpProfile) { var pumpISFProfile = pumpProfile.isfProfile; }
|
|
|
+ if (pumpISFProfile && pumpISFProfile.sensitivities[0]) {
|
|
|
+ var pumpISF = pumpISFProfile.sensitivities[0].sensitivity;
|
|
|
+ var pumpCarbRatio = pumpProfile.carb_ratio;
|
|
|
+ var pumpCSF = pumpISF / pumpCarbRatio;
|
|
|
+ }
|
|
|
+ if (! carbRatio) { carbRatio = pumpCarbRatio; }
|
|
|
+ if (! CSF) { CSF = pumpCSF; }
|
|
|
+ if (! ISF) { ISF = pumpISF; }
|
|
|
+ //console.error(CSF);
|
|
|
+ var preppedGlucose = inputs.preppedGlucose;
|
|
|
+ var CSFGlucose = preppedGlucose.CSFGlucoseData;
|
|
|
+ //console.error(CSFGlucose[0]);
|
|
|
+ var ISFGlucose = preppedGlucose.ISFGlucoseData;
|
|
|
+ //console.error(ISFGlucose[0]);
|
|
|
+ var basalGlucose = preppedGlucose.basalGlucoseData;
|
|
|
+ //console.error(basalGlucose[0]);
|
|
|
+ var CRData = preppedGlucose.CRData;
|
|
|
+ //console.error(CRData);
|
|
|
+ var diaDeviations = preppedGlucose.diaDeviations;
|
|
|
+ //console.error(diaDeviations);
|
|
|
+ var peakDeviations = preppedGlucose.peakDeviations;
|
|
|
+ //console.error(peakDeviations);
|
|
|
+
|
|
|
+ // tune DIA
|
|
|
+ var newDIA = DIA;
|
|
|
+ if (diaDeviations) {
|
|
|
+ var currentDIAMeanDev = diaDeviations[2].meanDeviation;
|
|
|
+ var currentDIARMSDev = diaDeviations[2].RMSDeviation;
|
|
|
+ //console.error(DIA,currentDIAMeanDev,currentDIARMSDev);
|
|
|
+ var minMeanDeviations = 1000000;
|
|
|
+ var minRMSDeviations = 1000000;
|
|
|
+ var meanBest = 2;
|
|
|
+ var RMSBest = 2;
|
|
|
+ for (var i=0; i < diaDeviations.length; i++) {
|
|
|
+ var meanDeviations = diaDeviations[i].meanDeviation;
|
|
|
+ var RMSDeviations = diaDeviations[i].RMSDeviation;
|
|
|
+ if (meanDeviations < minMeanDeviations) {
|
|
|
+ minMeanDeviations = Math.round(meanDeviations*1000)/1000;
|
|
|
+ meanBest = i;
|
|
|
+ }
|
|
|
+ if (RMSDeviations < minRMSDeviations) {
|
|
|
+ minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
|
|
|
+ RMSBest = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.error("Best insulinEndTime for meanDeviations:",diaDeviations[meanBest].dia,"hours");
|
|
|
+ console.error("Best insulinEndTime for RMSDeviations:",diaDeviations[RMSBest].dia,"hours");
|
|
|
+ if ( meanBest < 2 && RMSBest < 2 ) {
|
|
|
+ if ( diaDeviations[1].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[1].RMSDeviation < currentDIARMSDev * 0.99 ) {
|
|
|
+ newDIA = diaDeviations[1].dia;
|
|
|
+ }
|
|
|
+ } else if ( meanBest > 2 && RMSBest > 2 ) {
|
|
|
+ if ( diaDeviations[3].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[3].RMSDeviation < currentDIARMSDev * 0.99 ) {
|
|
|
+ newDIA = diaDeviations[3].dia;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ( newDIA > 12 ) {
|
|
|
+ console.error("insulinEndTime maximum is 12h: not raising further");
|
|
|
+ newDIA=12;
|
|
|
+ }
|
|
|
+ if ( newDIA !== DIA ) {
|
|
|
+ console.error("Adjusting insulinEndTime from",DIA,"to",newDIA,"hours");
|
|
|
+ } else {
|
|
|
+ console.error("Leaving insulinEndTime unchanged at",DIA,"hours");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // tune insulinPeakTime
|
|
|
+ var newPeak = peak;
|
|
|
+ if (peakDeviations && peakDeviations[2]) {
|
|
|
+ var currentPeakMeanDev = peakDeviations[2].meanDeviation;
|
|
|
+ var currentPeakRMSDev = peakDeviations[2].RMSDeviation;
|
|
|
+ //console.error(currentPeakMeanDev);
|
|
|
+ minMeanDeviations = 1000000;
|
|
|
+ minRMSDeviations = 1000000;
|
|
|
+ meanBest = 2;
|
|
|
+ RMSBest = 2;
|
|
|
+ for (i=0; i < peakDeviations.length; i++) {
|
|
|
+ meanDeviations = peakDeviations[i].meanDeviation;
|
|
|
+ RMSDeviations = peakDeviations[i].RMSDeviation;
|
|
|
+ if (meanDeviations < minMeanDeviations) {
|
|
|
+ minMeanDeviations = Math.round(meanDeviations*1000)/1000;
|
|
|
+ meanBest = i;
|
|
|
+ }
|
|
|
+ if (RMSDeviations < minRMSDeviations) {
|
|
|
+ minRMSDeviations = Math.round(RMSDeviations*1000)/1000;
|
|
|
+ RMSBest = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.error("Best insulinPeakTime for meanDeviations:",peakDeviations[meanBest].peak,"minutes");
|
|
|
+ console.error("Best insulinPeakTime for RMSDeviations:",peakDeviations[RMSBest].peak,"minutes");
|
|
|
+ if ( meanBest < 2 && RMSBest < 2 ) {
|
|
|
+ if ( peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[1].RMSDeviation < currentPeakRMSDev * 0.99 ) {
|
|
|
+ newPeak = peakDeviations[1].peak;
|
|
|
+ }
|
|
|
+ } else if ( meanBest > 2 && RMSBest > 2 ) {
|
|
|
+ if ( peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[3].RMSDeviation < currentPeakRMSDev * 0.99 ) {
|
|
|
+ newPeak = peakDeviations[3].peak;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ( newPeak !== peak ) {
|
|
|
+ console.error("Adjusting insulinPeakTime from",peak,"to",newPeak,"minutes");
|
|
|
+ } else {
|
|
|
+ console.error("Leaving insulinPeakTime unchanged at",peak);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // Calculate carb ratio (CR) independently of CSF and ISF
|
|
|
+ // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
|
|
|
+ // For now, if another meal IOB/COB stacks on top of it, consider them together
|
|
|
+ // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
|
|
|
+ // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
|
|
|
+
|
|
|
+ var CRTotalCarbs = 0;
|
|
|
+ var CRTotalInsulin = 0;
|
|
|
+ CRData.forEach(function(CRDatum) {
|
|
|
+ var CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG;
|
|
|
+ var CRInsulinReq = CRBGChange / ISF;
|
|
|
+ var CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB;
|
|
|
+ CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq;
|
|
|
+ //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRDatum.CRInsulinTotal);
|
|
|
+ var CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000;
|
|
|
+ //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRDatum.CRInsulinTotal);
|
|
|
+ //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulin,"CRDatum.CRInsulinTotal:",CRDatum.CRInsulinTotal,"CR:",CR);
|
|
|
+ if (CRDatum.CRInsulinTotal > 0) {
|
|
|
+ CRTotalCarbs += CRDatum.CRCarbs;
|
|
|
+ CRTotalInsulin += CRDatum.CRInsulinTotal;
|
|
|
+ //console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ CRTotalInsulin = Math.round(CRTotalInsulin*1000)/1000;
|
|
|
+ var totalCR = Math.round( CRTotalCarbs / CRTotalInsulin * 1000 )/1000;
|
|
|
+ console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin,"totalCR:",totalCR);
|
|
|
+
|
|
|
+ // convert the basal profile to hourly if it isn't already
|
|
|
+ var hourlyBasalProfile = [];
|
|
|
+ var hourlyPumpProfile = [];
|
|
|
+ for (i=0; i < 24; i++) {
|
|
|
+ // autotuned basal profile
|
|
|
+ for (var j=0; j < basalProfile.length; ++j) {
|
|
|
+ if (basalProfile[j].minutes <= i * 60) {
|
|
|
+ if (basalProfile[j].rate === 0) {
|
|
|
+ console.error("ERROR: bad basalProfile",basalProfile[j]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ hourlyBasalProfile[i] = JSON.parse(JSON.stringify(basalProfile[j]));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hourlyBasalProfile[i].i=i;
|
|
|
+ hourlyBasalProfile[i].minutes=i*60;
|
|
|
+ var zeroPadHour = ("000"+i).slice(-2);
|
|
|
+ hourlyBasalProfile[i].start=zeroPadHour + ":00:00";
|
|
|
+ hourlyBasalProfile[i].rate=Math.round(hourlyBasalProfile[i].rate*1000)/1000
|
|
|
+ // pump basal profile
|
|
|
+ if (pumpBasalProfile && pumpBasalProfile[0]) {
|
|
|
+ for (j=0; j < pumpBasalProfile.length; ++j) {
|
|
|
+ //console.error(pumpBasalProfile[j]);
|
|
|
+ if (pumpBasalProfile[j].rate === 0) {
|
|
|
+ console.error("ERROR: bad pumpBasalProfile",pumpBasalProfile[j]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (pumpBasalProfile[j].minutes <= i * 60) {
|
|
|
+ hourlyPumpProfile[i] = JSON.parse(JSON.stringify(pumpBasalProfile[j]));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hourlyPumpProfile[i].i=i;
|
|
|
+ hourlyPumpProfile[i].minutes=i*60;
|
|
|
+ hourlyPumpProfile[i].rate=Math.round(hourlyPumpProfile[i].rate*1000)/1000
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //console.error(hourlyPumpProfile);
|
|
|
+ //console.error(hourlyBasalProfile);
|
|
|
+ var newHourlyBasalProfile = JSON.parse(JSON.stringify(hourlyBasalProfile));
|
|
|
+
|
|
|
+ // look at net deviations for each hour
|
|
|
+ for (var hour=0; hour < 24; hour++) {
|
|
|
+ var deviations = 0;
|
|
|
+ for (i=0; i < basalGlucose.length; ++i) {
|
|
|
+ var BGTime;
|
|
|
+
|
|
|
+ if (basalGlucose[i].date) {
|
|
|
+ BGTime = new Date(basalGlucose[i].date);
|
|
|
+ } else if (basalGlucose[i].displayTime) {
|
|
|
+ BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' '));
|
|
|
+ } else if (basalGlucose[i].dateString) {
|
|
|
+ BGTime = new Date(basalGlucose[i].dateString);
|
|
|
+ } else {
|
|
|
+ console.error("Could not determine last BG time");
|
|
|
+ }
|
|
|
+
|
|
|
+ var myHour = BGTime.getHours();
|
|
|
+ if (hour === myHour) {
|
|
|
+ //console.error(basalGlucose[i].deviation);
|
|
|
+ deviations += parseFloat(basalGlucose[i].deviation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ deviations = Math.round( deviations * 1000 ) / 1000
|
|
|
+ console.error("Hour",hour.toString(),"total deviations:",deviations,"mg/dL");
|
|
|
+ // calculate how much less or additional basal insulin would have been required to eliminate the deviations
|
|
|
+ // only apply 20% of the needed adjustment to keep things relatively stable
|
|
|
+ var basalNeeded = 0.2 * deviations / ISF;
|
|
|
+ basalNeeded = Math.round( basalNeeded * 100 ) / 100
|
|
|
+ // if basalNeeded is positive, adjust each of the 1-3 hour prior basals by 10% of the needed adjustment
|
|
|
+ console.error("Hour",hour,"basal adjustment needed:",basalNeeded,"U/hr");
|
|
|
+ if (basalNeeded > 0 ) {
|
|
|
+ for (var offset=-3; offset < 0; offset++) {
|
|
|
+ var offsetHour = hour + offset;
|
|
|
+ if (offsetHour < 0) { offsetHour += 24; }
|
|
|
+ //console.error(offsetHour);
|
|
|
+ newHourlyBasalProfile[offsetHour].rate += basalNeeded / 3;
|
|
|
+ newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000
|
|
|
+ }
|
|
|
+ // otherwise, figure out the percentage reduction required to the 1-3 hour prior basals
|
|
|
+ // and adjust all of them downward proportionally
|
|
|
+ } else if (basalNeeded < 0) {
|
|
|
+ var threeHourBasal = 0;
|
|
|
+ for (offset=-3; offset < 0; offset++) {
|
|
|
+ offsetHour = hour + offset;
|
|
|
+ if (offsetHour < 0) { offsetHour += 24; }
|
|
|
+ threeHourBasal += newHourlyBasalProfile[offsetHour].rate;
|
|
|
+ }
|
|
|
+ var adjustmentRatio = 1.0 + basalNeeded / threeHourBasal;
|
|
|
+ //console.error(adjustmentRatio);
|
|
|
+ for (offset=-3; offset < 0; offset++) {
|
|
|
+ offsetHour = hour + offset;
|
|
|
+ if (offsetHour < 0) { offsetHour += 24; }
|
|
|
+ newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio;
|
|
|
+ newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (pumpBasalProfile && pumpBasalProfile[0]) {
|
|
|
+ for (hour=0; hour < 24; hour++) {
|
|
|
+ //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2);
|
|
|
+ // cap adjustments at autosens_max and autosens_min
|
|
|
+ if (typeof pumpProfile.autosens_max !== 'undefined') {
|
|
|
+ var autotuneMax = pumpProfile.autosens_max;
|
|
|
+ } else {
|
|
|
+ var autotuneMax = 1.2;
|
|
|
+ }
|
|
|
+ if (typeof pumpProfile.autosens_min !== 'undefined') {
|
|
|
+ var autotuneMin = pumpProfile.autosens_min;
|
|
|
+ } else {
|
|
|
+ var autotuneMin = 0.7;
|
|
|
+ }
|
|
|
+ var maxRate = hourlyPumpProfile[hour].rate * autotuneMax;
|
|
|
+ var minRate = hourlyPumpProfile[hour].rate * autotuneMin;
|
|
|
+ if (newHourlyBasalProfile[hour].rate > maxRate ) {
|
|
|
+ console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is",autotuneMax,"* pump basal of",hourlyPumpProfile[hour].rate,")");
|
|
|
+ //console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is 20% above pump basal of",hourlyPumpProfile[hour].rate,")");
|
|
|
+ newHourlyBasalProfile[hour].rate = maxRate;
|
|
|
+ } else if (newHourlyBasalProfile[hour].rate < minRate ) {
|
|
|
+ console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is",autotuneMin,"* pump basal of",hourlyPumpProfile[hour].rate,")");
|
|
|
+ //console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is 20% below pump basal of",hourlyPumpProfile[hour].rate,")");
|
|
|
+ newHourlyBasalProfile[hour].rate = minRate;
|
|
|
+ }
|
|
|
+ newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate*basal_scale)/basal_scale;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // some hours of the day rarely have data to tune basals due to meals.
|
|
|
+ // when no adjustments are needed to a particular hour, we should adjust it toward the average of the
|
|
|
+ // periods before and after it that do have data to be tuned
|
|
|
+
|
|
|
+ var lastAdjustedHour = 0;
|
|
|
+ // scan through newHourlyBasalProfile and find hours where the rate is unchanged
|
|
|
+ for (hour=0; hour < 24; hour++) {
|
|
|
+ if (hourlyBasalProfile[hour].rate === newHourlyBasalProfile[hour].rate) {
|
|
|
+ var nextAdjustedHour = 23;
|
|
|
+ for (var nextHour = hour; nextHour < 24; nextHour++) {
|
|
|
+ if (! (hourlyBasalProfile[nextHour].rate === newHourlyBasalProfile[nextHour].rate)) {
|
|
|
+ nextAdjustedHour = nextHour;
|
|
|
+ break;
|
|
|
+ //} else {
|
|
|
+ //console.error(nextHour, hourlyBasalProfile[nextHour].rate, newHourlyBasalProfile[nextHour].rate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //console.error(hour, newHourlyBasalProfile);
|
|
|
+ newHourlyBasalProfile[hour].rate = Math.round( (0.8*hourlyBasalProfile[hour].rate + 0.1*newHourlyBasalProfile[lastAdjustedHour].rate + 0.1*newHourlyBasalProfile[nextAdjustedHour].rate)*basal_scale)/basal_scale;
|
|
|
+ if (newHourlyBasalProfile[hour].untuned)
|
|
|
+ newHourlyBasalProfile[hour].untuned++;
|
|
|
+ else
|
|
|
+ newHourlyBasalProfile[hour].untuned = 1;
|
|
|
+ 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);
|
|
|
+ } else {
|
|
|
+ lastAdjustedHour = hour;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.error(newHourlyBasalProfile);
|
|
|
+ basalProfile = newHourlyBasalProfile;
|
|
|
+
|
|
|
+ // Calculate carb ratio (CR) independently of CSF and ISF
|
|
|
+ // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
|
|
|
+ // For now, if another meal IOB/COB stacks on top of it, consider them together
|
|
|
+ // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
|
|
|
+ // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // calculate net deviations while carbs are absorbing
|
|
|
+ // measured from carb entry until COB and deviations both drop to zero
|
|
|
+
|
|
|
+ var deviations = 0;
|
|
|
+ var mealCarbs = 0;
|
|
|
+ var totalMealCarbs = 0;
|
|
|
+ var totalDeviations = 0;
|
|
|
+ var fullNewCSF;
|
|
|
+ //console.error(CSFGlucose[0].mealAbsorption);
|
|
|
+ //console.error(CSFGlucose[0]);
|
|
|
+ for (i=0; i < CSFGlucose.length; ++i) {
|
|
|
+ //console.error(CSFGlucose[i].mealAbsorption, i);
|
|
|
+ if ( CSFGlucose[i].mealAbsorption === "start" ) {
|
|
|
+ deviations = 0;
|
|
|
+ mealCarbs = parseInt(CSFGlucose[i].mealCarbs);
|
|
|
+ } else if (CSFGlucose[i].mealAbsorption === "end") {
|
|
|
+ deviations += parseFloat(CSFGlucose[i].deviation);
|
|
|
+ // compare the sum of deviations from start to end vs. current CSF * mealCarbs
|
|
|
+ //console.error(CSF,mealCarbs);
|
|
|
+ var csfRise = CSF * mealCarbs;
|
|
|
+ //console.error(deviations,ISF);
|
|
|
+ //console.error("csfRise:",csfRise,"deviations:",deviations);
|
|
|
+ totalMealCarbs += mealCarbs;
|
|
|
+ totalDeviations += deviations;
|
|
|
+
|
|
|
+ } else {
|
|
|
+ deviations += Math.max(0*previousAutotune.min_5m_carbimpact,parseFloat(CSFGlucose[i].deviation));
|
|
|
+ mealCarbs = Math.max(mealCarbs, parseInt(CSFGlucose[i].mealCarbs));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 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)
|
|
|
+ // TODO: figure out what to do with dinner carbs that don't finish absorbing by midnight
|
|
|
+ if (totalMealCarbs === 0) { totalMealCarbs += mealCarbs; }
|
|
|
+ if (totalDeviations === 0) { totalDeviations += deviations; }
|
|
|
+ //console.error(totalDeviations, totalMealCarbs);
|
|
|
+ if (totalMealCarbs === 0) {
|
|
|
+ // if no meals today, CSF is unchanged
|
|
|
+ fullNewCSF = CSF;
|
|
|
+ } else {
|
|
|
+ // how much change would be required to account for all of the deviations
|
|
|
+ fullNewCSF = Math.round( (totalDeviations / totalMealCarbs)*100 )/100;
|
|
|
+ }
|
|
|
+ // only adjust by 20%
|
|
|
+ var newCSF = ( 0.8 * CSF ) + ( 0.2 * fullNewCSF );
|
|
|
+ // safety cap CSF
|
|
|
+ if (typeof(pumpCSF) !== 'undefined') {
|
|
|
+ var maxCSF = pumpCSF * autotuneMax;
|
|
|
+ var minCSF = pumpCSF * autotuneMin;
|
|
|
+ if (newCSF > maxCSF) {
|
|
|
+ console.error("Limiting CSF to",maxCSF.toFixed(2),"(which is",autotuneMax,"* pump CSF of",pumpCSF,")");
|
|
|
+ newCSF = maxCSF;
|
|
|
+ } else if (newCSF < minCSF) {
|
|
|
+ console.error("Limiting CSF to",minCSF.toFixed(2),"(which is",autotuneMin,"* pump CSF of",pumpCSF,")");
|
|
|
+ newCSF = minCSF;
|
|
|
+ } //else { console.error("newCSF",newCSF,"is close enough to",pumpCSF); }
|
|
|
+ }
|
|
|
+ var oldCSF = Math.round( CSF * 1000 ) / 1000;
|
|
|
+ newCSF = Math.round( newCSF * 1000 ) / 1000;
|
|
|
+ totalDeviations = Math.round ( totalDeviations * 1000 )/1000;
|
|
|
+ console.error("totalMealCarbs:",totalMealCarbs,"totalDeviations:",totalDeviations,"oldCSF",oldCSF,"fullNewCSF:",fullNewCSF,"newCSF:",newCSF);
|
|
|
+ // this is where CSF is set based on the outputs
|
|
|
+ if (newCSF) {
|
|
|
+ CSF = newCSF;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (totalCR === 0) {
|
|
|
+ // if no meals today, CR is unchanged
|
|
|
+ var fullNewCR = carbRatio;
|
|
|
+ } else {
|
|
|
+ // how much change would be required to account for all of the deviations
|
|
|
+ fullNewCR = totalCR;
|
|
|
+ }
|
|
|
+ // don't tune CR out of bounds
|
|
|
+ var maxCR = pumpCarbRatio * autotuneMax;
|
|
|
+ if (maxCR > 150) { maxCR = 150 }
|
|
|
+ var minCR = pumpCarbRatio * autotuneMin;
|
|
|
+ if (minCR < 1) { minCR = 3 }
|
|
|
+ // safety cap fullNewCR
|
|
|
+ if (typeof(pumpCarbRatio) !== 'undefined') {
|
|
|
+ if (fullNewCR > maxCR) {
|
|
|
+ console.error("Limiting fullNewCR from",fullNewCR,"to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")");
|
|
|
+ fullNewCR = maxCR;
|
|
|
+ } else if (fullNewCR < minCR) {
|
|
|
+ console.error("Limiting fullNewCR from",fullNewCR,"to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")");
|
|
|
+ fullNewCR = minCR;
|
|
|
+ } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); }
|
|
|
+ }
|
|
|
+ // only adjust by 20%
|
|
|
+ var newCR = ( 0.8 * carbRatio ) + ( 0.2 * fullNewCR );
|
|
|
+ // safety cap newCR
|
|
|
+ if (typeof(pumpCarbRatio) !== 'undefined') {
|
|
|
+ if (newCR > maxCR) {
|
|
|
+ console.error("Limiting CR to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")");
|
|
|
+ newCR = maxCR;
|
|
|
+ } else if (newCR < minCR) {
|
|
|
+ console.error("Limiting CR to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")");
|
|
|
+ newCR = minCR;
|
|
|
+ } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); }
|
|
|
+ }
|
|
|
+ newCR = Math.round( newCR * 1000 ) / 1000;
|
|
|
+ console.error("oldCR:",carbRatio,"fullNewCR:",fullNewCR,"newCR:",newCR);
|
|
|
+ // this is where CR is set based on the outputs
|
|
|
+ //var ISFFromCRAndCSF = ISF;
|
|
|
+ if (newCR) {
|
|
|
+ carbRatio = newCR;
|
|
|
+ //ISFFromCRAndCSF = Math.round( carbRatio * CSF * 1000)/1000;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // calculate median deviation and bgi in data attributable to ISF
|
|
|
+ var deviations = [];
|
|
|
+ var BGIs = [];
|
|
|
+ var avgDeltas = [];
|
|
|
+ var ratios = [];
|
|
|
+ for (i=0; i < ISFGlucose.length; ++i) {
|
|
|
+ deviation = parseFloat(ISFGlucose[i].deviation);
|
|
|
+ deviations.push(deviation);
|
|
|
+ var BGI = parseFloat(ISFGlucose[i].BGI);
|
|
|
+ BGIs.push(BGI);
|
|
|
+ var avgDelta = parseFloat(ISFGlucose[i].avgDelta);
|
|
|
+ avgDeltas.push(avgDelta);
|
|
|
+ var ratio = 1 + deviation / BGI;
|
|
|
+ //console.error("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio);
|
|
|
+ ratios.push(ratio);
|
|
|
+ }
|
|
|
+ avgDeltas.sort(function(a, b){return a-b});
|
|
|
+ BGIs.sort(function(a, b){return a-b});
|
|
|
+ deviations.sort(function(a, b){return a-b});
|
|
|
+ ratios.sort(function(a, b){return a-b});
|
|
|
+ var p50deviation = percentile(deviations, 0.50);
|
|
|
+ var p50BGI = percentile(BGIs, 0.50);
|
|
|
+ var p50ratios = Math.round( percentile(ratios, 0.50) * 1000)/1000;
|
|
|
+ var fullNewISF = ISF;
|
|
|
+ if (ISFGlucose.length < 10) {
|
|
|
+ // leave ISF unchanged if fewer than 10 ISF data points
|
|
|
+ console.error ("Only found",ISFGlucose.length,"ISF data points, leaving ISF unchanged at",ISF);
|
|
|
+ } else {
|
|
|
+ // calculate what adjustments to ISF would have been necessary to bring median deviation to zero
|
|
|
+ fullNewISF = ISF * p50ratios;
|
|
|
+ }
|
|
|
+ fullNewISF = Math.round( fullNewISF * 1000 ) / 1000;
|
|
|
+ //console.error("p50ratios:",p50ratios,"fullNewISF:",fullNewISF,ratios[Math.floor(ratios.length/2)]);
|
|
|
+ // adjust the target ISF to be a weighted average of fullNewISF and pumpISF
|
|
|
+ var adjustmentFraction;
|
|
|
+
|
|
|
+ if (typeof(pumpProfile.autotune_isf_adjustmentFraction) !== 'undefined') {
|
|
|
+ adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction;
|
|
|
+ } else {
|
|
|
+ adjustmentFraction = 1.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // low autosens ratio = high ISF
|
|
|
+ var maxISF = pumpISF / autotuneMin;
|
|
|
+ // high autosens ratio = low ISF
|
|
|
+ var minISF = pumpISF / autotuneMax;
|
|
|
+ if (typeof(pumpISF) !== 'undefined') {
|
|
|
+ if ( fullNewISF < 0 ) {
|
|
|
+ var adjustedISF = ISF;
|
|
|
+ } else {
|
|
|
+ adjustedISF = adjustmentFraction*fullNewISF + (1-adjustmentFraction)*pumpISF;
|
|
|
+ }
|
|
|
+ // cap adjustedISF before applying 10%
|
|
|
+ //console.error(adjustedISF, maxISF, minISF);
|
|
|
+ if (adjustedISF > maxISF) {
|
|
|
+ console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")");
|
|
|
+ adjustedISF = maxISF;
|
|
|
+ } else if (adjustedISF < minISF) {
|
|
|
+ console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")");
|
|
|
+ adjustedISF = minISF;
|
|
|
+ }
|
|
|
+
|
|
|
+ // and apply 20% of that adjustment
|
|
|
+ var newISF = ( 0.8 * ISF ) + ( 0.2 * adjustedISF );
|
|
|
+
|
|
|
+ if (newISF > maxISF) {
|
|
|
+ console.error("Limiting ISF of",newISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")");
|
|
|
+ newISF = maxISF;
|
|
|
+ } else if (newISF < minISF) {
|
|
|
+ console.error("Limiting ISF of",newISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")");
|
|
|
+ newISF = minISF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ newISF = Math.round( newISF * 1000 ) / 1000;
|
|
|
+ //console.error(avgRatio);
|
|
|
+ //console.error(newISF);
|
|
|
+ p50deviation = Math.round( p50deviation * 1000 ) / 1000;
|
|
|
+ p50BGI = Math.round( p50BGI * 1000 ) / 1000;
|
|
|
+ adjustedISF = Math.round( adjustedISF * 1000 ) / 1000;
|
|
|
+ console.error("p50deviation:",p50deviation,"p50BGI",p50BGI,"p50ratios:",p50ratios,"Old ISF:",ISF,"fullNewISF:",fullNewISF,"adjustedISF:",adjustedISF,"newISF:",newISF,"newDIA:",newDIA,"newPeak:",newPeak);
|
|
|
+
|
|
|
+ if (newISF) {
|
|
|
+ ISF = newISF;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // reconstruct updated version of previousAutotune as autotuneOutput
|
|
|
+ var autotuneOutput = previousAutotune;
|
|
|
+ autotuneOutput.basalprofile = basalProfile;
|
|
|
+ isfProfile.sensitivities[0].sensitivity = ISF;
|
|
|
+ autotuneOutput.isfProfile = isfProfile;
|
|
|
+ autotuneOutput.sens = ISF;
|
|
|
+ autotuneOutput.csf = CSF;
|
|
|
+ //carbRatio = ISF / CSF;
|
|
|
+ carbRatio = Math.round( carbRatio * 1000 ) / 1000;
|
|
|
+ autotuneOutput.carb_ratio = carbRatio;
|
|
|
+ autotuneOutput.dia = newDIA;
|
|
|
+ autotuneOutput.insulinPeakTime = newPeak;
|
|
|
+ if (diaDeviations || peakDeviations) {
|
|
|
+ autotuneOutput.useCustomPeakTime = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return autotuneOutput;
|
|
|
+}
|
|
|
+
|
|
|
+exports = module.exports = tuneAllTheThings;
|