package info.nightscout.androidaps.plugins.OpenAPSAMA; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.data.IobTotal; import info.nightscout.androidaps.db.BgReading; import info.nightscout.androidaps.plugins.NSClientInternal.data.NSProfile; import info.nightscout.utils.Round; import info.nightscout.utils.SP; import info.nightscout.utils.SafeParse; public class Autosens { private static Logger log = LoggerFactory.getLogger(Autosens.class); public static AutosensResult detectSensitivityandCarbAbsorption(List<BgReading> glucose_data, Long mealTime) { NSProfile profile = MainApp.getConfigBuilder().getActiveProfile().getProfile(); //console.error(mealTime); double deviationSum = 0; double carbsAbsorbed = 0; List<BgReading> bucketed_data = new ArrayList<>(); bucketed_data.add(glucose_data.get(0)); int j = 0; for (int i = 1; i < glucose_data.size(); ++i) { long bgTime = glucose_data.get(i).getTimeIndex(); long lastbgTime = glucose_data.get(i - 1).getTimeIndex(); //log.error("Processing " + i + ": " + new Date(bgTime).toString() + " " + glucose_data.get(i).value + " Previous: " + new Date(lastbgTime).toString() + " " + glucose_data.get(i - 1).value); if (glucose_data.get(i).value < 39 || glucose_data.get(i - 1).value < 39) { continue; } long elapsed_minutes = (bgTime - lastbgTime) / (60 * 1000); if (Math.abs(elapsed_minutes) > 8) { // interpolate missing data points double lastbg = glucose_data.get(i - 1).value; elapsed_minutes = Math.abs(elapsed_minutes); //console.error(elapsed_minutes); long nextbgTime; while (elapsed_minutes > 5) { nextbgTime = lastbgTime - 5 * 60 * 1000; j++; BgReading newBgreading = new BgReading(); newBgreading.timeIndex = nextbgTime; double gapDelta = glucose_data.get(i).value - lastbg; //console.error(gapDelta, lastbg, elapsed_minutes); double nextbg = lastbg + (5d / elapsed_minutes * gapDelta); newBgreading.value = Math.round(nextbg); //console.error("Interpolated", bucketed_data[j]); bucketed_data.add(newBgreading); //log.error("******************************************************************************************************* Adding:" + new Date(newBgreading.timeIndex).toString() + " " + newBgreading.value); elapsed_minutes = elapsed_minutes - 5; lastbg = nextbg; lastbgTime = nextbgTime; } j++; BgReading newBgreading = new BgReading(); newBgreading.value = glucose_data.get(i).value; newBgreading.timeIndex = bgTime; bucketed_data.add(newBgreading); //log.error("******************************************************************************************************* Copying:" + new Date(newBgreading.timeIndex).toString() + " " + newBgreading.value); } else if (Math.abs(elapsed_minutes) > 2) { j++; BgReading newBgreading = new BgReading(); newBgreading.value = glucose_data.get(i).value; newBgreading.timeIndex = bgTime; bucketed_data.add(newBgreading); //log.error("******************************************************************************************************* Copying:" + new Date(newBgreading.timeIndex).toString() + " " + newBgreading.value); } else { bucketed_data.get(j).value = (bucketed_data.get(j).value + glucose_data.get(i).value) / 2; //log.error("***** Average"); } } //console.error(bucketed_data); double[] avgDeltas = new double[bucketed_data.size() - 2]; double[] bgis = new double[bucketed_data.size() - 2]; double[] deviations = new double[bucketed_data.size() - 2]; String pastSensitivity = ""; for (int i = 0; i < bucketed_data.size() - 3; ++i) { long bgTime = bucketed_data.get(i).timeIndex; int secondsFromMidnight = NSProfile.secondsFromMidnight(new Date(bgTime)); String hour = ""; //log.debug(new Date(bgTime).toString()); if (secondsFromMidnight % 3600 < 2.5 * 60 || secondsFromMidnight % 3600 > 57.5 * 60) { hour += "(" + Math.round(secondsFromMidnight / 3600d) + ")"; } double sens = NSProfile.toMgdl(profile.getIsf(secondsFromMidnight), profile.getUnits()); //console.error(bgTime , bucketed_data[i].glucose); double bg; double avgDelta; double delta; bg = bucketed_data.get(i).value; if (bg < 40 || bucketed_data.get(i + 3).value < 40) { log.error("! value < 40"); continue; } avgDelta = (bg - bucketed_data.get(i + 3).value) / 3; delta = (bg - bucketed_data.get(i + 1).value); // avgDelta = avgDelta.toFixed(2); IobTotal iob = IobTotal.calulateFromTreatmentsAndTemps(bgTime); double bgi = Math.round((-iob.activity * sens * 5) * 100) / 100d; // bgi = bgi.toFixed(2); //console.error(delta); double deviation = delta - bgi; // deviation = deviation.toFixed(2); //if (deviation < 0 && deviation > -2) { console.error("BG: "+bg+", avgDelta: "+avgDelta+", BGI: "+bgi+", deviation: "+deviation); } // Exclude large positive deviations (carb absorption) from autosens if (avgDelta - bgi < 6) { if (deviation > 0) { pastSensitivity += "+"; } else if (deviation == 0) { pastSensitivity += "="; } else { pastSensitivity += "-"; } avgDeltas[i] = avgDelta; bgis[i] = bgi; deviations[i] = deviation; deviationSum += deviation; } else { pastSensitivity += ">"; //console.error(bgTime); } pastSensitivity += hour; //log.debug("TIME: " + new Date(bgTime).toString() + " BG: " + bg + " SENS: " + sens + " DELTA: " + delta + " AVGDELTA: " + avgDelta + " IOB: " + iob.iob + " ACTIVITY: " + iob.activity + " BGI: " + bgi + " DEVIATION: " + deviation); // if bgTime is more recent than mealTime if (mealTime != null && bgTime > mealTime) { // figure out how many carbs that represents // but always assume at least 3mg/dL/5m (default) absorption double ci = Math.max(deviation, SP.getDouble("openapsama_min_5m_carbimpact", 3.0)); double absorbed = ci * profile.getIc(secondsFromMidnight) / sens; // and add that to the running total carbsAbsorbed carbsAbsorbed += absorbed; } } double ratio = 1; String ratioLimit = ""; String sensResult = ""; if (mealTime == null) { //console.error(""); log.debug(pastSensitivity); //console.log(JSON.stringify(avgDeltas)); //console.log(JSON.stringify(bgis)); Arrays.sort(avgDeltas); Arrays.sort(bgis); Arrays.sort(deviations); for (double i = 0.9; i > 0.1; i = i - 0.02) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); if (percentile(deviations, (i + 0.02)) >= 0 && percentile(deviations, i) < 0) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); log.debug(Math.round(100 * i) + "% of non-meal deviations negative (target 45%-50%)"); } } double pSensitive = percentile(deviations, 0.50); double pResistant = percentile(deviations, 0.45); //p30 = percentile(deviations, 0.3); // average = deviationSum / deviations.length; //console.error("Mean deviation: "+average.toFixed(2)); double basalOff = 0; if (pSensitive < 0) { // sensitive basalOff = pSensitive * (60 / 5) / NSProfile.toMgdl(profile.getIsf(NSProfile.secondsFromMidnight()), profile.getUnits()); sensResult = "Excess insulin sensitivity detected"; } else if (pResistant > 0) { // resistant basalOff = pResistant * (60 / 5) / NSProfile.toMgdl(profile.getIsf(NSProfile.secondsFromMidnight()), profile.getUnits()); sensResult = "Excess insulin resistance detected"; } else { sensResult = "Sensitivity normal"; } log.debug(sensResult); ratio = 1 + (basalOff / profile.getMaxDailyBasal()); // don't adjust more than 1.5x double rawRatio = ratio; ratio = Math.max(ratio, SafeParse.stringToDouble(SP.getString("openapsama_autosens_min", "0.7"))); ratio = Math.min(ratio, SafeParse.stringToDouble(SP.getString("openapsama_autosens_max", "1.2"))); if (ratio != rawRatio) { ratioLimit = "Ratio limited from " + rawRatio + " to " + ratio; log.debug(ratioLimit); } double newisf = Math.round(NSProfile.toMgdl(profile.getIsf(NSProfile.secondsFromMidnight()), profile.getUnits()) / ratio); if (ratio != 1) { log.debug("ISF adjusted from " + NSProfile.toMgdl(profile.getIsf(NSProfile.secondsFromMidnight()), profile.getUnits()) + " to " + newisf); } //console.error("Basal adjustment "+basalOff.toFixed(2)+"U/hr"); //console.error("Ratio: "+ratio*100+"%: new ISF: "+newisf.toFixed(1)+"mg/dL/U"); } AutosensResult output = new AutosensResult(); output.ratio = Round.roundTo(ratio, 0.01); output.carbsAbsorbed = Round.roundTo(carbsAbsorbed, 0.01); output.pastSensitivity = pastSensitivity; output.ratioLimit = ratioLimit; output.sensResult = sensResult; return output; } // From https://gist.github.com/IceCreamYou/6ffa1b18c4c8f6aeaad2 // Returns the value at a given percentile in a sorted numeric array. // "Linear interpolation between closest ranks" method public static double percentile(double[] arr, double p) { if (arr.length == 0) return 0; if (p <= 0) return arr[0]; if (p >= 1) return arr[arr.length - 1]; double index = arr.length * p, lower = Math.floor(index), upper = lower + 1, weight = index % 1; if (upper >= arr.length) return arr[(int) lower]; return arr[(int) lower] * (1 - weight) + arr[(int) upper] * weight; } // Returns the percentile of the given value in a sorted numeric array. public static double percentRank(double[] arr, double v) { for (int i = 0, l = arr.length; i < l; i++) { if (v <= arr[i]) { while (i < l && v == arr[i]) i++; if (i == 0) return 0; if (v != arr[i - 1]) { i += (v - arr[i - 1]) / (arr[i] - arr[i - 1]); } return i / l; } } return 1; } }