From 8aad0033117f6601b44c92c2b98133bbd4757366 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 4 Jan 2017 22:21:46 +0100 Subject: [PATCH] copy basic files oref0 dev 2017/01/04, make it running without crash --- .../main/assets/OpenAPSAMA/basal-set-temp.js | 61 +++ .../main/assets/OpenAPSAMA/determine-basal.js | 465 ++++++++++++------ app/src/main/assets/OpenAPSAMA/round-basal.js | 46 ++ .../androidaps/db/DatabaseHelper.java | 32 +- .../DetermineBasalAdapterAMAJS.java | 57 ++- .../plugins/OpenAPSAMA/OpenAPSAMAPlugin.java | 2 +- .../plugins/Treatments/TreatmentsPlugin.java | 1 + 7 files changed, 476 insertions(+), 188 deletions(-) create mode 100644 app/src/main/assets/OpenAPSAMA/basal-set-temp.js create mode 100644 app/src/main/assets/OpenAPSAMA/round-basal.js diff --git a/app/src/main/assets/OpenAPSAMA/basal-set-temp.js b/app/src/main/assets/OpenAPSAMA/basal-set-temp.js new file mode 100644 index 0000000000..9745869194 --- /dev/null +++ b/app/src/main/assets/OpenAPSAMA/basal-set-temp.js @@ -0,0 +1,61 @@ +'use strict'; + +function reason(rT, msg) { + rT.reason = (rT.reason ? rT.reason + '. ' : '') + msg; + console.error(msg); +} + +var tempBasalFunctions = {}; + +tempBasalFunctions.getMaxSafeBasal = function getMaxSafeBasal(profile) { + + var max_daily_safety_multiplier = (isNaN(profile.max_daily_safety_multiplier) || profile.max_daily_safety_multiplier == null) ? 3 : profile.max_daily_safety_multiplier; + var current_basal_safety_multiplier = (isNaN(profile.current_basal_safety_multiplier) || profile.current_basal_safety_multiplier == null) ? 4 : profile.current_basal_safety_multiplier; + + return Math.min(profile.max_basal, max_daily_safety_multiplier * profile.max_daily_basal, current_basal_safety_multiplier * profile.current_basal); +}; + +tempBasalFunctions.setTempBasal = function setTempBasal(rate, duration, profile, rT, currenttemp) { + //var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); + + var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile); +var round_basal = require('./round-basal'); + + if (rate < 0) { + rate = 0; + } // if >30m @ 0 required, zero temp will be extended to 30m instead + else if (rate > maxSafeBasal) { + rate = maxSafeBasal; + } + + var suggestedRate = round_basal(rate, profile); + if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && typeof(currenttemp.rate) !== 'undefined' && currenttemp.duration > 20 && suggestedRate <= currenttemp.rate * 1.2 && suggestedRate >= currenttemp.rate * 0.8) { + rT.reason += ", but "+currenttemp.duration+"m left and " + currenttemp.rate + " ~ req " + suggestedRate + "U/hr: no action required"; + return rT; + } + + if (suggestedRate === profile.current_basal) { + if (profile.skip_neutral_temps) { + if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && currenttemp.duration > 0) { + reason(rT, 'Suggested rate is same as profile rate, a temp basal is active, canceling current temp'); + rT.duration = 0; + rT.rate = 0; + return rT; + } else { + reason(rT, 'Suggested rate is same as profile rate, no temp basal is active, doing nothing'); + return rT; + } + } else { + reason(rT, 'Setting neutral temp basal of ' + profile.current_basal + 'U/hr'); + rT.duration = duration; + rT.rate = suggestedRate; + return rT; + } + } else { + rT.duration = duration; + rT.rate = suggestedRate; + return rT; + } +}; + +module.exports = tempBasalFunctions; diff --git a/app/src/main/assets/OpenAPSAMA/determine-basal.js b/app/src/main/assets/OpenAPSAMA/determine-basal.js index 4c5a00fade..3f7d869359 100644 --- a/app/src/main/assets/OpenAPSAMA/determine-basal.js +++ b/app/src/main/assets/OpenAPSAMA/determine-basal.js @@ -12,7 +12,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, offline, meal_data, setTempBasal) { + + +var round_basal = require('../round-basal') + +// Rounds value to 'digits' decimal places +function round(value, digits) +{ + var scale = Math.pow(10, digits); + return Math.round(value * scale) / scale; +} + +// we expect BG to rise or fall at the rate of BGI, +// adjusted by the rate at which BG would need to rise / +// fall to get eventualBG to target over DIA/2 hours +function calculate_expected_delta(dia, target_bg, eventual_bg, bgi) { + // (hours * mins_per_hour) / 5 = how many 5 minute periods in dia/2 + var dia_in_5min_blocks = (dia/2 * 60) / 5; + var target_delta = target_bg - eventual_bg; + var expectedDelta = round(bgi + (target_delta / dia_in_5min_blocks), 1); + return expectedDelta; +} + + +function convert_bg(value, profile) +{ + if (profile.out_units == "mmol/L") + { + return round(value / 18, 1).toFixed(1); + } + else + { + return value.toFixed(0); + } +} + +var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions) { var rT = { //short for requestedTemp }; @@ -20,17 +55,40 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ rT.error ='Error: could not get current basal rate'; return rT; } + var basal = profile.current_basal; + if (typeof autosens_data !== 'undefined' ) { + basal = profile.current_basal * autosens_data.ratio; + basal = round_basal(basal, profile); + if (basal != profile.current_basal) { + console.error("Adjusting basal from "+profile.current_basal+" to "+basal); + } + } var bg = glucose_status.glucose; - if (bg < 38) { //Dexcom is in ??? mode or calibrating, do nothing. Asked @benwest for raw data in iter_glucose - rT.error = "CGM is calibrating or in ??? state"; - return rT; + // TODO: figure out how to use raw isig data to estimate BG + if (bg < 39) { //Dexcom is in ??? mode or calibrating + rT.reason = "CGM is calibrating or in ??? state"; + if (basal <= currenttemp.rate * 1.2) { // high temp is running + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } else { //do nothing. + rT.reason += ", temp " + currenttemp.rate + " <~ current basal " + basal + "U/hr"; + return rT; + } } var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver // if target_bg is set, great. otherwise, if min and max are set, then set target to their average var target_bg; + var min_bg; + var max_bg; + if (typeof profile.min_bg !== 'undefined') { + min_bg = profile.min_bg; + } + if (typeof profile.max_bg !== 'undefined') { + max_bg = profile.max_bg; + } if (typeof profile.target_bg !== 'undefined') { target_bg = profile.target_bg; } else { @@ -42,61 +100,90 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } } + // adjust min, max, and target BG for sensitivity, such that 50% increase in ISF raises target from 100 to 120 + if (typeof autosens_data !== 'undefined' && profile.autosens_adjust_targets) { + if (profile.temptargetSet) { + console.error("Temp Target set, not adjusting with autosens"); + } else { + min_bg = Math.round((min_bg - 60) / autosens_data.ratio) + 60; + max_bg = Math.round((max_bg - 60) / autosens_data.ratio) + 60; + new_target_bg = Math.round((target_bg - 60) / autosens_data.ratio) + 60; + if (target_bg == new_target_bg) { + console.error("target_bg unchanged:", new_target_bg); + } else { + console.error("Adjusting target_bg from", target_bg, "to", new_target_bg); + } + target_bg = new_target_bg; + } + } if (typeof iob_data === 'undefined' ) { rT.error ='Error: iob_data undefined'; return rT; } - if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' || typeof iob_data.activity === 'undefined') { + var iobArray = iob_data; + if (typeof(iob_data.length) && iob_data.length > 1) { + iob_data = iobArray[0]; + //console.error(JSON.stringify(iob_data[0])); + } + + if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' ) { rT.error ='Error: iob_data missing some property'; return rT; } var tick; - if (glucose_status.delta >= 0) { - tick = "+" + glucose_status.delta; + if (glucose_status.delta > -0.5) { + tick = "+" + round(glucose_status.delta,0); } else { - tick = glucose_status.delta; + tick = round(glucose_status.delta,0); } - var minDelta = Math.min(glucose_status.delta, glucose_status.avgdelta); - //var maxDelta = Math.max(glucose_status.delta, glucose_status.avgdelta); + var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta); + var minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta); + var sens = profile.sens; + if (typeof autosens_data !== 'undefined' ) { + sens = profile.sens / autosens_data.ratio; + sens = round(sens, 1); + if (sens != profile.sens) { + console.error("Adjusting sens from "+profile.sens+" to "+sens); + } + } //calculate BG impact: the amount BG "should" be rising or falling based on insulin activity alone - var bgi = Math.round(( -iob_data.activity * profile.sens * 5 )*100)/100; - // project positive deviations for 15 minutes - var deviation = Math.round( 15 / 5 * ( glucose_status.avgdelta - bgi ) ); - // project negative deviations for 30 minutes + var bgi = round(( -iob_data.activity * sens * 5 ), 2); + // project deviations for 30 minutes + var deviation = Math.round( 30 / 5 * ( minDelta - bgi ) ); + // don't overreact to a big negative delta: use minAvgDelta if deviation is negative if (deviation < 0) { - deviation = Math.round( 30 / 5 * ( glucose_status.avgdelta - bgi ) ); + deviation = Math.round( (30 / 5) * ( minAvgDelta - bgi ) ); } - //console.log("Avg.Delta: " + glucose_status.avgdelta.toFixed(1) + ", BGI: " + bgi.toFixed(1) + " 15m activity projection: " + deviation.toFixed(0)); // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity - var naive_eventualBG = Math.round( bg - (iob_data.iob * profile.sens) ); + if (iob_data.iob > 0) { + var naive_eventualBG = Math.round( bg - (iob_data.iob * sens) ); + } else { // if IOB is negative, be more conservative and use the lower of sens, profile.sens + var naive_eventualBG = Math.round( bg - (iob_data.iob * Math.min(sens, profile.sens) ) ); + } // and adjust it for the deviation above var eventualBG = naive_eventualBG + deviation; // calculate what portion of that is due to bolussnooze - var bolusContrib = iob_data.bolussnooze * profile.sens; + var bolusContrib = iob_data.bolussnooze * sens; // and add it back in to get snoozeBG, plus another 50% to avoid low-temping at mealtime var naive_snoozeBG = Math.round( naive_eventualBG + 1.5 * bolusContrib ); // adjust that for deviation like we did eventualBG var snoozeBG = naive_snoozeBG + deviation; - //console.log("BG: " + bg +"(" + tick + ","+glucose_status.avgdelta.toFixed(1)+")"+ " -> " + eventualBG + "-" + snoozeBG + " (Unadjusted: " + naive_eventualBG + "-" + naive_snoozeBG + "), BGI: " + bgi); - - var expectedDelta = Math.round(( bgi + ( target_bg - eventualBG ) / ( profile.dia * 60 / 5 ) )*10)/10; - //console.log("expectedDelta: " + expectedDelta); - + var expectedDelta = calculate_expected_delta(profile.dia, target_bg, eventualBG, bgi); if (typeof eventualBG === 'undefined' || isNaN(eventualBG)) { rT.error ='Error: could not calculate eventualBG'; return rT; } // min_bg of 90 -> threshold of 70, 110 -> 80, and 130 -> 90 - var threshold = profile.min_bg - 0.5*(profile.min_bg-50); + var threshold = min_bg - 0.5*(min_bg-50); rT = { 'temp': 'absolute' @@ -109,208 +196,292 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var basaliob; if (iob_data.basaliob) { basaliob = iob_data.basaliob; } else { basaliob = iob_data.iob - iob_data.bolussnooze; } - // allow meal assist to run when carbs are just barely covered - if (minDelta > Math.max(3, bgi) && ( (meal_data.carbs > 0 && (1.1 * meal_data.carbs/profile.carb_ratio > meal_data.boluses + basaliob)) || ( deviation > 25 && minDelta > 7 ) ) ) { - // ignore all covered IOB, and just set eventualBG to the current bg - eventualBG = Math.max(bg,eventualBG) + deviation; - rT.eventualBG = eventualBG; - profile.min_bg = 80; - target_bg = (profile.min_bg + profile.max_bg) / 2; - expectedDelta = Math.round(( bgi + ( target_bg - eventualBG ) / ( profile.dia * 60 / 5 ) )*10)/10; - rT.mealAssist = "On: Carbs: " + meal_data.carbs + " Boluses: " + meal_data.boluses + " Target: " + target_bg + " Deviation: " + deviation + " BGI: " + bgi; - } else { - rT.mealAssist = "Off: Carbs: " + meal_data.carbs + " Boluses: " + meal_data.boluses + " Target: " + target_bg + " Deviation: " + deviation + " BGI: " + bgi; + + // generate predicted future BGs based on IOB, COB, and current absortpion rate + + var COBpredBGs = []; + var aCOBpredBGs = []; + var IOBpredBGs = []; + COBpredBGs.push(bg); + aCOBpredBGs.push(bg); + IOBpredBGs.push(bg); + //console.error(meal_data); + // carb impact and duration are 0 unless changed below + var ci = 0; + var cid = 0; + // calculate current carb absorption rate, and how long to absorb all carbs + // CI = current carb impact on BG in mg/dL/5m + ci = Math.round((minDelta - bgi)*10)/10; + if (meal_data.mealCOB * 2 > meal_data.carbs) { + // set ci to a minimum of 3mg/dL/5m (default) if less than half of carbs have absorbed + ci = Math.max(profile.min_5m_carbimpact, ci); } - if (bg < threshold) { // low glucose suspend mode: BG is < ~80 - rT.reason = "BG " + bg + "<" + threshold; - if ((glucose_status.delta <= 0 && glucose_status.avgdelta <= 0) || (glucose_status.delta < expectedDelta && glucose_status.avgdelta < expectedDelta)) { - // BG is still falling / rising slower than predicted - return setTempBasal(0, 30, profile, rT, offline); + aci = 10; + //5m data points = g * (1U/10g) * (40mg/dL/1U) / (mg/dL/5m) + cid = meal_data.mealCOB * ( sens / profile.carb_ratio ) / ci; + acid = meal_data.mealCOB * ( sens / profile.carb_ratio ) / aci; + console.error("Carb Impact:",ci,"mg/dL per 5m; CI Duration:",Math.round(10*cid/6)/10,"hours"); + console.error("Accel. Carb Impact:",aci,"mg/dL per 5m; ACI Duration:",Math.round(10*acid/6)/10,"hours"); + var minPredBG = 999; + var maxPredBG = bg; + var eventualPredBG = bg; + try { + iobArray.forEach(function(iobTick) { + //console.error(iobTick); + predBGI = round(( -iobTick.activity * sens * 5 ), 2); + // predicted deviation impact drops linearly from current deviation down to zero + // over 60 minutes (data points every 5m) + predDev = ci * ( 1 - Math.min(1,IOBpredBGs.length/(60/5)) ); + IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + predBGI + predDev; + //IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + predBGI; + // predicted carb impact drops linearly from current carb impact down to zero + // eventually accounting for all carbs (if they can be absorbed over DIA) + predCI = Math.max(0, ci * ( 1 - COBpredBGs.length/Math.max(cid*2,1) ) ); + predACI = Math.max(0, aci * ( 1 - COBpredBGs.length/Math.max(acid*2,1) ) ); + COBpredBG = COBpredBGs[COBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predCI; + aCOBpredBG = aCOBpredBGs[aCOBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predACI; + //console.error(predBGI, predCI, predBG); + IOBpredBGs.push(IOBpredBG); + COBpredBGs.push(COBpredBG); + aCOBpredBGs.push(aCOBpredBG); + // wait 45m before setting minPredBG + if ( COBpredBGs.length > 9 && (COBpredBG < minPredBG) ) { minPredBG = COBpredBG; } + if ( COBpredBG > maxPredBG ) { maxPredBG = COBpredBG; } + }); + // set eventualBG to include effect of carbs + //console.error("PredBGs:",JSON.stringify(predBGs)); + } catch (e) { + console.error("Problem with iobArray. Optional feature Advanced Meal Assist disabled."); + } + rT.predBGs = {}; + IOBpredBGs.forEach(function(p, i, theArray) { + theArray[i] = Math.round(Math.min(401,Math.max(39,p))); + }); + for (var i=IOBpredBGs.length-1; i > 12; i--) { + if (IOBpredBGs[i-1] != IOBpredBGs[i]) { break; } + else { IOBpredBGs.pop(); } + } + rT.predBGs.IOB = IOBpredBGs; + if (meal_data.mealCOB > 0) { + aCOBpredBGs.forEach(function(p, i, theArray) { + theArray[i] = Math.round(Math.min(401,Math.max(39,p))); + }); + for (var i=aCOBpredBGs.length-1; i > 12; i--) { + if (aCOBpredBGs[i-1] != aCOBpredBGs[i]) { break; } + else { aCOBpredBGs.pop(); } } - if (glucose_status.delta > glucose_status.avgdelta) { + rT.predBGs.aCOB = aCOBpredBGs; + } + if (meal_data.mealCOB > 0 && ci > 0 ) { + COBpredBGs.forEach(function(p, i, theArray) { + theArray[i] = Math.round(Math.min(401,Math.max(39,p))); + }); + for (var i=COBpredBGs.length-1; i > 12; i--) { + if (COBpredBGs[i-1] != COBpredBGs[i]) { break; } + else { COBpredBGs.pop(); } + } + rT.predBGs.COB = COBpredBGs; + eventualBG = Math.max(eventualBG, Math.round(COBpredBGs[COBpredBGs.length-1]) ); + rT.eventualBG = eventualBG; + minPredBG = Math.min(minPredBG, eventualBG); + // set snoozeBG to minPredBG + snoozeBG = Math.round(Math.max(snoozeBG,minPredBG)); + rT.snoozeBG = snoozeBG; + } + + rT.COB=meal_data.mealCOB; + rT.IOB=iob_data.iob; + rT.reason="COB: " + meal_data.mealCOB + ", Dev: " + deviation + ", BGI: " + bgi + ", ISF: " + convert_bg(sens, profile) + ", Target: " + convert_bg(target_bg, profile) + "; "; + if (bg < threshold) { // low glucose suspend mode: BG is < ~80 + rT.reason += "BG " + convert_bg(bg, profile) + "<" + convert_bg(threshold, profile); + if ((glucose_status.delta <= 0 && minDelta <= 0) || (glucose_status.delta < expectedDelta && minDelta < expectedDelta) || bg < 60 ) { + // BG is still falling / rising slower than predicted + return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp); + } + if (glucose_status.delta > minDelta) { rT.reason += ", delta " + glucose_status.delta + ">0"; } else { - rT.reason += ", avg delta " + glucose_status.avgdelta.toFixed(2) + ">0"; + rT.reason += ", min delta " + minDelta.toFixed(2) + ">0"; } - if (currenttemp.rate > profile.current_basal) { // if a high-temp is running - rT.reason += ", cancel high temp"; - return setTempBasal(0, 0, profile, rT, offline); // cancel high temp - } else if (currenttemp.duration && eventualBG > profile.max_bg) { // if low-temped and predicted to go high from negative IOB - rT.reason += ", cancel low temp"; - return setTempBasal(0, 0, profile, rT, offline); // cancel low temp - } - rT.reason += "; no high-temp to cancel"; - return rT; - } - if (eventualBG < profile.min_bg) { // if eventual BG is below target: - if (rT.mealAssist.indexOf("On") == 0) { - rT.reason = "Meal assist: " + meal_data.carbs + "g, " + meal_data.boluses + "U"; + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr"; + return rT; } else { - rT.reason = "Eventual BG " + eventualBG + "<" + profile.min_bg; - // if 5m or 15m avg BG is rising faster than expected delta - if (minDelta > expectedDelta && minDelta > 0) { - if (glucose_status.delta > glucose_status.avgdelta) { - rT.reason += ", but Delta " + tick + " > Exp. Delta " + expectedDelta; - } else { - rT.reason += ", but Avg. Delta " + glucose_status.avgdelta.toFixed(2) + " > Exp. Delta " + expectedDelta; - } - if (currenttemp.duration > 0) { // if there is currently any temp basal running - rT.reason = rT.reason += "; cancel"; - return setTempBasal(0, 0, profile, rT, offline); // cancel temp - } else { - rT.reason = rT.reason += "; no temp to cancel"; + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } + + if (eventualBG < min_bg) { // if eventual BG is below target: + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " < " + convert_bg(min_bg, profile); + // if 5m or 30m avg BG is rising faster than expected delta + if (minDelta > expectedDelta && minDelta > 0) { + if (glucose_status.delta > minDelta) { + rT.reason += ", but Delta " + tick + " > Exp. Delta " + expectedDelta; + } else { + rT.reason += ", but Min. Delta " + minDelta.toFixed(2) + " > Exp. Delta " + expectedDelta; + } + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr"; return rT; - } + } else { + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); } } - if (eventualBG < profile.min_bg) { - // if this is just due to boluses, we can snooze until the bolus IOB decays (at double speed) - if (snoozeBG > profile.min_bg) { // if adding back in the bolus contribution BG would be above min - // if BG is falling and high-temped, or rising and low-temped, cancel - // compare against zero here, not BGI, because BGI will be highly negative from boluses and no carbs - if (glucose_status.delta < 0 && currenttemp.duration > 0 && currenttemp.rate > profile.current_basal) { - rT.reason += tick + ", and temp " + currenttemp.rate + " > basal " + profile.current_basal; - return setTempBasal(0, 0, profile, rT, offline); // cancel temp - } else if (glucose_status.delta > 0 && currenttemp.duration > 0 && currenttemp.rate < profile.current_basal) { - rT.reason += tick + ", and temp " + currenttemp.rate + " < basal " + profile.current_basal; - return setTempBasal(0, 0, profile, rT, offline); // cancel temp + if (eventualBG < min_bg) { + // if we've bolused recently, we can snooze until the bolus IOB decays (at double speed) + if (snoozeBG > min_bg) { // if adding back in the bolus contribution BG would be above min + rT.reason += ", bolus snooze: eventual BG range " + convert_bg(eventualBG, profile) + "-" + convert_bg(snoozeBG, profile); + //console.error(currenttemp, basal ); + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr"; + return rT; + } else { + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); } - - rT.reason += ", bolus snooze: eventual BG range " + eventualBG + "-" + snoozeBG; - return rT; } else { // calculate 30m low-temp required to get projected BG up to target // use snoozeBG to more gradually ramp in any counteraction of the user's boluses // multiply by 2 to low-temp faster for increased hypo safety - var insulinReq = 2 * Math.min(0, (snoozeBG - target_bg) / profile.sens); + var insulinReq = 2 * Math.min(0, (snoozeBG - target_bg) / sens); if (minDelta < 0 && minDelta > expectedDelta) { // if we're barely falling, newinsulinReq should be barely negative - rT.reason += ", Snooze BG " + snoozeBG; - var newinsulinReq = Math.round(( insulinReq * (minDelta / expectedDelta) ) * 100)/100; - //console.log("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq); + rT.reason += ", Snooze BG " + convert_bg(snoozeBG, profile); + var newinsulinReq = round(( insulinReq * (minDelta / expectedDelta) ), 2); + //console.error("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq); insulinReq = newinsulinReq; } // rate required to deliver insulinReq less insulin over 30m: - var rate = profile.current_basal + (2 * insulinReq); - rate = Math.round( rate * 1000 ) / 1000; + var rate = basal + (2 * insulinReq); + rate = round_basal(rate, profile); // if required temp < existing temp basal - var insulinScheduled = currenttemp.duration * (currenttemp.rate - profile.current_basal) / 60; - if (insulinScheduled < insulinReq - 0.2) { // if current temp would deliver >0.2U less than the required insulin, raise the rate - rT.reason = currenttemp.duration + "m@" + (currenttemp.rate - profile.current_basal).toFixed(3) + " = " + insulinScheduled.toFixed(3) + " < req " + insulinReq + "-0.2U"; - return setTempBasal(rate, 30, profile, rT, offline); + var insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60; + if (insulinScheduled < insulinReq - basal*0.3) { // if current temp would deliver a lot (30% of basal) less than the required insulin, raise the rate + rT.reason += ", "+currenttemp.duration + "m@" + (currenttemp.rate - basal).toFixed(3) + " = " + insulinScheduled.toFixed(3) + " < req " + insulinReq + "-" + basal*0.3; + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); } - if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate > currenttemp.rate - 0.1)) { + if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate >= currenttemp.rate * 0.8)) { rT.reason += ", temp " + currenttemp.rate + " ~< req " + rate + "U/hr"; return rT; } else { rT.reason += ", setting " + rate + "U/hr"; - return setTempBasal(rate, 30, profile, rT, offline); + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); } } } } + // if there is a low-temp running, and eventualBG would be below min_bg without it, let it run + if (round_basal(currenttemp.rate, profile) < round_basal(basal, profile) ) { + var lowtempimpact = (currenttemp.rate - basal) * (currenttemp.duration/60) * sens; + var adjEventualBG = eventualBG + lowtempimpact; + if ( adjEventualBG < min_bg ) { + rT.reason += "letting low temp of " + currenttemp.rate + " run."; + return rT; + } + } + // if eventual BG is above min but BG is falling faster than expected Delta if (minDelta < expectedDelta) { - if (glucose_status.delta < glucose_status.avgdelta) { - rT.reason = "Eventual BG " + eventualBG + ">" + profile.min_bg + " but Delta " + tick + " < Exp. Delta " + expectedDelta; + if (glucose_status.delta < minDelta) { + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Delta " + tick + " < Exp. Delta " + expectedDelta; } else { - rT.reason = "Eventual BG " + eventualBG + ">" + profile.min_bg + " but Avg. Delta " + glucose_status.avgdelta.toFixed(2) + " < Exp. Delta " + expectedDelta; + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Min. Delta " + minDelta.toFixed(2) + " < Exp. Delta " + expectedDelta; } - if (currenttemp.duration > 0) { // if there is currently any temp basal running - rT.reason = rT.reason += "; cancel"; - return setTempBasal(0, 0, profile, rT, offline); // cancel temp + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr"; + return rT; } else { - rT.reason = rT.reason += "; no temp to cancel"; + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } + } + // eventualBG or snoozeBG (from minPredBG) is below max_bg + if (eventualBG < max_bg || snoozeBG < max_bg) { + // if there is a high-temp running and eventualBG > max_bg, let it run + if (eventualBG > max_bg && round_basal(currenttemp.rate, profile) > round_basal(basal, profile) ) { + rT.reason += ", " + eventualBG + " > " + max_bg + ": no action required (letting high temp of " + currenttemp.rate + " run)." return rT; } - } - if (eventualBG < profile.max_bg) { - rT.reason = eventualBG + " is in range. No temp required"; - if (currenttemp.duration > 0) { // if there is currently any temp basal running - rT.reason = rT.reason += "; cancel"; - return setTempBasal(0, 0, profile, rT, offline); // cancel temp - } - if (offline == 'Offline') { - // if no temp is running or required, set the current basal as a temp, so you can see on the pump that the loop is working - if ((!currenttemp.duration || (currenttemp.rate == profile.current_basal)) && !rT.duration) { - rT.reason = rT.reason + "; setting current basal of " + profile.current_basal + " as temp"; - return setTempBasal(profile.current_basal, 30, profile, rT, offline); - } - } - return rT; - } - - if (snoozeBG < profile.max_bg) { - rT.reason = snoozeBG + " < " + profile.max_bg; - if (currenttemp.duration > 0) { // if there is currently any temp basal running - rT.reason = rT.reason += "; cancel"; - return setTempBasal(0, 0, profile, rT, offline); // cancel temp - } else { - rT.reason = rT.reason += "; no temp to cancel"; + rT.reason += convert_bg(eventualBG, profile)+"-"+convert_bg(snoozeBG, profile)+" in range: no temp required"; + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr"; return rT; + } else { + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); } } - // eventual BG is at/above target: // if iob is over max, just cancel any temps var basaliob; if (iob_data.basaliob) { basaliob = iob_data.basaliob; } else { basaliob = iob_data.iob - iob_data.bolussnooze; } - rT.reason = "Eventual BG " + eventualBG + ">=" + profile.max_bg + ", "; + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " >= " + convert_bg(max_bg, profile) + ", "; if (basaliob > max_iob) { - rT.reason = "basaliob " + basaliob + " > max_iob " + max_iob; - return setTempBasal(0, 0, profile, rT, offline); + rT.reason += "basaliob " + round(basaliob,2) + " > max_iob " + max_iob; + if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { + rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr"; + return rT; + } else { + rT.reason += "; setting current basal of " + basal + " as temp"; + return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + } } else { // otherwise, calculate 30m high-temp required to get projected BG down to target // insulinReq is the additional insulin required to get down to max bg: // if in meal assist mode, check if snoozeBG is lower, as eventualBG is not dependent on IOB - var insulinReq = (Math.min(snoozeBG,eventualBG) - target_bg) / profile.sens; + var insulinReq = round( (Math.min(snoozeBG,eventualBG) - target_bg) / sens, 2); if (minDelta < 0 && minDelta > expectedDelta) { - var newinsulinReq = Math.round(( insulinReq * (1 - (minDelta / expectedDelta)) ) * 100)/100; - //console.log("Reducing insulinReq from " + insulinReq + " to " + newinsulinReq); + var newinsulinReq = round(( insulinReq * (1 - (minDelta / expectedDelta)) ), 2); + //console.error("Reducing insulinReq from " + insulinReq + " to " + newinsulinReq); insulinReq = newinsulinReq; } // if that would put us over max_iob, then reduce accordingly if (insulinReq > max_iob-basaliob) { - rT.reason = "max_iob " + max_iob + ", "; + rT.reason += "max_iob " + max_iob + ", "; insulinReq = max_iob-basaliob; } // rate required to deliver insulinReq more insulin over 30m: - var rate = profile.current_basal + (2 * insulinReq); - rate = Math.round( rate * 1000 ) / 1000; + var rate = basal + (2 * insulinReq); + rate = round_basal(rate, profile); + +// var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * basal); + + var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile); - var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); if (rate > maxSafeBasal) { - rT.reason += "adj. req. rate:"+rate.toFixed(1) +" to maxSafeBasal:"+maxSafeBasal.toFixed(1)+", "; - rate = maxSafeBasal; + rT.reason += "adj. req. rate: "+rate+" to maxSafeBasal: "+maxSafeBasal+", "; + rate = round_basal(maxSafeBasal, profile); } - var insulinScheduled = currenttemp.duration * (currenttemp.rate - profile.current_basal) / 60; - if (insulinScheduled > insulinReq + 0.2) { // if current temp would deliver >0.2U more than the required insulin, lower the rate - rT.reason = currenttemp.duration + "m@" + (currenttemp.rate - profile.current_basal).toFixed(3) + " = " + insulinScheduled.toFixed(3) + " > req " + insulinReq + "+0.2U"; - return setTempBasal(rate, 30, profile, rT, offline); + var insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60; + if (insulinScheduled >= insulinReq * 2) { // if current temp would deliver >2x more than the required insulin, lower the rate + rT.reason += currenttemp.duration + "m@" + (currenttemp.rate - basal).toFixed(3) + " = " + insulinScheduled.toFixed(3) + " > 2 * req " + insulinReq + ". Setting temp basal of " + rate + "U/hr"; + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); } if (typeof currenttemp.duration == 'undefined' || currenttemp.duration == 0) { // no temp is set rT.reason += "no temp, setting " + rate + "U/hr"; - return setTempBasal(rate, 30, profile, rT, offline); + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); } - if (currenttemp.duration > 5 && rate < currenttemp.rate + 0.1) { // if required temp <~ existing temp basal + if (currenttemp.duration > 5 && (round_basal(rate, profile) <= round_basal(currenttemp.rate, profile))) { // if required temp <~ existing temp basal rT.reason += "temp " + currenttemp.rate + " >~ req " + rate + "U/hr"; return rT; } // required temp > existing temp basal rT.reason += "temp " + currenttemp.rate + "<" + rate + "U/hr"; - return setTempBasal(rate, 30, profile, rT, offline); + return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp); } }; -module.exports = determine_basal; \ No newline at end of file +module.exports = determine_basal; diff --git a/app/src/main/assets/OpenAPSAMA/round-basal.js b/app/src/main/assets/OpenAPSAMA/round-basal.js new file mode 100644 index 0000000000..39d286a53d --- /dev/null +++ b/app/src/main/assets/OpenAPSAMA/round-basal.js @@ -0,0 +1,46 @@ +var endsWith = function endsWith(text, val) { + return text.indexOf(val, text.length - val.length) !== -1; +} + +var round_basal = function round_basal(basal, profile) { + + /* x23 and x54 pumps change basal increment depending on how much basal is being delivered: + 0.025u for 0.025 < x < 0.975 + 0.05u for 1 < x < 9.95 + 0.1u for 10 < x + To round numbers nicely for the pump, use a scale factor of (1 / increment). */ + + var lowest_rate_scale = 20; + + // Has profile even been passed in? + if (typeof profile !== 'undefined') + { + // Make sure optional model has been set + if (typeof profile.model == 'string') + { + if (endsWith(profile.model, "54") || endsWith(profile.model, "23")) + { + lowest_rate_scale = 40; + } + } + } + + var rounded_result = basal; + // Shouldn't need to check against 0 as pumps can't deliver negative basal anyway? + if (basal < 1) + { + rounded_basal = Math.round(basal * lowest_rate_scale) / lowest_rate_scale; + } + else if (basal < 10) + { + rounded_basal = Math.round(basal * 20) / 20; + } + else + { + rounded_basal = Math.round(basal * 10) / 10; + } + + return rounded_basal; +} + +exports = module.exports = round_basal diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index fccb964394..28bd2faf89 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -209,10 +209,12 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { /* * Returns glucose_status for openAPS or null if no actual data available */ - public static class GlucoseStatus implements Parcelable { + public static class GlucoseStatus { public double glucose = 0d; public double delta = 0d; public double avgdelta = 0d; + public double short_avgdelta = 0d; // TODO: add calculation for AMA + public double long_avgdelta = 0d; // TODO: add calculation for AMA @Override public String toString() { @@ -227,34 +229,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { "" + MainApp.sResources.getString(R.string.avgdelta) + ": " + DecimalFormatter.to2Decimal(avgdelta) + " mg/dl"); } - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeDouble(avgdelta); - dest.writeDouble(delta); - dest.writeDouble(glucose); - } - - public final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public GlucoseStatus createFromParcel(Parcel in) { - return new GlucoseStatus(in); - } - - public GlucoseStatus[] newArray(int size) { - return new GlucoseStatus[size]; - } - }; - - private GlucoseStatus(Parcel in) { - avgdelta = in.readDouble(); - delta = in.readDouble(); - glucose = in.readDouble(); - } - public GlucoseStatus() { } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java index 54a3d5aa6a..7e0d28d9e3 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/DetermineBasalAdapterAMAJS.java @@ -34,21 +34,24 @@ public class DetermineBasalAdapterAMAJS { private V8Object mIobData; private V8Object mMealData; private V8Object mCurrentTemp; + private V8Object mAutosensData = null; private final String PARAM_currentTemp = "currentTemp"; private final String PARAM_iobData = "iobData"; private final String PARAM_glucoseStatus = "glucose_status"; private final String PARAM_profile = "profile"; private final String PARAM_meal_data = "meal_data"; + private final String PARAM_autosens_data = "autosens_data"; private String storedCurrentTemp = null; - public String storedIobData = null; + private String storedIobData = null; private String storedGlucoseStatus = null; private String storedProfile = null; private String storedMeal_data = null; + private String storedAutosens_data = null; /** - * Main code + * Main code */ public DetermineBasalAdapterAMAJS(ScriptReader scriptReader) throws IOException { @@ -102,17 +105,22 @@ public class DetermineBasalAdapterAMAJS { mMealData = new V8Object(mV8rt); mMealData.add("carbs", 0); mMealData.add("boluses", 0); + mMealData.add("mealCOB", 0.0d); mV8rt.add(PARAM_meal_data, mMealData); + // Autosens data + mV8rt.executeVoidScript("autosens_data = undefined"); } public DetermineBasalResultAMA invoke() { + mV8rt.executeVoidScript( "console.error(\"determine_basal(\"+\n" + "JSON.stringify(" + PARAM_glucoseStatus + ")+ \", \" +\n" + - "JSON.stringify(" + PARAM_currentTemp + ")+ \", \" +\n" + - "JSON.stringify(" + PARAM_iobData + ")+ \", \" +\n" + - "JSON.stringify(" + PARAM_profile + ")+ \", \" +\n" + - "JSON.stringify(" + PARAM_meal_data + ")+ \") \");" + "JSON.stringify(" + PARAM_currentTemp + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_iobData + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_profile + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_autosens_data + ")+ \", \" +\n" + + "JSON.stringify(" + PARAM_meal_data + ")+ \") \");" ); mV8rt.executeVoidScript( "var rT = determine_basal(" + @@ -120,9 +128,9 @@ public class DetermineBasalAdapterAMAJS { PARAM_currentTemp + ", " + PARAM_iobData + ", " + PARAM_profile + ", " + - "undefined, " + + PARAM_autosens_data + ", " + PARAM_meal_data + ", " + - "setTempBasal" + + "tempBasalFunctions" + ");"); @@ -144,6 +152,8 @@ public class DetermineBasalAdapterAMAJS { storedCurrentTemp = mV8rt.executeStringScript("JSON.stringify(" + PARAM_currentTemp + ");"); storedProfile = mV8rt.executeStringScript("JSON.stringify(" + PARAM_profile + ");"); storedMeal_data = mV8rt.executeStringScript("JSON.stringify(" + PARAM_meal_data + ");"); + if (mAutosensData != null) + storedAutosens_data = mV8rt.executeStringScript("JSON.stringify(" + PARAM_autosens_data + ");"); return result; } @@ -168,10 +178,21 @@ public class DetermineBasalAdapterAMAJS { return storedMeal_data; } + String getAutosensDataParam() { + return storedAutosens_data; + } + private void loadScript() throws IOException { + mV8rt.executeVoidScript(readFile("OpenAPSAMA/round-basal.js"), "OpenAPSAMA/round-basal.js", 0); + mV8rt.executeVoidScript("var round_basal = module.exports;"); + mV8rt.executeVoidScript("require = function() {return round_basal;};"); + + mV8rt.executeVoidScript(readFile("OpenAPSAMA/basal-set-temp.js"), "OpenAPSAMA/basal-set-temp.js ", 0); + mV8rt.executeVoidScript("var tempBasalFunctions = module.exports;"); + mV8rt.executeVoidScript( readFile("OpenAPSAMA/determine-basal.js"), - "OpenAPSAMA/bin/oref0-determine-basal.js", + "OpenAPSAMA/determine-basal.js", 0); mV8rt.executeVoidScript("var determine_basal = module.exports;"); mV8rt.executeVoidScript( @@ -228,7 +249,8 @@ public class DetermineBasalAdapterAMAJS { PumpInterface pump, IobTotal iobData, DatabaseHelper.GlucoseStatus glucoseStatus, - TreatmentsPlugin.MealData mealData) { + TreatmentsPlugin.MealData mealData, + JSONObject autosensData) { String units = profile.getUnits(); @@ -258,18 +280,31 @@ public class DetermineBasalAdapterAMAJS { mGlucoseStatus.add("glucose", glucoseStatus.glucose); mGlucoseStatus.add("delta", glucoseStatus.delta); mGlucoseStatus.add("avgdelta", glucoseStatus.avgdelta); + mGlucoseStatus.add("short_avgdelta", glucoseStatus.short_avgdelta); + mGlucoseStatus.add("long_avgdelta", glucoseStatus.long_avgdelta); mMealData.add("carbs", mealData.carbs); mMealData.add("boluses", mealData.boluses); + mMealData.add("mealCOB", mealData.mealCOB); + + if (autosensData != null) { + mAutosensData = new V8Object(mV8rt); + // TODO: add autosens data here for AMA + } else { + mV8rt.executeVoidScript("autosens_data = undefined"); + } } - public void release() { + public void release() { mProfile.release(); mCurrentTemp.release(); mIobData.release(); mMealData.release(); mGlucoseStatus.release(); + if (mAutosensData != null) { + mAutosensData.release(); + } mV8rt.release(); } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java index b666465a2e..06544b79ca 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/OpenAPSAMA/OpenAPSAMAPlugin.java @@ -188,7 +188,7 @@ public class OpenAPSAMAPlugin implements PluginBase, APSInterface { if (!checkOnlyHardLimits(profile.getMaxDailyBasal(), "max_daily_basal", 0.1, 10)) return; if (!checkOnlyHardLimits(pump.getBaseBasalRate(), "current_basal", 0.01, 5)) return; - determineBasalAdapterAMAJS.setData(profile, maxIob, maxBasal, minBg, maxBg, targetBg, pump, iobTotal, glucoseStatus, mealData); + determineBasalAdapterAMAJS.setData(profile, maxIob, maxBasal, minBg, maxBg, targetBg, pump, iobTotal, glucoseStatus, mealData, null); DetermineBasalResultAMA determineBasalResultAMA = determineBasalAdapterAMAJS.invoke(); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsPlugin.java index d26b6d0b8c..432c9339bc 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsPlugin.java @@ -141,6 +141,7 @@ public class TreatmentsPlugin implements PluginBase, TreatmentsInterface { public class MealData { public double boluses = 0d; public double carbs = 0d; + public double mealCOB = 0.0d; // TODO: add calculation for AMA } @Override