diff --git a/bin/oref0-autotune-prep.js b/bin/oref0-autotune-prep.js index 76cda757f..5b79c525a 100755 --- a/bin/oref0-autotune-prep.js +++ b/bin/oref0-autotune-prep.js @@ -22,7 +22,7 @@ var generate = require('oref0/lib/autotune-prep'); function usage ( ) { - console.error('usage: ', process.argv.slice(0, 2), ' [carbhistory.json] [autotune/glucose.json]'); + console.error('usage: ', process.argv.slice(0, 2), ' [pumpprofile.json] [carbhistory.json] [autotune/glucose.json]'); } if (!module.parent) { @@ -33,8 +33,9 @@ if (!module.parent) { } var profile_input = process.argv.slice(3, 4).pop(); var glucose_input = process.argv.slice(4, 5).pop(); - var carb_input = process.argv.slice(5, 6).pop() - var prepped_glucose_input = process.argv.slice(6, 7).pop() + var pumpprofile_input = process.argv.slice(5, 6).pop() + var carb_input = process.argv.slice(6, 7).pop() + var prepped_glucose_input = process.argv.slice(7, 8).pop() if ( !pumphistory_input || !profile_input || !glucose_input ) { usage( ); @@ -51,10 +52,23 @@ if (!module.parent) { return console.error("Could not parse input data: ", e); } + var pumpprofile_data = { }; + if (typeof pumpprofile_input != 'undefined') { + try { + pumpprofile_data = JSON.parse(fs.readFileSync(pumpprofile_input, 'utf8')); + } catch (e) { + console.error("Warning: could not parse "+pumpprofile_input); + } + } + // disallow impossibly low carbRatios due to bad decoding if ( typeof(profile_data.carb_ratio) == 'undefined' || profile_data.carb_ratio < 2 ) { - console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratio ' + profile_data.carb_ratio + ' out of bounds" }'); - return console.error("Error: carb_ratio " + profile_data.carb_ratio + " out of bounds"); + if ( typeof(pumpprofile_data.carb_ratio) == 'undefined' || pumpprofile_data.carb_ratio < 2 ) { + console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratios ' + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + ' out of bounds" }'); + return console.error("Error: carb_ratios " + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + " out of bounds"); + } else { + profile_data.carb_ratio = pumpprofile_data.carb_ratio; + } } try { diff --git a/bin/oref0-autotune-recommends-report.sh b/bin/oref0-autotune-recommends-report.sh index 09627cc43..2c0fb89c2 100755 --- a/bin/oref0-autotune-recommends-report.sh +++ b/bin/oref0-autotune-recommends-report.sh @@ -64,7 +64,7 @@ csf_new=$(cat $directory/autotune/profile.json | jq '.csf') carb_ratio_new=$(cat $directory/autotune/profile.json | jq '.carb_ratio') # Print Header Info -printf "%-${parameter_width}s| %-${data_width}s| %-${data_width}s\n" "Parameter" "Current" "Autotune" >> $report_file +printf "%-${parameter_width}s| %-${data_width}s| %-${data_width}s\n" "Parameter" "Pump" "Autotune" >> $report_file printf "%s\n" "-------------------------------------" >> $report_file # Print ISF, CSF and Carb Ratio Recommendations @@ -74,10 +74,10 @@ if [ $csf_current != null ]; then else printf "%-${parameter_width}s| %-${data_width}s| %-${data_width}.3f\n" "CSF [mg/dL/g]" "n/a" $csf_new >> $report_file fi -printf "%-${parameter_width}s| %-${data_width}.3f| %-${data_width}.3f\n" "Carb Ratio [g]" $carb_ratio_current $carb_ratio_new >> $report_file +printf "%-${parameter_width}s| %-${data_width}.3f| %-${data_width}.3f\n" "Carb Ratio[g/U]" $carb_ratio_current $carb_ratio_new >> $report_file # Print Basal Profile Recommendations -printf "%-${parameter_width}s| %-${data_width}s|\n" "Basal Profile [unit/hour]" "-" >> $report_file +printf "%-${parameter_width}s| %-${data_width}s|\n" "Basals [U/hr]" "-" >> $report_file # Build time_list array of H:M in 30 minute increments to mirror pump basal schedule time_list=() diff --git a/bin/oref0-autotune.sh b/bin/oref0-autotune.sh index 64a15e487..1b1ecac32 100755 --- a/bin/oref0-autotune.sh +++ b/bin/oref0-autotune.sh @@ -1,7 +1,7 @@ #!/bin/bash # This script sets up an easy test environment for autotune, allowing the user to vary parameters -# like start/end date and number of runs. +# like start/end date. # # Required Inputs: # DIR, (--dir=) @@ -10,8 +10,6 @@ # Optional Inputs: # END_DATE, (--end-date=) # if no end date supplied, assume we want a months worth or until day before current day -# NUMBER_OF_RUNS (--runs=) -# if no number of runs designated, then default to 5 # EXPORT_EXCEL (--xlsx=) # export to excel. Disabled by default # TERMINAL_LOGGING (--log @@ -39,7 +37,8 @@ DIR="" NIGHTSCOUT_HOST="" START_DATE="" END_DATE="" -NUMBER_OF_RUNS=1 # Default to a single run if not otherwise specified +START_DAYS_AGO=1 # Default to yesterday if not otherwise specified +END_DAYS_AGO=1 # Default to yesterday if not otherwise specified EXPORT_EXCEL="" # Default is to not export to Microsoft Excel TERMINAL_LOGGING=true RECOMMENDS_REPORT=true @@ -96,8 +95,12 @@ case $i in END_DATE=`date --date="$END_DATE" +%Y-%m-%d` shift # past argument=value ;; - -r=*|--runs=*) - NUMBER_OF_RUNS="${i#*=}" + -t=*|--start-days-ago=*) + START_DAYS_AGO="${i#*=}" + shift # past argument=value + ;; + -d=*|--end-days-ago=*) + END_DAYS_AGO="${i#*=}" shift # past argument=value ;; -x=*|--xlsx=*) @@ -116,22 +119,25 @@ case $i in esac done +# remove any trailing / from NIGHTSCOUT_HOST +NIGHTSCOUT_HOST=$(echo $NIGHTSCOUT_HOST | sed 's/\/$//g') + if [[ -z "$DIR" || -z "$NIGHTSCOUT_HOST" ]]; then - echo "Usage: oref0-autotune <--dir=myopenaps_directory> <--ns-host=https://mynightscout.azurewebsites.net> [--start-date=YYYY-MM-DD] [--end-date=YYYY-MM-DD] [--runs=number_of_runs] [--xlsx=autotune.xlsx] [--log=(true)|false]" + echo "Usage: oref0-autotune <--dir=myopenaps_directory> <--ns-host=https://mynightscout.azurewebsites.net> [--start-days-ago=number_of_days] [--end-days-ago=number_of_days] [--start-date=YYYY-MM-DD] [--end-date=YYYY-MM-DD] [--xlsx=autotune.xlsx] [--log=(true)|false]" exit 1 fi if [[ -z "$START_DATE" ]]; then # Default start date of yesterday - START_DATE=`date --date="1 day ago" +%Y-%m-%d` + START_DATE=`date --date="$START_DAYS_AGO days ago" +%Y-%m-%d` fi if [[ -z "$END_DATE" ]]; then # Default end-date as this morning at midnight in order to not get partial day samples for now # (ISF/CSF adjustments are still single values across each day) - END_DATE=`date --date="1 day ago" +%Y-%m-%d` + END_DATE=`date --date="$END_DAYS_AGO days ago" +%Y-%m-%d` fi if [[ -z "$UNKNOWN_OPTION" ]] ; then # everything is ok - echo "Running oref0-autotune --dir=$DIR --ns-host=$NIGHTSCOUT_HOST --start-date=$START_DATE --runs=$NUMBER_OF_RUNS --end-date=$END_DATE" + echo "Running oref0-autotune --dir=$DIR --ns-host=$NIGHTSCOUT_HOST --start-date=$START_DATE --end-date=$END_DATE" else echo "Unknown options. Exiting" exit 1 @@ -157,15 +163,6 @@ if [[ $TERMINAL_LOGGING = "true" ]]; then exec &> >(tee -a autotune.$(date +%Y-%m-%d-%H%M%S).log) fi -# Pull Nightscout Data -echo "Grabbing NIGHTSCOUT treatments.json for date range..." - -# Get Nightscout carb and insulin Treatments -query="find%5Bcreated_at%5D%5B%24gte%5D=`date --date="$START_DATE -4 hours" -Iminutes`&find%5Bcreated_at%5D%5B%24lte%5D=`date --date="$END_DATE +1 days" -Iminutes`" -echo Query: $NIGHTSCOUT_HOST/$query -ns-get host $NIGHTSCOUT_HOST treatments.json $query > ns-treatments.json || die "Couldn't download ns-treatments.json" -ls -la ns-treatments.json || die "No ns-treatments.json downloaded" - # Build date list for autotune iteration date_list=() date=$START_DATE; @@ -179,76 +176,74 @@ do fi done -echo "Grabbing NIGHTSCOUT entries/sgv.json for date range..." +echo "Grabbing NIGHTSCOUT treatments.json and entries/sgv.json for date range..." # Get Nightscout BG (sgv.json) Entries for i in "${date_list[@]}" do - query="find%5Bdate%5D%5B%24gte%5D=`(date -d $i +%s | tr -d '\n'; echo 000)`&find%5Bdate%5D%5B%24lte%5D=`(date --date="$i +1 days" +%s | tr -d '\n'; echo 000)`&count=1000" - echo Query: $NIGHTSCOUT_HOST $query - ns-get host $NIGHTSCOUT_HOST entries/sgv.json $query > ns-entries.$i.json || die "Couldn't download ns-entries.$i.json" - ls -la ns-entries.$i.json || die "No ns-entries.$i.json downloaded" -done - -echo "Running $NUMBER_OF_RUNS runs from $START_DATE to $END_DATE" -sleep 2 - -# Do iterative runs over date range, save autotune.json (prepped data) and input/output -# profile.json -# Loop 1: Run 1 to Number of Runs specified by user or by default (1) -for run_number in $(seq 1 $NUMBER_OF_RUNS) -do - # Loop 2: Iterate through Date Range - for i in "${date_list[@]}" - do - cp profile.json profile.$run_number.$i.json + query="find%5Bdate%5D%5B%24gte%5D=`(date -d $i +%s | tr -d '\n'; echo 000)`&find%5Bdate%5D%5B%24lte%5D=`(date --date="$i +1 days" +%s | tr -d '\n'; echo 000)`&count=1000" + echo Query: $NIGHTSCOUT_HOST $query + ns-get host $NIGHTSCOUT_HOST entries/sgv.json $query > ns-entries.$i.json || die "Couldn't download ns-entries.$i.json" + ls -la ns-entries.$i.json || die "No ns-entries.$i.json downloaded" + + # Get Nightscout carb and insulin Treatments + # echo $i $START_DATE; + #query="find%5Bdate%5D%5B%24gte%5D=`(date -d $i +%s | tr -d'\n'; echo 000)`&find%5Bdate%5D%5B%24lte%5D=`(date --date="$i +1 days" +%s | tr -d '\n'; echo 000)`&count=1000" + query="find%5Bcreated_at%5D%5B%24gte%5D=`date --date="$i -5 hours" -Iminutes`&find%5Bcreated_at%5D%5B%24lte%5D=`date --date="$i +1 days" -Iminutes`" + echo Query: $NIGHTSCOUT_HOST/$query + ns-get host $NIGHTSCOUT_HOST treatments.json $query > ns-treatments.$i.json || die "Couldn't download ns-treatments.$i.json" + ls -la ns-treatments.$i.json || die "No ns-treatments.$i.json downloaded" + + + # Do iterative runs over date range, save autotune.json (prepped data) and input/output profile.json + cp profile.json profile.$i.json # Autotune Prep (required args, ), output prepped glucose # data or below - echo "oref0-autotune-prep ns-treatments.json profile.json ns-entries.$i.json > autotune.$run_number.$i.json" - oref0-autotune-prep ns-treatments.json profile.json ns-entries.$i.json > autotune.$run_number.$i.json \ - || die "Could not run oref0-autotune-prep ns-treatments.json profile.json ns-entries.$i.json" + echo "oref0-autotune-prep ns-treatments.$i.json profile.json ns-entries.$i.json profile.pump.json > autotune.$i.json" + oref0-autotune-prep ns-treatments.$i.json profile.json ns-entries.$i.json profile.pump.json > autotune.$i.json \ + || die "Could not run oref0-autotune-prep ns-treatments.$i.json profile.json ns-entries.$i.json" # Autotune (required args, ), # output autotuned profile or what will be used as in the next iteration - echo "oref0-autotune-core autotune.$run_number.$i.json profile.json profile.pump.json > newprofile.$run_number.$i.json" - if ! oref0-autotune-core autotune.$run_number.$i.json profile.json profile.pump.json > newprofile.$run_number.$i.json; then + echo "oref0-autotune-core autotune.$i.json profile.json profile.pump.json > newprofile.$i.json" + if ! oref0-autotune-core autotune.$i.json profile.json profile.pump.json > newprofile.$i.json; then if cat profile.json | jq --exit-status .carb_ratio==null; then echo "ERROR: profile.json contains null carb_ratio: using profile.pump.json" cp profile.pump.json profile.json exit else - die "Could not run oref0-autotune-core autotune.$run_number.$i.json profile.json profile.pump.json" + die "Could not run oref0-autotune-core autotune.$i.json profile.json profile.pump.json" fi else # Copy tuned profile produced by autotune to profile.json for use with next day of data - cp newprofile.$run_number.$i.json profile.json + cp newprofile.$i.json profile.json fi - done # End Date Range Iteration -done # End Number of Runs Loop -if ! [[ -z "$EXPORT_EXCEL" ]]; then - echo Exporting to $EXPORT_EXCEL - oref0-autotune-export-to-xlsx --dir $DIR --output $EXPORT_EXCEL -fi + if ! [[ -z "$EXPORT_EXCEL" ]]; then + echo Exporting to $EXPORT_EXCEL + oref0-autotune-export-to-xlsx --dir $DIR --output $EXPORT_EXCEL + fi -# Create Summary Report of Autotune Recommendations and display in the terminal -if [[ $RECOMMENDS_REPORT == "true" ]]; then - # Set the report file name, so we can let the user know where it is and cat - # it to the screen - report_file=$directory/autotune/autotune_recommendations.log + # Create Summary Report of Autotune Recommendations and display in the terminal + if [[ $RECOMMENDS_REPORT == "true" ]]; then + # Set the report file name, so we can let the user know where it is and cat + # it to the screen + report_file=$directory/autotune/autotune_recommendations.log - echo - echo "Autotune pump profile recommendations:" - echo "---------------------------------------------------------" + echo + echo "Autotune pump profile recommendations:" + echo "---------------------------------------------------------" - # Let the user know where the Autotune Recommendations are logged - echo "Recommendations Log File: $report_file" - echo + # Let the user know where the Autotune Recommendations are logged + echo "Recommendations Log File: $report_file" + echo - # Run the Autotune Recommends Report - oref0-autotune-recommends-report $directory + # Run the Autotune Recommends Report + oref0-autotune-recommends-report $directory - # Go ahead and echo autotune_recommendations.log to the terminal, minus blank lines - cat $report_file | egrep -v "\| *\| *$" -fi + # Go ahead and echo autotune_recommendations.log to the terminal, minus blank lines + cat $report_file | egrep -v "\| *\| *$" + fi + +done # End Date Range Iteration diff --git a/bin/oref0-pump-loop.sh b/bin/oref0-pump-loop.sh index d6b71afbe..0628426b8 100755 --- a/bin/oref0-pump-loop.sh +++ b/bin/oref0-pump-loop.sh @@ -330,10 +330,10 @@ function mmtune { reset_spi_serial.py 2>/dev/null fi oref0_init_pump_comms.py - echo -n "Listening for 30s silence before mmtuning: " + echo -n "Listening for 40s silence before mmtuning: " for i in $(seq 1 800); do echo -n . - any_pump_comms 30 2>/dev/null | egrep -v subg | egrep No \ + any_pump_comms 40 2>/dev/null | egrep -v subg | egrep No \ && break done echo {} > monitor/mmtune.json @@ -353,12 +353,12 @@ function maybe_mmtune { if ( find monitor/ -mmin -15 | egrep -q "pump_loop_completed" ); then # mmtune ~ 25% of the time [[ $(( ( RANDOM % 100 ) )) > 75 ]] \ - && echo "Waiting for 30s silence before mmtuning" \ - && wait_for_silence 30 \ + && echo "Waiting for 40s silence before mmtuning" \ + && wait_for_silence 40 \ && mmtune else - echo "pump_loop_completed more than 15m old; waiting for 30s silence before mmtuning" - wait_for_silence 30 + echo "pump_loop_completed more than 15m old; waiting for 40s silence before mmtuning" + wait_for_silence 40 mmtune fi } @@ -370,7 +370,7 @@ function any_pump_comms { # listen for $1 seconds of silence (no other rigs talking to pump) before continuing function wait_for_silence { if [ -z $1 ]; then - waitfor=30 + waitfor=40 else waitfor=$1 fi diff --git a/lib/autotune-prep/categorize.js b/lib/autotune-prep/categorize.js index 91ac0995e..378301f9a 100644 --- a/lib/autotune-prep/categorize.js +++ b/lib/autotune-prep/categorize.js @@ -2,6 +2,8 @@ var tz = require('moment-timezone'); var basal = require('oref0/lib/profile/basal'); var getIOB = require('oref0/lib/iob'); var ISF = require('../profile/isf'); +var find_insulin = require('oref0/lib/iob/history'); +var dosed = require('./dosed'); // main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals. @@ -37,10 +39,11 @@ function categorizeBGDatums(opts) { profile: profileData , history: opts.pumpHistory }; - var mealCOB = 0; var CSFGlucoseData = []; var ISFGlucoseData = []; var basalGlucoseData = []; + var UAMGlucoseData = []; + var CRData = []; var bucketedData = []; bucketedData[0] = glucoseData[0]; @@ -96,10 +99,12 @@ function categorizeBGDatums(opts) { } } //console.error(treatments); - absorbing = 0; - uam = 0; // unannounced meal - mealCOB = 0; - mealCarbs = 0; + var calculatingCR = false; + var absorbing = 0; + var uam = 0; // unannounced meal + var mealCOB = 0; + var mealCarbs = 0; + var CRCarbs = 0; var type=""; // main for loop for (var i=bucketedData.length-5; i > 0; --i) { @@ -110,6 +115,7 @@ function categorizeBGDatums(opts) { // As we're processing each data point, go through the treatment.carbs and see if any of them are older than // the current BG data point. If so, add those carbs to COB. var treatment = treatments[treatments.length-1]; + var myCarbs = 0; if (treatment) { var treatmentDate = new Date(tz(treatment.timestamp)); var treatmentTime = treatmentDate.getTime(); @@ -118,6 +124,7 @@ function categorizeBGDatums(opts) { if (treatment.carbs >= 1) { mealCOB += parseFloat(treatment.carbs); mealCarbs += parseFloat(treatment.carbs); + myCarbs = treatment.carbs; } treatments.pop(); } @@ -141,12 +148,12 @@ function categorizeBGDatums(opts) { avgDelta = avgDelta.toFixed(2); glucoseDatum.avgDelta = avgDelta; - + //sens = ISF var sens = ISF.isfLookup(IOBInputs.profile.isfProfile,BGDate); IOBInputs.clock=BGDate.toISOString(); - // use the average of the last 4 hours' basals to help convergence; - // this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge. + // use the average of the last 4 hours' basals to help convergence; + // this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge. currentBasal = basal.basalLookup(opts.basalprofile, BGDate); BGDate1hAgo = new Date(BGTime-1*60*60*1000); BGDate2hAgo = new Date(BGTime-2*60*60*1000); @@ -159,7 +166,7 @@ function categorizeBGDatums(opts) { //console.error(currentBasal,basal1hAgo,basal2hAgo,basal3hAgo,IOBInputs.profile.currentBasal); // basalBGI is BGI of basal insulin activity. - basalBGI = Math.round(( currentBasal * sens / 60 * 5 )*100)/100; // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m + basalBGI = Math.round(( currentBasal * sens / 60 * 5 )*100)/100; // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m //console.log(JSON.stringify(IOBInputs.profile)); // call iob since calculated elsewhere var iob = getIOB(IOBInputs)[0]; @@ -182,14 +189,70 @@ function categorizeBGDatums(opts) { var profile = profileData; ci = Math.max(deviation, profile.min_5m_carbimpact); absorbed = ci * profile.carb_ratio / sens; + // Store the COB, and use it as the starting point for the next data point. mealCOB = Math.max(0, mealCOB-absorbed); } - // Store the COB, and use it as the starting point for the next data point. + + + // 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. + + if (mealCOB > 0 || calculatingCR ) { + // set initial values when we first see COB + CRCarbs += myCarbs; + if (!calculatingCR) { + CRInitialIOB = iob.iob; + CRInitialBG = glucoseDatum.glucose; + CRInitialCarbTime = new Date(glucoseDatum.date); + console.error("CRInitialIOB:",CRInitialIOB,"CRInitialBG:",CRInitialBG,"CRInitialCarbTime:",CRInitialCarbTime); + } + // keep calculatingCR as long as we have COB or enough IOB + if ( mealCOB > 0 && i>1 ) { + calculatingCR = true; + } else if ( iob.iob > currentBasal/2 && i>1 ) { + calculatingCR = true; + // when COB=0 and IOB drops low enough, record end values and be done calculatingCR + } else { + CREndIOB = iob.iob; + CREndBG = glucoseDatum.glucose; + CREndTime = new Date(glucoseDatum.date); + console.error("CREndIOB:",CREndIOB,"CREndBG:",CREndBG,"CREndTime:",CREndTime); + var CRDatum = { + CRInitialIOB: CRInitialIOB + , CRInitialBG: CRInitialBG + , CRInitialCarbTime: CRInitialCarbTime + , CREndIOB: CREndIOB + , CREndBG: CREndBG + , CREndTime: CREndTime + , CRCarbs: CRCarbs + }; + //console.error(CRDatum); + + var CRElapsedMinutes = Math.round((CREndTime - CRInitialCarbTime) / 1000 / 60); + //console.error(CREndTime - CRInitialCarbTime, CRElapsedMinutes); + if ( CRElapsedMinutes < 60 ) { + console.error("Ignoring",CRElapsedMinutes,"m CR period."); + } else { + CRData.push(CRDatum); + } + + CRCarbs = 0; + calculatingCR = false; + } + } + // If mealCOB is zero but all deviations since hitting COB=0 are positive, assign those data points to CSFGlucoseData // Once deviations go negative for at least one data point after COB=0, we can use the rest of the data to tune ISF or basals if (mealCOB > 0 || absorbing || mealCarbs > 0) { - if (deviation > 0) { + // if meal IOB has decayed, then end absorption after this data point unless COB > 0 + if ( iob.iob < currentBasal/2 ) { + absorbing = 0; + // otherwise, as long as deviations are positive, keep tracking carb deviations + } else if (deviation > 0) { absorbing = 1; } else { absorbing = 0; @@ -214,7 +277,8 @@ function categorizeBGDatums(opts) { console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption"); } - if (iob.iob > currentBasal || uam) { + // check that we have a decent amount of basal tuning data before excluding data as UAM + if ((iob.iob > currentBasal || uam) ) { if (deviation > 0) { uam = 1; } else { @@ -225,6 +289,7 @@ function categorizeBGDatums(opts) { console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption"); } type="uam"; + UAMGlucoseData.push(glucoseDatum); } else { if ( type === "uam" ) { console.error("end unannounced meal absorption"); @@ -240,12 +305,12 @@ function categorizeBGDatums(opts) { // attempting to prevent basal from being calculated as negative; should help prevent basals from going below 0 var minPossibleDeviation = -( basalBGI + Math.max(0,BGI) ); //var minPossibleDeviation = -basalBGI; - if ( deviation < minPossibleDeviation ) { - console.error("Adjusting deviation",deviation,"to",minPossibleDeviation.toFixed(2)); - deviation = minPossibleDeviation; - deviation = deviation.toFixed(2); - glucoseDatum.deviation = deviation; - } + //if ( deviation < minPossibleDeviation ) { + //console.error("Adjusting deviation",deviation,"to",minPossibleDeviation.toFixed(2)); + //deviation = minPossibleDeviation; + //deviation = deviation.toFixed(2); + //glucoseDatum.deviation = deviation; + //} type="basal"; basalGlucoseData.push(glucoseDatum); } else { @@ -266,7 +331,49 @@ function categorizeBGDatums(opts) { console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"basalBGI:",basalBGI.toFixed(1),"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",deviation,"avgDelta:",avgDelta,type); } + var IOBInputs = { + profile: profileData + , history: opts.pumpHistory + }; + var treatments = find_insulin(IOBInputs); + CRData.forEach(function(CRDatum) { + var dosedOpts = { + treatments: treatments + , profile: opts.profile + , start: CRDatum.CRInitialCarbTime + , end: CRDatum.CREndTime + }; + var insulinDosed = dosed(dosedOpts); + CRDatum.CRInsulin = insulinDosed.insulin; + //console.error(CRDatum); + }); + + var CSFLength = CSFGlucoseData.length; + var ISFLength = ISFGlucoseData.length; + var UAMLength = UAMGlucoseData.length; + var basalLength = basalGlucoseData.length; + + if (4*basalLength < CSFLength) { + console.error("Warning: too many deviations categorized as meals"); + //console.error("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones"); + //var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData); + console.error("Adding",CSFLength,"CSF deviations to",ISFLength,"ISF ones"); + var ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData); + CSFGlucoseData = []; + } + + if (2*basalLength < UAMLength) { + //console.error(basalGlucoseData, UAMGlucoseData); + console.error("Warning: too many deviations categorized as UnAnnounced Meals"); + console.error("Adding",UAMLength,"UAM deviations to",basalLength,"basal ones"); + var basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData); + console.error("Adding",UAMLength,"UAM deviations to",ISFLength,"ISF ones"); + var ISFGlucoseData = ISFGlucoseData.concat(UAMGlucoseData); + //console.error(ISFGlucoseData.length, UAMLength); + } + return { + CRData: CRData, CSFGlucoseData: CSFGlucoseData, ISFGlucoseData: ISFGlucoseData, basalGlucoseData: basalGlucoseData diff --git a/lib/autotune-prep/dosed.js b/lib/autotune-prep/dosed.js new file mode 100644 index 000000000..41c207ab3 --- /dev/null +++ b/lib/autotune-prep/dosed.js @@ -0,0 +1,28 @@ +function insulinDosed(opts) { + + var start = opts.start.getTime(); + var end = opts.end.getTime(); + var treatments = opts.treatments; + var profile_data = opts.profile; + var insulinDosed = 0; + if (!treatments) { + console.error("No treatments to process."); + return {}; + } + + treatments.forEach(function(treatment) { + //console.error(treatment); + if(treatment.insulin && treatment.date > start && treatment.date <= end) { + insulinDosed += treatment.insulin; + } + }); + //console.error(insulinDosed); + + var rval = { + insulin: Math.round( insulinDosed * 1000 ) / 1000 + }; + + return rval; +} + +exports = module.exports = insulinDosed; diff --git a/lib/autotune-prep/index.js b/lib/autotune-prep/index.js index 0afa3dde9..971610468 100644 --- a/lib/autotune-prep/index.js +++ b/lib/autotune-prep/index.js @@ -3,7 +3,7 @@ var tz = require('moment-timezone'); var find_meals = require('oref0/lib/meal/history'); -var sum = require('./categorize'); +var categorize = require('./categorize'); function generate (inputs) { @@ -19,7 +19,7 @@ function generate (inputs) { , basalprofile: inputs.profile.basalprofile }; - var autotune_prep_output = sum(opts); + var autotune_prep_output = categorize(opts); return autotune_prep_output; } diff --git a/lib/autotune/index.js b/lib/autotune/index.js index 10739dbcd..b29f2e588 100644 --- a/lib/autotune/index.js +++ b/lib/autotune/index.js @@ -23,6 +23,9 @@ function tuneAllTheThings (inputs) { pumpCarbRatio = pumpProfile.carb_ratio; 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; @@ -31,12 +34,40 @@ function tuneAllTheThings (inputs) { //console.error(ISFGlucose[0]); var basalGlucose = preppedGlucose.basalGlucoseData; //console.error(basalGlucose[0]); + var CRData = preppedGlucose.CRData; + //console.error(CRData); + + // 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) { + CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG; + CRInsulinReq = CRBGChange / ISF; + CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB; + CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq; + //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRInsulinTotal); + CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000; + //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRInsulinTotal); + //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulinTotal,"CR:",CR); + if (CRDatum.CRInsulin > 0) { + CRTotalCarbs += CRDatum.CRCarbs; + CRTotalInsulin += CRDatum.CRInsulinTotal; + } + }); + CRTotalInsulin = Math.round(CRTotalInsulin*1000)/1000; + 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 - hourlyBasalProfile = []; - hourlyPumpProfile = []; + var hourlyBasalProfile = []; + var hourlyPumpProfile = []; for (var i=0; i < 24; i++) { - // aututuned basal profile + // autotuned basal profile for (var j=0; j < basalProfile.length; ++j) { if (basalProfile[j].minutes <= i * 60) { if (basalProfile[j].rate == 0) { @@ -70,6 +101,7 @@ function tuneAllTheThings (inputs) { } //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++) { @@ -98,8 +130,8 @@ function tuneAllTheThings (inputs) { offsetHour = hour + offset; if (offsetHour < 0) { offsetHour += 24; } //console.error(offsetHour); - hourlyBasalProfile[offsetHour].rate += basalNeeded / 3; - hourlyBasalProfile[offsetHour].rate=Math.round(hourlyBasalProfile[offsetHour].rate*1000)/1000 + 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 @@ -108,41 +140,171 @@ function tuneAllTheThings (inputs) { for (var offset=-3; offset < 0; offset++) { offsetHour = hour + offset; if (offsetHour < 0) { offsetHour += 24; } - threeHourBasal += hourlyBasalProfile[offsetHour].rate; + threeHourBasal += newHourlyBasalProfile[offsetHour].rate; } var adjustmentRatio = 1.0 + basalNeeded / threeHourBasal; //console.error(adjustmentRatio); for (var offset=-3; offset < 0; offset++) { offsetHour = hour + offset; if (offsetHour < 0) { offsetHour += 24; } - hourlyBasalProfile[offsetHour].rate = hourlyBasalProfile[offsetHour].rate * adjustmentRatio; - hourlyBasalProfile[offsetHour].rate=Math.round(hourlyBasalProfile[offsetHour].rate*1000)/1000 + newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio; + newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000 } } } if (pumpBasalProfile && pumpBasalProfile[0]) { for (var hour=0; hour < 24; hour++) { - //console.error(hourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2); + //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2); // cap adjustments at autosens_max and autosens_min autotuneMax = pumpProfile.autosens_max; autotuneMin = pumpProfile.autosens_min; var maxRate = hourlyPumpProfile[hour].rate * autotuneMax; var minRate = hourlyPumpProfile[hour].rate * autotuneMin; - if (hourlyBasalProfile[hour].rate > maxRate ) { + 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,")"); - hourlyBasalProfile[hour].rate = maxRate; - } else if (hourlyBasalProfile[hour].rate < minRate ) { + 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,")"); - hourlyBasalProfile[hour].rate = minRate; + newHourlyBasalProfile[hour].rate = minRate; + } + newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate*1000)/1000; + } + } + + // 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 (var 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); + } } - hourlyBasalProfile[hour].rate = Math.round(hourlyBasalProfile[hour].rate*1000)/1000; + //console.error(hour, newHourlyBasalProfile); + newHourlyBasalProfile[hour].rate = Math.round( (0.8*hourlyBasalProfile[hour].rate + 0.1*newHourlyBasalProfile[lastAdjustedHour].rate + 0.1*newHourlyBasalProfile[nextAdjustedHour].rate)*1000 )/1000; + 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(hourlyBasalProfile); - basalProfile = hourlyBasalProfile; + 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 (var 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); + 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% + 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); } + } + 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 + fullNewCR = carbRatio; + } else { + // how much change would be required to account for all of the deviations + fullNewCR = totalCR; + } + // only adjust by 10% + newCR = ( 0.8 * carbRatio ) + ( 0.2 * fullNewCR ); + // safety cap CR + if (typeof(pumpCarbRatio) !== 'undefined') { + var maxCR = pumpCarbRatio * autotuneMax; + var minCR = pumpCarbRatio * autotuneMin; + 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 = []; @@ -195,6 +357,12 @@ function tuneAllTheThings (inputs) { adjustedISF = minISF; } + // average both the directly-calculated ISF and the ISFFromCRAndCSF if we're reasonably well tuned + if (p50ratios > 0.4 && p50ratios < 0.6) { + // TODO: figure out if there's a way to do this without messing up CSF tuning + // adjustedISF = (adjustedISF + ISFFromCRAndCSF) / 2; + } + // and apply 10% of that adjustment var newISF = ( 0.9 * ISF ) + ( 0.1 * adjustedISF ); @@ -213,70 +381,15 @@ function tuneAllTheThings (inputs) { 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); - ISF = newISF; - - // 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 (var 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); - 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)); - } + if (newISF) { + ISF = newISF; } - // 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 10% - newCSF = ( 0.9 * CSF ) + ( 0.1 * 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); } - } - newCSF = Math.round( newCSF * 1000 ) / 1000; - console.error("totalMealCarbs:",totalMealCarbs,"totalDeviations:",totalDeviations,"fullNewCSF:",fullNewCSF,"newCSF:",newCSF); - // this is where CSF is set based on the outputs - CSF = newCSF; + // reconstruct updated version of previousAutotune as autotuneOutput autotuneOutput = previousAutotune; @@ -285,7 +398,7 @@ function tuneAllTheThings (inputs) { autotuneOutput.isfProfile = isfProfile; autotuneOutput.sens = ISF; autotuneOutput.csf = CSF; - carbRatio = ISF / CSF; + //carbRatio = ISF / CSF; carbRatio = Math.round( carbRatio * 1000 ) / 1000; autotuneOutput.carb_ratio = carbRatio;