Merge pull request #1699 from Philoul/Autotune/AutotuneClean

Autotune Plugin (to replace #1468)
This commit is contained in:
Milos Kozak 2022-05-10 10:48:59 +02:00 committed by GitHub
commit 79b348ba76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 3637 additions and 9 deletions

View file

@ -28,6 +28,7 @@ import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.configBuilder.PluginStore
import info.nightscout.androidaps.plugins.constraints.safety.SafetyPlugin
import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin
import info.nightscout.androidaps.plugins.general.autotune.AutotunePlugin
import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin
import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin
import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus
@ -65,6 +66,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
@Inject lateinit var config: Config
@Inject lateinit var automationPlugin: AutomationPlugin
@Inject lateinit var autotunePlugin: AutotunePlugin
@Inject lateinit var danaRPlugin: DanaRPlugin
@Inject lateinit var danaRKoreanPlugin: DanaRKoreanPlugin
@Inject lateinit var danaRv2Plugin: DanaRv2Plugin
@ -187,6 +189,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
addPreferencesFromResourceIfEnabled(tidepoolPlugin, rootKey)
addPreferencesFromResourceIfEnabled(smsCommunicatorPlugin, rootKey)
addPreferencesFromResourceIfEnabled(automationPlugin, rootKey)
addPreferencesFromResourceIfEnabled(autotunePlugin, rootKey)
addPreferencesFromResourceIfEnabled(wearPlugin, rootKey)
addPreferencesFromResourceIfEnabled(statusLinePlugin, rootKey)
addPreferencesFromResource(R.xml.pref_alerts, rootKey)

View file

@ -12,6 +12,7 @@ import info.nightscout.androidaps.dana.di.DanaModule
import info.nightscout.androidaps.danar.di.DanaRModule
import info.nightscout.androidaps.danars.di.DanaRSModule
import info.nightscout.androidaps.database.DatabaseModule
import info.nightscout.androidaps.dependencyInjection.AutotuneModule
import info.nightscout.androidaps.diaconn.di.DiaconnG8Module
import info.nightscout.androidaps.insight.di.InsightDatabaseModule
import info.nightscout.androidaps.insight.di.InsightModule
@ -37,6 +38,7 @@ import javax.inject.Singleton
ReceiversModule::class,
ServicesModule::class,
AutomationModule::class,
AutotuneModule::class,
CommandQueueModule::class,
ObjectivesModule::class,
WizardModule::class,

View file

@ -15,6 +15,7 @@ import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin
import info.nightscout.androidaps.plugins.configBuilder.PluginStore
import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctionImplementation
import info.nightscout.androidaps.plugins.general.autotune.AutotunePlugin
import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefsImpl
import info.nightscout.androidaps.plugins.general.maintenance.PrefFileListProvider
import info.nightscout.androidaps.plugins.general.nsclient.DataSyncSelectorImplementation
@ -98,6 +99,7 @@ open class AppModule {
@Binds fun bindImportExportPrefsInterface(importExportPrefs: ImportExportPrefsImpl): ImportExportPrefs
@Binds fun bindIconsProviderInterface(iconsProvider: IconsProviderImplementation): IconsProvider
@Binds fun bindLoopInterface(loopPlugin: LoopPlugin): Loop
@Binds fun bindAutotuneInterface(autotunePlugin: AutotunePlugin): Autotune
@Binds fun bindIobCobCalculatorInterface(iobCobCalculatorPlugin: IobCobCalculatorPlugin): IobCobCalculator
@Binds fun bindSmsCommunicatorInterface(smsCommunicatorPlugin: SmsCommunicatorPlugin): SmsCommunicator
@Binds fun bindDataSyncSelector(dataSyncSelectorImplementation: DataSyncSelectorImplementation): DataSyncSelector

View file

@ -0,0 +1,23 @@
package info.nightscout.androidaps.dependencyInjection
import dagger.Module
import dagger.android.ContributesAndroidInjector
import info.nightscout.androidaps.plugins.general.autotune.AutotuneCore
import info.nightscout.androidaps.plugins.general.autotune.AutotuneIob
import info.nightscout.androidaps.plugins.general.autotune.AutotunePrep
import info.nightscout.androidaps.plugins.general.autotune.AutotuneFS
import info.nightscout.androidaps.plugins.general.autotune.data.*
@Module
@Suppress("unused")
abstract class AutotuneModule {
@ContributesAndroidInjector abstract fun autoTunePrepInjector(): AutotunePrep
@ContributesAndroidInjector abstract fun autoTuneIobInjector(): AutotuneIob
@ContributesAndroidInjector abstract fun autoTuneCoreInjector(): AutotuneCore
@ContributesAndroidInjector abstract fun autoTuneFSInjector(): AutotuneFS
@ContributesAndroidInjector abstract fun autoTuneATProfileInjector(): ATProfile
@ContributesAndroidInjector abstract fun autoTuneBGDatumInjector(): BGDatum
@ContributesAndroidInjector abstract fun autoTuneCRDatumInjector(): CRDatum
@ContributesAndroidInjector abstract fun autoTunePreppedGlucoseInjector(): PreppedGlucose
}

View file

@ -12,6 +12,7 @@ import info.nightscout.androidaps.plugins.constraints.objectives.ObjectivesFragm
import info.nightscout.androidaps.plugins.constraints.objectives.activities.ObjectivesExamDialog
import info.nightscout.androidaps.plugins.general.actions.ActionsFragment
import info.nightscout.androidaps.plugins.general.automation.AutomationFragment
import info.nightscout.androidaps.plugins.general.autotune.AutotuneFragment
import info.nightscout.androidaps.plugins.general.food.FoodFragment
import info.nightscout.androidaps.plugins.general.maintenance.MaintenanceFragment
import info.nightscout.androidaps.plugins.general.nsclient.NSClientFragment
@ -36,6 +37,7 @@ abstract class FragmentsModule {
@ContributesAndroidInjector abstract fun contributesActionsFragment(): ActionsFragment
@ContributesAndroidInjector abstract fun contributesAutomationFragment(): AutomationFragment
@ContributesAndroidInjector abstract fun contributesAutotuneFragment(): AutotuneFragment
@ContributesAndroidInjector abstract fun contributesBGSourceFragment(): BGSourceFragment
@ContributesAndroidInjector abstract fun contributesConfigBuilderFragment(): ConfigBuilderFragment

View file

@ -25,6 +25,7 @@ import info.nightscout.androidaps.plugins.constraints.storage.StorageConstraintP
import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerPlugin
import info.nightscout.androidaps.plugins.general.actions.ActionsPlugin
import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin
import info.nightscout.androidaps.plugins.general.autotune.AutotunePlugin
import info.nightscout.androidaps.plugins.general.dataBroadcaster.DataBroadcastPlugin
import info.nightscout.androidaps.plugins.general.food.FoodPlugin
import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin
@ -232,6 +233,12 @@ abstract class PluginsModule {
@IntKey(250)
abstract fun bindAutomationPlugin(plugin: AutomationPlugin): PluginBase
@Binds
@AllConfigs
@IntoMap
@IntKey(255)
abstract fun bindAutotunePlugin(plugin: AutotunePlugin): PluginBase
@Binds
@AllConfigs
@IntoMap

View file

@ -0,0 +1,516 @@
package info.nightscout.androidaps.plugins.general.autotune
import info.nightscout.androidaps.R
import info.nightscout.androidaps.data.LocalInsulin
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin
import info.nightscout.androidaps.utils.Round
import info.nightscout.shared.sharedPreferences.SP
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AutotuneCore @Inject constructor(
private val sp: SP,
private val autotuneFS: AutotuneFS
) {
fun tuneAllTheThings(preppedGlucose: PreppedGlucose, previousAutotune: ATProfile, pumpProfile: ATProfile): ATProfile {
//var pumpBasalProfile = pumpProfile.basalprofile;
val pumpBasalProfile = pumpProfile.basal
//console.error(pumpBasalProfile);
var basalProfile = previousAutotune.basal
//console.error(basalProfile);
//console.error(isfProfile);
var isf = previousAutotune.isf
//console.error(isf);
var carbRatio = previousAutotune.ic
//console.error(carbRatio);
var csf = isf / carbRatio
var dia = previousAutotune.dia
var peak = previousAutotune.peak
val csfGlucose = preppedGlucose.csfGlucoseData
val isfGlucose = preppedGlucose.isfGlucoseData
val basalGlucose = preppedGlucose.basalGlucoseData
val crData = preppedGlucose.crData
val diaDeviations = preppedGlucose.diaDeviations
val peakDeviations = preppedGlucose.peakDeviations
val pumpISF = pumpProfile.isf
val pumpCarbRatio = pumpProfile.ic
val pumpCSF = pumpISF / pumpCarbRatio
// Autosens constraints
val autotuneMax = sp.getDouble(R.string.key_openapsama_autosens_max, 1.2)
val autotuneMin = sp.getDouble(R.string.key_openapsama_autosens_min, 0.7)
val min5minCarbImpact = sp.getDouble(R.string.key_openapsama_min_5m_carbimpact, 3.0)
// tune DIA
var newDia = dia
if (diaDeviations.size > 0)
{
val currentDiaMeanDev = diaDeviations[2].meanDeviation
val currentDiaRMSDev = diaDeviations[2].rmsDeviation
//Console.WriteLine(DIA,currentDIAMeanDev,currentDIARMSDev);
var minMeanDeviations = 1000000.0
var minRmsDeviations = 1000000.0
var meanBest = 2
var rmsBest = 2
for (i in 0..diaDeviations.size-1)
{
val meanDeviations = diaDeviations[i].meanDeviation
val rmsDeviations = diaDeviations[i].rmsDeviation
if (meanDeviations < minMeanDeviations)
{
minMeanDeviations = Round.roundTo(meanDeviations, 0.001)
meanBest = i
}
if (rmsDeviations < minRmsDeviations)
{
minRmsDeviations = Round.roundTo(rmsDeviations, 0.001)
rmsBest = i
}
}
log("Best insulinEndTime for meanDeviations: ${diaDeviations[meanBest].dia} hours")
log("Best insulinEndTime for RMSDeviations: ${diaDeviations[rmsBest].dia} hours")
if (meanBest < 2 && rmsBest < 2)
{
if (diaDeviations[1].meanDeviation < currentDiaMeanDev * 0.99 && diaDeviations[1].rmsDeviation < currentDiaRMSDev * 0.99)
newDia = diaDeviations[1].dia
}
else if (meanBest > 2 && rmsBest > 2)
{
if (diaDeviations[3].meanDeviation < currentDiaMeanDev * 0.99 && diaDeviations[3].rmsDeviation < currentDiaRMSDev * 0.99)
newDia = diaDeviations[3].dia
}
if (newDia > 12.0)
{
log("insulinEndTime maximum is 12h: not raising further")
newDia = 12.0
}
if (newDia != dia)
log("Adjusting insulinEndTime from $dia to $newDia hours")
else
log("Leaving insulinEndTime unchanged at $dia hours")
}
// tune insulinPeakTime
var newPeak = peak
if (peakDeviations.size > 2)
{
val currentPeakMeanDev = peakDeviations[2].meanDeviation
val currentPeakRMSDev = peakDeviations[2].rmsDeviation
//Console.WriteLine(currentPeakMeanDev);
var minMeanDeviations = 1000000.0
var minRmsDeviations = 1000000.0
var meanBest = 2
var rmsBest = 2
for (i in 0..peakDeviations.size-1)
{
val meanDeviations = peakDeviations[i].meanDeviation;
val rmsDeviations = peakDeviations[i].rmsDeviation;
if (meanDeviations < minMeanDeviations)
{
minMeanDeviations = Round.roundTo(meanDeviations, 0.001)
meanBest = i
}
if (rmsDeviations < minRmsDeviations)
{
minRmsDeviations = Round.roundTo(rmsDeviations, 0.001)
rmsBest = i
}
}
log("Best insulinPeakTime for meanDeviations: ${peakDeviations[meanBest].peak} minutes")
log("Best insulinPeakTime for RMSDeviations: ${peakDeviations[rmsBest].peak} minutes")
if (meanBest < 2 && rmsBest < 2)
{
if (peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[1].rmsDeviation < currentPeakRMSDev * 0.99)
newPeak = peakDeviations[1].peak
}
else if (meanBest > 2 && rmsBest > 2)
{
if (peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[3].rmsDeviation < currentPeakRMSDev * 0.99)
newPeak = peakDeviations[3].peak
}
if (newPeak != peak)
log("Adjusting insulinPeakTime from " + peak + " to " + newPeak + " minutes")
else
log("Leaving insulinPeakTime unchanged at " + peak)
}
// Calculate carb ratio (CR) independently of csf and isf
// Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2
// For now, if another meal IOB/COB stacks on top of it, consider them together
// Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize
// Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR.
//autotune-core (lib/autotune/index.js) #149-#165
var crTotalCarbs = 0.0
var crTotalInsulin = 0.0
for (i in crData.indices) {
val crDatum = crData[i]
val crBGChange = crDatum.crEndBG - crDatum.crInitialBG
val crInsulinReq = crBGChange / isf
//val crIOBChange = crDatum.crEndIOB - crDatum.crInitialIOB
crDatum.crInsulinTotal = crDatum.crInitialIOB + crDatum.crInsulin + crInsulinReq
//log(crDatum.crInitialIOB + " " + crDatum.crInsulin + " " + crInsulinReq + " " + crDatum.crInsulinTotal);
//val cr = Round.roundTo(crDatum.crCarbs / crDatum.crInsulinTotal, 0.001)
//log(crBGChange + " " + crInsulinReq + " " + crIOBChange + " " + crDatum.crInsulinTotal);
//log("CRCarbs: " + crDatum.crCarbs + " CRInsulin: " + crDatum.crInsulinTotal + " CR:" + cr);
if (crDatum.crInsulinTotal > 0) {
crTotalCarbs += crDatum.crCarbs
crTotalInsulin += crDatum.crInsulinTotal
}
}
//autotune-core (lib/autotune/index.js) #166-#169
crTotalInsulin = Round.roundTo(crTotalInsulin, 0.001)
var totalCR = 0.0
if (crTotalInsulin != 0.0)
totalCR = Round.roundTo(crTotalCarbs / crTotalInsulin, 0.001)
log("crTotalCarbs: $crTotalCarbs crTotalInsulin: $crTotalInsulin totalCR: $totalCR")
//autotune-core (lib/autotune/index.js) #170-#209 (already hourly in aaps)
// convert the basal profile to hourly if it isn't already
val hourlyBasalProfile = basalProfile
//log(hourlyPumpProfile.toString());
//log(hourlyBasalProfile.toString());
val newHourlyBasalProfile = DoubleArray(24)
for (i in 0..23) {
newHourlyBasalProfile[i] = hourlyBasalProfile[i]
}
val basalUntuned = previousAutotune.basalUntuned
//autotune-core (lib/autotune/index.js) #210-#266
// look at net deviations for each hour
for (hour in 0..23) {
var deviations = 0.0
for (i in basalGlucose.indices) {
val BGTime = Calendar.getInstance()
//var BGTime: Date? = null
if (basalGlucose[i].date != 0L) {
BGTime.setTimeInMillis(basalGlucose[i].date)
//BGTime = Date(basalGlucose[i].date)
} else {
log("Could not determine last BG time")
}
val myHour = BGTime.get(Calendar.HOUR_OF_DAY)
//val myHour = BGTime!!.hours
if (hour == myHour) {
//log.debug(basalGlucose[i].deviation);
deviations += basalGlucose[i].deviation
}
}
deviations = Round.roundTo(deviations, 0.001)
log("Hour $hour total deviations: $deviations mg/dL")
// calculate how much less or additional basal insulin would have been required to eliminate the deviations
// only apply 20% of the needed adjustment to keep things relatively stable
var basalNeeded = 0.2 * deviations / isf
basalNeeded = Round.roundTo(basalNeeded, 0.01)
// if basalNeeded is positive, adjust each of the 1-3 hour prior basals by 10% of the needed adjustment
log("Hour $hour basal adjustment needed: $basalNeeded U/hr")
if (basalNeeded > 0) {
for (offset in -3..-1) {
var offsetHour = hour + offset
if (offsetHour < 0) {
offsetHour += 24
}
//log.debug(offsetHour);
newHourlyBasalProfile[offsetHour] = newHourlyBasalProfile[offsetHour] + basalNeeded / 3
newHourlyBasalProfile[offsetHour] = Round.roundTo(newHourlyBasalProfile[offsetHour], 0.001)
}
// otherwise, figure out the percentage reduction required to the 1-3 hour prior basals
// and adjust all of them downward proportionally
} else if (basalNeeded < 0) {
var threeHourBasal = 0.0
for (offset in -3..-1) {
var offsetHour = hour + offset
if (offsetHour < 0) {
offsetHour += 24
}
threeHourBasal += newHourlyBasalProfile[offsetHour]
}
val adjustmentRatio = 1.0 + basalNeeded / threeHourBasal
//log.debug(adjustmentRatio);
for (offset in -3..-1) {
var offsetHour = hour + offset
if (offsetHour < 0) {
offsetHour += 24
}
newHourlyBasalProfile[offsetHour] = newHourlyBasalProfile[offsetHour] * adjustmentRatio
newHourlyBasalProfile[offsetHour] = Round.roundTo(newHourlyBasalProfile[offsetHour], 0.001)
}
}
}
//autotune-core (lib/autotune/index.js) #267-#294
for (hour in 0..23) {
//log.debug(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2);
// cap adjustments at autosens_max and autosens_min
val maxRate = pumpBasalProfile[hour] * autotuneMax
val minRate = pumpBasalProfile[hour] * autotuneMin
if (newHourlyBasalProfile[hour] > maxRate) {
log("Limiting hour " + hour + " basal to " + Round.roundTo(maxRate, 0.01) + " (which is " + Round.roundTo(autotuneMax, 0.01) + " * pump basal of " + pumpBasalProfile[hour] + ")")
//log.debug("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is 20% above pump basal of",hourlyPumpProfile[hour].rate,")");
newHourlyBasalProfile[hour] = maxRate
} else if (newHourlyBasalProfile[hour] < minRate) {
log("Limiting hour " + hour + " basal to " + Round.roundTo(minRate, 0.01) + " (which is " + autotuneMin + " * pump basal of " + newHourlyBasalProfile[hour] + ")")
//log.debug("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is 20% below pump basal of",hourlyPumpProfile[hour].rate,")");
newHourlyBasalProfile[hour] = minRate
}
newHourlyBasalProfile[hour] = Round.roundTo(newHourlyBasalProfile[hour], 0.001)
}
// 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
//autotune-core (lib/autotune/index.js) #302-#323
for (hour in 0..23) {
if (hourlyBasalProfile[hour] == newHourlyBasalProfile[hour]) {
var nextAdjustedHour = 23
for (nextHour in hour..23) {
if (hourlyBasalProfile[nextHour] != newHourlyBasalProfile[nextHour]) {
nextAdjustedHour = nextHour
break
//} else {
// log("At hour: "+nextHour +" " + hourlyBasalProfile[nextHour] + " " +newHourlyBasalProfile[nextHour]);
}
}
//log.debug(hour, newHourlyBasalProfile);
newHourlyBasalProfile[hour] = Round.roundTo(0.8 * hourlyBasalProfile[hour] + 0.1 * newHourlyBasalProfile[lastAdjustedHour] + 0.1 * newHourlyBasalProfile[nextAdjustedHour], 0.001)
basalUntuned[hour]++
log("Adjusting hour " + hour + " basal from " + hourlyBasalProfile[hour] + " to " + newHourlyBasalProfile[hour] + " based on hour " + lastAdjustedHour + " = " + newHourlyBasalProfile[lastAdjustedHour] + " and hour " + nextAdjustedHour + " = " + newHourlyBasalProfile[nextAdjustedHour])
} else {
lastAdjustedHour = hour
}
}
//log(newHourlyBasalProfile.toString());
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.0
var mealCarbs = 0
var totalMealCarbs = 0
var totalDeviations = 0.0
val fullNewCSF: Double
//log.debug(CSFGlucose[0].mealAbsorption);
//log.debug(CSFGlucose[0]);
//autotune-core (lib/autotune/index.js) #346-#365
for (i in csfGlucose.indices) {
//log.debug(CSFGlucose[i].mealAbsorption, i);
if (csfGlucose[i].mealAbsorption === "start") {
deviations = 0.0
mealCarbs = csfGlucose[i].mealCarbs
} else if (csfGlucose[i].mealAbsorption === "end") {
deviations += csfGlucose[i].deviation
// compare the sum of deviations from start to end vs. current csf * mealCarbs
//log.debug(csf,mealCarbs);
//val csfRise = csf * mealCarbs
//log.debug(deviations,isf);
//log.debug("csfRise:",csfRise,"deviations:",deviations);
totalMealCarbs += mealCarbs
totalDeviations += deviations
} else {
//todo Philoul check 0 * min5minCarbImpact ???
deviations += Math.max(0 * min5minCarbImpact, csfGlucose[i].deviation)
mealCarbs = Math.max(mealCarbs, 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.0) {
totalDeviations += deviations
}
//log.debug(totalDeviations, totalMealCarbs);
fullNewCSF = if (totalMealCarbs == 0) {
// if no meals today, csf is unchanged
csf
} else {
// how much change would be required to account for all of the deviations
Round.roundTo(totalDeviations / totalMealCarbs, 0.01)
}
// only adjust by 20%
var newCSF = 0.8 * csf + 0.2 * fullNewCSF
// safety cap csf
if (pumpCSF != 0.0) {
val maxCSF = pumpCSF * autotuneMax
val minCSF = pumpCSF * autotuneMin
if (newCSF > maxCSF) {
log("Limiting csf to " + Round.roundTo(maxCSF, 0.01) + " (which is " + autotuneMax + "* pump csf of " + pumpCSF + ")")
newCSF = maxCSF
} else if (newCSF < minCSF) {
log("Limiting csf to " + Round.roundTo(minCSF, 0.01) + " (which is" + autotuneMin + "* pump csf of " + pumpCSF + ")")
newCSF = minCSF
} //else { log.debug("newCSF",newCSF,"is close enough to",pumpCSF); }
}
val oldCSF = Round.roundTo(csf, 0.001)
newCSF = Round.roundTo(newCSF, 0.001)
totalDeviations = Round.roundTo(totalDeviations, 0.001)
log("totalMealCarbs: $totalMealCarbs totalDeviations: $totalDeviations oldCSF $oldCSF fullNewCSF: $fullNewCSF newCSF: $newCSF")
// this is where csf is set based on the outputs
//if (newCSF != 0.0) {
// csf = newCSF
//}
var fullNewCR: Double
fullNewCR = if (totalCR == 0.0) {
// if no meals today, CR is unchanged
carbRatio
} else {
// how much change would be required to account for all of the deviations
totalCR
}
// don't tune CR out of bounds
var maxCR = pumpCarbRatio * autotuneMax
if (maxCR > 150) {
maxCR = 150.0
}
var minCR = pumpCarbRatio * autotuneMin
if (minCR < 3) {
minCR = 3.0
}
// safety cap fullNewCR
if (pumpCarbRatio != 0.0) {
if (fullNewCR > maxCR) {
log("Limiting fullNewCR from " + fullNewCR + " to " + Round.roundTo(maxCR, 0.01) + " (which is " + autotuneMax + " * pump CR of " + pumpCarbRatio + ")")
fullNewCR = maxCR
} else if (fullNewCR < minCR) {
log("Limiting fullNewCR from " + fullNewCR + " to " + Round.roundTo(minCR, 0.01) + " (which is " + autotuneMin + " * pump CR of " + pumpCarbRatio + ")")
fullNewCR = minCR
} //else { log.debug("newCR",newCR,"is close enough to",pumpCarbRatio); }
}
// only adjust by 20%
var newCR = 0.8 * carbRatio + 0.2 * fullNewCR
// safety cap newCR
if (pumpCarbRatio != 0.0) {
if (newCR > maxCR) {
log("Limiting CR to " + Round.roundTo(maxCR, 0.01) + " (which is " + autotuneMax + " * pump CR of " + pumpCarbRatio + ")")
newCR = maxCR
} else if (newCR < minCR) {
log("Limiting CR to " + Round.roundTo(minCR, 0.01) + " (which is " + autotuneMin + " * pump CR of " + pumpCarbRatio + ")")
newCR = minCR
} //else { log.debug("newCR",newCR,"is close enough to",pumpCarbRatio); }
}
newCR = Round.roundTo(newCR, 0.001)
log("oldCR: $carbRatio fullNewCR: $fullNewCR newCR: $newCR")
// this is where CR is set based on the outputs
//var ISFFromCRAndCSF = isf;
if (newCR != 0.0) {
carbRatio = newCR
//ISFFromCRAndCSF = Math.round( carbRatio * csf * 1000)/1000;
}
// calculate median deviation and bgi in data attributable to isf
val isfDeviations: MutableList<Double> = ArrayList()
val bGIs: MutableList<Double> = ArrayList()
val avgDeltas: MutableList<Double> = ArrayList()
val ratios: MutableList<Double> = ArrayList()
var count = 0
for (i in isfGlucose.indices) {
val deviation = isfGlucose[i].deviation
isfDeviations.add(deviation)
val BGI = isfGlucose[i].bgi
bGIs.add(BGI)
val avgDelta = isfGlucose[i].avgDelta
avgDeltas.add(avgDelta)
val ratio = 1 + deviation / BGI
//log.debug("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio);
ratios.add(ratio)
count++
}
Collections.sort(avgDeltas)
Collections.sort(bGIs)
Collections.sort(isfDeviations)
Collections.sort(ratios)
var p50deviation = IobCobCalculatorPlugin.percentile(isfDeviations.toTypedArray(), 0.50)
var p50BGI = IobCobCalculatorPlugin.percentile(bGIs.toTypedArray(), 0.50)
val p50ratios = Round.roundTo(IobCobCalculatorPlugin.percentile(ratios.toTypedArray(), 0.50), 0.001)
var fullNewISF = isf
if (count < 10) {
// leave isf unchanged if fewer than 5 isf data points
log("Only found " + isfGlucose.size + " ISF data points, leaving ISF unchanged at " + isf)
} else {
// calculate what adjustments to isf would have been necessary to bring median deviation to zero
fullNewISF = isf * p50ratios
}
fullNewISF = Round.roundTo(fullNewISF, 0.001)
// adjust the target isf to be a weighted average of fullNewISF and pumpISF
val adjustmentFraction: Double
/*
// TODO: philoul may be allow adjustmentFraction in settings with safety limits ?)
if (typeof(pumpProfile.autotune_isf_adjustmentFraction) !== 'undefined') {
adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction;
} else {*/
adjustmentFraction = 1.0
// }
// low autosens ratio = high isf
val maxISF = pumpISF / autotuneMin
// high autosens ratio = low isf
val minISF = pumpISF / autotuneMax
var adjustedISF = 0.0
var newISF = 0.0
if (pumpISF != 0.0) {
adjustedISF = if (fullNewISF < 0) {
isf
} else {
adjustmentFraction * fullNewISF + (1 - adjustmentFraction) * pumpISF
}
// cap adjustedISF before applying 10%
//log.debug(adjustedISF, maxISF, minISF);
if (adjustedISF > maxISF) {
log("Limiting adjusted isf of " + Round.roundTo(adjustedISF, 0.01) + " to " + Round.roundTo(maxISF, 0.01) + "(which is pump isf of " + pumpISF + "/" + autotuneMin + ")")
adjustedISF = maxISF
} else if (adjustedISF < minISF) {
log("Limiting adjusted isf of" + Round.roundTo(adjustedISF, 0.01) + " to " + Round.roundTo(minISF, 0.01) + "(which is pump isf of " + pumpISF + "/" + autotuneMax + ")")
adjustedISF = minISF
}
// and apply 20% of that adjustment
newISF = 0.8 * isf + 0.2 * adjustedISF
if (newISF > maxISF) {
log("Limiting isf of" + Round.roundTo(newISF, 0.01) + "to" + Round.roundTo(maxISF, 0.01) + "(which is pump isf of" + pumpISF + "/" + autotuneMin + ")")
newISF = maxISF
} else if (newISF < minISF) {
log("Limiting isf of" + Round.roundTo(newISF, 0.01) + "to" + Round.roundTo(minISF, 0.01) + "(which is pump isf of" + pumpISF + "/" + autotuneMax + ")")
newISF = minISF
}
}
newISF = Round.roundTo(newISF, 0.001)
//log.debug(avgRatio);
//log.debug(newISF);
p50deviation = Round.roundTo(p50deviation, 0.001)
p50BGI = Round.roundTo(p50BGI, 0.001)
adjustedISF = Round.roundTo(adjustedISF, 0.001)
log("p50deviation: $p50deviation p50BGI $p50BGI p50ratios: $p50ratios Old isf: $isf fullNewISF: $fullNewISF adjustedISF: $adjustedISF newISF: $newISF")
if (newISF != 0.0) {
isf = newISF
}
previousAutotune.from = preppedGlucose.from
previousAutotune.basal = basalProfile
previousAutotune.isf = isf
previousAutotune.ic = Round.roundTo(carbRatio, 0.001)
previousAutotune.basalUntuned = basalUntuned
previousAutotune.dia = newDia
previousAutotune.peak = newPeak
val localInsulin = LocalInsulin("Ins_$newPeak-$newDia", newPeak, newDia)
previousAutotune.localInsulin = localInsulin
previousAutotune.updateProfile()
return previousAutotune
}
private fun log(message: String) {
autotuneFS.atLog("[Core] $message")
}
}

View file

@ -0,0 +1,204 @@
package info.nightscout.androidaps.plugins.general.autotune
import info.nightscout.androidaps.interfaces.ResourceHelper
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
import info.nightscout.androidaps.plugins.general.maintenance.LoggerUtils
import info.nightscout.androidaps.R
import org.json.JSONException
import org.slf4j.LoggerFactory
import java.io.*
import java.text.SimpleDateFormat
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AutotuneFS @Inject constructor(
private val rh: ResourceHelper,
private val loggerUtils: LoggerUtils
) {
val AUTOTUNEFOLDER = "autotune"
val SETTINGSFOLDER = "settings"
val RECOMMENDATIONS = "autotune_recommendations.log"
val ENTRIESPREF = "aaps-entries."
val TREATMENTSPREF = "aaps-treatments."
val AAPSBOLUSESPREF = "aaps-boluses."
val PREPPEDPREF = "aaps-autotune."
val SETTINGS = "settings.json"
val PROFIL = "profil"
val PUMPPROFILE = "pumpprofile.json"
val TUNEDPROFILE = "newaapsprofile."
val LOGPREF = "autotune."
val ZIPPREF = "autotune_"
lateinit var autotunePath: File
lateinit var autotuneSettings: File
private var logString = ""
val BUFFER_SIZE = 2048
private val log = LoggerFactory.getLogger(AutotunePlugin::class.java)
/*****************************************************************************
* Create autotune folder for all files created during an autotune session
*****************************************************************************/
fun createAutotuneFolder() {
//create autotune subfolder for autotune files if not exists
autotunePath = File(loggerUtils.logDirectory, AUTOTUNEFOLDER)
if (!(autotunePath.exists() && autotunePath.isDirectory)) {
autotunePath.mkdir()
log("Create $AUTOTUNEFOLDER subfolder in ${loggerUtils.logDirectory}")
}
autotuneSettings = File(loggerUtils.logDirectory, SETTINGSFOLDER)
if (!(autotuneSettings.exists() && autotuneSettings.isDirectory)) {
autotuneSettings.mkdir()
log("Create $SETTINGSFOLDER subfolder in ${loggerUtils.logDirectory}")
}
}
/*****************************************************************************
* between each run of autotune, clean autotune folder content
*****************************************************************************/
fun deleteAutotuneFiles() {
autotunePath.listFiles()?.let { listFiles ->
for (file in listFiles) {
if (file.isFile) file.delete()
}
}
autotuneSettings.listFiles()?.let { listFiles ->
for (file in listFiles) {
if (file.isFile) file.delete()
}
}
log("Delete previous Autotune files")
}
/*****************************************************************************
* Create a JSON autotune files or settings files
*****************************************************************************/
fun exportSettings(settings: String) {
createAutotunefile(SETTINGS, settings, true)
}
fun exportPumpProfile(profile: ATProfile) {
createAutotunefile(PUMPPROFILE, profile.profiletoOrefJSON(), true)
createAutotunefile(PUMPPROFILE, profile.profiletoOrefJSON())
}
fun exportTunedProfile(tunedProfile: ATProfile) {
createAutotunefile(TUNEDPROFILE + formatDate(tunedProfile.from) + ".json", tunedProfile.profiletoOrefJSON())
try {
createAutotunefile(rh.gs(R.string.autotune_tunedprofile_name) + ".json", tunedProfile.profiletoOrefJSON(), true)
} catch (e: JSONException) {
}
}
fun exportEntries(autotuneIob: AutotuneIob) {
try {
createAutotunefile(ENTRIESPREF + formatDate(autotuneIob.startBG) + ".json", autotuneIob.glucoseToJSON())
} catch (e: JSONException) {
}
}
fun exportTreatments(autotuneIob: AutotuneIob) {
try {
createAutotunefile(TREATMENTSPREF + formatDate(autotuneIob.startBG) + ".json", autotuneIob.nsHistoryToJSON())
createAutotunefile(AAPSBOLUSESPREF + formatDate(autotuneIob.startBG) + ".json", autotuneIob.bolusesToJSON())
} catch (e: JSONException) {
}
}
fun exportPreppedGlucose(preppedGlucose: PreppedGlucose) {
createAutotunefile(PREPPEDPREF + formatDate(preppedGlucose.from) + ".json", preppedGlucose.toString(2))
}
fun exportResult(result: String) {
createAutotunefile(RECOMMENDATIONS, result)
}
fun exportLog(lastRun: Long, index: Int = 0) {
val suffix = if (index == 0) "" else "_" + index
log("Create " + LOGPREF + formatDate(lastRun) + suffix + ".log" + " file in " + AUTOTUNEFOLDER + " folder")
createAutotunefile(LOGPREF + formatDate(lastRun) + suffix + ".log", logString)
logString = ""
}
fun exportLogAndZip(lastRun: Long) {
log("Create " + LOGPREF + formatDate(lastRun) + ".log" + " file in " + AUTOTUNEFOLDER + " folder")
createAutotunefile(LOGPREF + formatDate(lastRun) + ".log", logString)
zipAutotune(lastRun)
logString = ""
}
private fun createAutotunefile(fileName: String, stringFile: String, isSettingFile: Boolean = false) {
val autotuneFile = File(if (isSettingFile) autotuneSettings.absolutePath else autotunePath.absolutePath, fileName)
try {
val fw = FileWriter(autotuneFile)
val pw = PrintWriter(fw)
pw.println(stringFile)
pw.close()
fw.close()
log("Create " + fileName + " file in " + (if (isSettingFile) SETTINGSFOLDER else AUTOTUNEFOLDER) + " folder")
} catch (e: FileNotFoundException) {
//log.error("Unhandled exception", e);
} catch (e: IOException) {
//log.error("Unhandled exception", e);
}
}
/**********************************************************************************
* create a zip file with all autotune files and settings in autotune folder at the end of run
**********************************************************************************/
fun zipAutotune(lastRun: Long) {
try {
val zipFileName = ZIPPREF + formatDate(lastRun, true) + ".zip"
val zipFile = File(loggerUtils.logDirectory, zipFileName)
val out = ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile)))
zipDirectory(autotunePath, autotunePath.name, out)
zipDirectory(autotuneSettings, autotuneSettings.name, out)
out.flush()
out.close()
log("Create $zipFileName file in ${loggerUtils.logDirectory} folder")
} catch (e: IOException) {
//log.error("Unhandled exception", e);
}
}
private fun log(message: String) {
atLog("[FS] $message")
}
fun atLog(message: String) {
logString += "$message\n"
log.debug(message)
}
private fun zipDirectory(folder: File, parentFolder: String, out: ZipOutputStream) {
folder.listFiles()?.let { listFiles ->
for (file in listFiles) {
if (file.isDirectory) {
zipDirectory(file, parentFolder + "/" + file.name, out)
continue
}
try {
out.putNextEntry(ZipEntry(parentFolder + "/" + file.name))
val bis = BufferedInputStream(FileInputStream(file))
//long bytesRead = 0;
val bytesIn = ByteArray(BUFFER_SIZE)
var read: Int
while (bis.read(bytesIn).also { read = it } != -1) {
out.write(bytesIn, 0, read)
}
out.closeEntry()
} catch (e: IOException) {
//log.error("Unhandled exception", e);
}
}
}
}
private fun formatDate(date: Long, dateHour: Boolean = false): String {
val dateFormat = if (dateHour) SimpleDateFormat("yyyy-MM-dd_HH-mm-ss") else SimpleDateFormat("yyyy-MM-dd")
return dateFormat.format(date)
}
}

View file

@ -0,0 +1,500 @@
package info.nightscout.androidaps.plugins.general.autotune
import android.graphics.Typeface
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import dagger.android.HasAndroidInjector
import dagger.android.support.DaggerFragment
import info.nightscout.androidaps.Constants
import info.nightscout.androidaps.R
import info.nightscout.androidaps.databinding.AutotuneFragmentBinding
import info.nightscout.androidaps.dialogs.ProfileViewerDialog
import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
import info.nightscout.androidaps.plugins.general.autotune.events.EventAutotuneUpdateGui
import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin
import info.nightscout.androidaps.plugins.profile.local.events.EventLocalProfileChanged
import info.nightscout.androidaps.data.LocalInsulin
import info.nightscout.androidaps.data.ProfileSealed
import info.nightscout.androidaps.database.entities.UserEntry
import info.nightscout.androidaps.database.entities.ValueWithUnit
import info.nightscout.androidaps.extensions.runOnUiThread
import info.nightscout.androidaps.interfaces.*
import info.nightscout.androidaps.logging.UserEntryLogger
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.FabricPrivacy
import info.nightscout.androidaps.utils.MidnightTime
import info.nightscout.androidaps.utils.Round
import info.nightscout.androidaps.utils.alertDialogs.OKDialog.showConfirmation
import info.nightscout.shared.SafeParse
import info.nightscout.shared.sharedPreferences.SP
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.json.JSONObject
//import org.slf4j.LoggerFactory
import java.text.DecimalFormat
import java.util.*
import javax.inject.Inject
class AutotuneFragment : DaggerFragment() {
@Inject lateinit var profileFunction: ProfileFunction
@Inject lateinit var autotunePlugin: AutotunePlugin
@Inject lateinit var autotuneFS: AutotuneFS
@Inject lateinit var sp: SP
@Inject lateinit var dateUtil: DateUtil
@Inject lateinit var activePlugin: ActivePlugin
@Inject lateinit var localProfilePlugin: LocalProfilePlugin
@Inject lateinit var fabricPrivacy: FabricPrivacy
@Inject lateinit var uel: UserEntryLogger
@Inject lateinit var rh: ResourceHelper
@Inject lateinit var rxBus: RxBus
@Inject lateinit var injector: HasAndroidInjector
private var disposable: CompositeDisposable = CompositeDisposable()
//private val log = LoggerFactory.getLogger(AutotunePlugin::class.java)
private var _binding: AutotuneFragmentBinding? = null
private lateinit var profileStore: ProfileStore
private var profileName = ""
private lateinit var profile: ATProfile
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = AutotuneFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
autotunePlugin.lastRun = sp.getLong(R.string.key_autotune_last_run, 0)
if (autotunePlugin.lastNbDays.isEmpty())
autotunePlugin.lastNbDays = sp.getInt(R.string.key_autotune_default_tune_days, 5).toString()
val defaultValue = sp.getInt(R.string.key_autotune_default_tune_days, 5).toDouble()
profileStore = activePlugin.activeProfileSource.profile ?: ProfileStore(injector, JSONObject(), dateUtil)
profileName = if (binding.profileList.text.toString() == rh.gs(R.string.active)) "" else binding.profileList.text.toString()
profileFunction.getProfile()?.let { currentProfile ->
profile = ATProfile(profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(it) } ?:currentProfile, LocalInsulin(""), injector)
}
binding.tuneDays.setParams(
savedInstanceState?.getDouble("tunedays")
?: defaultValue, 1.0, 30.0, 1.0, DecimalFormat("0"), false, null, textWatcher)
binding.autotuneRun.setOnClickListener {
val daysBack = SafeParse.stringToInt(binding.tuneDays.text)
autotunePlugin.calculationRunning = true
autotunePlugin.lastNbDays = daysBack.toString()
log("Run Autotune $profileName, $daysBack days")
Thread {
autotunePlugin.aapsAutotune(daysBack, false, profileName)
}.start()
updateGui()
}
binding.profileList.onItemClickListener = AdapterView.OnItemClickListener { _, _, _, _ ->
if (!autotunePlugin.calculationRunning)
{
profileName = if (binding.profileList.text.toString() == rh.gs(R.string.active)) "" else binding.profileList.text.toString()
profileFunction.getProfile()?.let { currentProfile ->
profile = ATProfile(profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(it) } ?:currentProfile, LocalInsulin(""), injector)
}
autotunePlugin.selectedProfile = profileName
resetParam()
}
updateGui()
}
binding.autotuneCopylocal.setOnClickListener {
val localName = rh.gs(R.string.autotune_tunedprofile_name) + " " + dateUtil.dateAndTimeString(autotunePlugin.lastRun)
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
autotunePlugin.tunedProfile?.let { tunedProfile ->
showConfirmation(requireContext(),
rh.gs(R.string.autotune_copy_localprofile_button),
rh.gs(R.string.autotune_copy_local_profile_message) + "\n" + localName + " " + dateUtil.dateAndTimeString(autotunePlugin.lastRun),
Runnable {
localProfilePlugin.addProfile(localProfilePlugin.copyFrom(tunedProfile.getProfile(circadian), localName))
rxBus.send(EventLocalProfileChanged())
uel.log(
UserEntry.Action.NEW_PROFILE,
UserEntry.Sources.Autotune,
ValueWithUnit.SimpleString(localName)
)
updateGui()
})
}
}
binding.autotuneUpdateProfile.setOnClickListener {
val localName = autotunePlugin.pumpProfile.profilename
showConfirmation(requireContext(),
rh.gs(R.string.autotune_update_input_profile_button),
rh.gs(R.string.autotune_update_local_profile_message, localName),
Runnable {
autotunePlugin.tunedProfile?.profilename = localName
autotunePlugin.updateProfile(autotunePlugin.tunedProfile)
autotunePlugin.updateButtonVisibility = View.GONE
uel.log(
UserEntry.Action.STORE_PROFILE,
UserEntry.Sources.Autotune,
ValueWithUnit.SimpleString(localName)
)
updateGui()
}
)
}
binding.autotuneRevertProfile.setOnClickListener {
val localName = autotunePlugin.pumpProfile.profilename
showConfirmation(requireContext(),
rh.gs(R.string.autotune_revert_input_profile_button),
rh.gs(R.string.autotune_revert_local_profile_message, localName),
Runnable {
autotunePlugin.tunedProfile?.profilename = ""
autotunePlugin.updateProfile(autotunePlugin.pumpProfile)
autotunePlugin.updateButtonVisibility = View.VISIBLE
uel.log(
UserEntry.Action.STORE_PROFILE,
UserEntry.Sources.Autotune,
ValueWithUnit.SimpleString(localName)
)
updateGui()
}
)
}
binding.autotuneCheckInputProfile.setOnClickListener {
val pumpProfile = profileFunction.getProfile()?.let { currentProfile ->
profileStore.getSpecificProfile(profileName)?.let { specificProfile ->
ATProfile(ProfileSealed.Pure(specificProfile), LocalInsulin(""), injector).also {
it.profilename = profileName
}
}
?: ATProfile(currentProfile, LocalInsulin(""), injector).also {
it.profilename = profileFunction.getProfileName()
}
}
pumpProfile?.let {
ProfileViewerDialog().also { pvd ->
pvd.arguments = Bundle().also {
it.putLong("time", dateUtil.now())
it.putInt("mode", ProfileViewerDialog.Mode.CUSTOM_PROFILE.ordinal)
it.putString("customProfile", pumpProfile.profile.toPureNsJson(dateUtil).toString())
it.putString("customProfileUnits", profileFunction.getUnits().asText)
it.putString("customProfileName", pumpProfile.profilename)
}
}.show(childFragmentManager, "ProfileViewDialog")
}
}
binding.autotuneCompare.setOnClickListener {
val pumpProfile = autotunePlugin.pumpProfile
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
val tunedprofile = if (circadian) autotunePlugin.tunedProfile?.circadianProfile else autotunePlugin.tunedProfile?.profile
ProfileViewerDialog().also { pvd ->
pvd.arguments = Bundle().also {
it.putLong("time", dateUtil.now())
it.putInt("mode", ProfileViewerDialog.Mode.PROFILE_COMPARE.ordinal)
it.putString("customProfile", pumpProfile.profile.toPureNsJson(dateUtil).toString())
it.putString("customProfile2", tunedprofile?.toPureNsJson(dateUtil).toString())
it.putString("customProfileUnits", profileFunction.getUnits().asText)
it.putString("customProfileName", pumpProfile.profilename + "\n" + rh.gs(R.string.autotune_tunedprofile_name))
}
}.show(childFragmentManager, "ProfileViewDialog")
}
binding.autotuneProfileswitch.setOnClickListener {
val tunedProfile = autotunePlugin.tunedProfile
autotunePlugin.updateProfile(tunedProfile)
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
tunedProfile?.let { tunedP ->
tunedP.profileStore(circadian)?.let {
showConfirmation(requireContext(),
rh.gs(R.string.activate_profile) + ": " + tunedP.profilename + " ?",
Runnable {
uel.log(
UserEntry.Action.STORE_PROFILE,
UserEntry.Sources.Autotune,
ValueWithUnit.SimpleString(tunedP.profilename)
)
val now = dateUtil.now()
if (profileFunction.createProfileSwitch(
it,
profileName = tunedP.profilename,
durationInMinutes = 0,
percentage = 100,
timeShiftInHours = 0,
timestamp = now
)
) {
uel.log(
UserEntry.Action.PROFILE_SWITCH,
UserEntry.Sources.Autotune,
"Autotune AutoSwitch",
ValueWithUnit.SimpleString(autotunePlugin.tunedProfile!!.profilename)
)
}
rxBus.send(EventLocalProfileChanged())
updateGui()
}
)
}
}
}
}
@Synchronized
override fun onResume() {
super.onResume()
disposable += rxBus
.toObservable(EventAutotuneUpdateGui::class.java)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
updateGui()
}, { fabricPrivacy.logException(it) })
checkNewDay()
binding.tuneDays.value = autotunePlugin.lastNbDays.toDouble()
updateGui()
}
@Synchronized
override fun onPause() {
super.onPause()
disposable.clear()
}
@Synchronized
private fun updateGui() {
_binding ?: return
profileStore = activePlugin.activeProfileSource.profile ?: ProfileStore(injector, JSONObject(), dateUtil)
profileName = if (binding.profileList.text.toString() == rh.gs(R.string.active)) "" else binding.profileList.text.toString()
profileFunction.getProfile()?.let { currentProfile ->
profile = ATProfile(profileStore.getSpecificProfile(profileName)?.let { ProfileSealed.Pure(it) } ?:currentProfile, LocalInsulin(""), injector)
}
val profileList: ArrayList<CharSequence> = profileStore.getProfileList()
profileList.add(0, rh.gs(R.string.active))
context?.let { context ->
binding.profileList.setAdapter(ArrayAdapter(context, R.layout.spinner_centered, profileList))
} ?: return
// set selected to actual profile
if (autotunePlugin.selectedProfile.isNotEmpty())
binding.profileList.setText(autotunePlugin.selectedProfile, false)
else {
binding.profileList.setText(profileList[0], false)
}
binding.autotuneRun.visibility = View.GONE
binding.autotuneCheckInputProfile.visibility = View.GONE
binding.autotuneCopylocal.visibility = View.GONE
binding.autotuneUpdateProfile.visibility = View.GONE
binding.autotuneRevertProfile.visibility = View.GONE
binding.autotuneProfileswitch.visibility = View.GONE
binding.autotuneCompare.visibility = View.GONE
when {
autotunePlugin.calculationRunning -> {
binding.tuneWarning.text = rh.gs(R.string.autotune_warning_during_run)
}
autotunePlugin.lastRunSuccess -> {
binding.autotuneCopylocal.visibility = View.VISIBLE
binding.autotuneUpdateProfile.visibility = autotunePlugin.updateButtonVisibility
binding.autotuneRevertProfile.visibility = if (autotunePlugin.updateButtonVisibility == View.VISIBLE) View.GONE else View.VISIBLE
binding.autotuneProfileswitch.visibility = View.VISIBLE
binding.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run)
binding.autotuneCompare.visibility = View.VISIBLE
}
else -> {
binding.autotuneRun.visibility = View.VISIBLE
binding.autotuneCheckInputProfile.visibility = View.VISIBLE
}
}
binding.tuneLastrun.text = dateUtil.dateAndTimeString(autotunePlugin.lastRun)
showResults()
}
private fun checkNewDay() {
val runToday = autotunePlugin.lastRun > MidnightTime.calc(dateUtil.now() - autotunePlugin.autotuneStartHour * 3600 * 1000L) + autotunePlugin.autotuneStartHour * 3600 * 1000L
if (runToday && autotunePlugin.result != "")
{
binding.tuneWarning.text = rh.gs(R.string.autotune_warning_after_run)
} else if (!runToday || autotunePlugin.result.isEmpty()) { //if new day reinit result, default days, warning and button's visibility
resetParam(!runToday)
}
}
private fun addWarnings(): String {
var warning = ""
var nl = ""
if (profileFunction.getProfile() == null) {
warning = rh.gs(R.string.profileswitch_ismissing)
return warning
}
profileFunction.getProfile()?.let {
if (!profile.isValid) return rh.gs(R.string.autotune_profile_invalid)
if (profile.icSize > 1) {
warning += nl + rh.gs(R.string.autotune_ic_warning, profile.icSize, profile.ic)
nl = "\n"
}
if (profile.isfSize > 1) {
warning += nl + rh.gs(R.string.autotune_isf_warning, profile.isfSize, Profile.fromMgdlToUnits(profile.isf, profileFunction.getUnits()), profileFunction.getUnits().asText)
}
}
return warning
}
private fun resetParam(resetDay: Boolean = true) {
binding.tuneWarning.text = addWarnings()
if (resetDay)
autotunePlugin.lastNbDays = sp.getInt(R.string.key_autotune_default_tune_days, 5).toString()
autotunePlugin.result = ""
binding.autotuneResults.removeAllViews()
autotunePlugin.tunedProfile = null
autotunePlugin.lastRunSuccess = false
autotunePlugin.updateButtonVisibility = View.GONE
}
private val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable) { updateGui() }
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (binding.tuneDays.text.isNotEmpty()) {
try {
if (autotunePlugin.calculationRunning)
binding.tuneDays.value = autotunePlugin.lastNbDays.toDouble()
if (binding.tuneDays.value != autotunePlugin.lastNbDays.toDouble()) {
autotunePlugin.lastNbDays = binding.tuneDays.text
resetParam(false)
}
} catch (e:Exception) { }
}
}
}
private fun showResults() {
Thread {
runOnUiThread {
binding.autotuneResults.removeAllViews()
if (autotunePlugin.result.isNotBlank()) {
var toMgDl = 1.0
if (profileFunction.getUnits() == GlucoseUnit.MMOL) toMgDl = Constants.MMOLL_TO_MGDL
var isf_Format = if (profileFunction.getUnits() == GlucoseUnit.MMOL) "%.2f" else "%.1f"
binding.autotuneResults.addView(
TableLayout(context).also { layout ->
layout.addView(
TextView(context).apply {
text = autotunePlugin.result
setTypeface(typeface, Typeface.BOLD)
gravity = Gravity.CENTER_HORIZONTAL
setTextAppearance(android.R.style.TextAppearance_Material_Medium)
})
autotunePlugin.tunedProfile?.let { tuned ->
layout.addView(toTableRowHeader())
val tuneInsulin = sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false)
if (tuneInsulin) {
layout.addView(toTableRowValue(rh.gs(R.string.insulin_peak), autotunePlugin.pumpProfile.localInsulin.peak.toDouble(), tuned.localInsulin.peak.toDouble(), "%.0f"))
layout.addView(toTableRowValue(rh.gs(R.string.dia), Round.roundTo(autotunePlugin.pumpProfile.localInsulin.dia, 0.1), Round.roundTo(tuned.localInsulin.dia, 0.1),"%.1f"))
}
layout.addView(toTableRowValue(rh.gs(R.string.isf_short), Round.roundTo(autotunePlugin.pumpProfile.isf / toMgDl, 0.001), Round.roundTo(tuned.isf / toMgDl, 0.001), isf_Format))
layout.addView(toTableRowValue(rh.gs(R.string.ic_short), Round.roundTo(autotunePlugin.pumpProfile.ic, 0.001), Round.roundTo(tuned.ic, 0.001), "%.2f"))
layout.addView(
TextView(context).apply {
text = rh.gs(R.string.basal)
setTypeface(typeface, Typeface.BOLD)
gravity = Gravity.CENTER_HORIZONTAL
setTextAppearance(android.R.style.TextAppearance_Material_Medium)
}
)
layout.addView(toTableRowHeader(true))
var totalPump = 0.0
var totalTuned = 0.0
for (h in 0 until tuned.basal.size) {
val df = DecimalFormat("00")
val time = df.format(h.toLong()) + ":00"
totalPump += autotunePlugin.pumpProfile.basal[h]
totalTuned += tuned.basal[h]
layout.addView(toTableRowValue(time, autotunePlugin.pumpProfile.basal[h], tuned.basal[h], "%.3f", tuned.basalUntuned[h].toString()))
}
layout.addView(toTableRowValue("", totalPump, totalTuned, " "))
}
}
)
}
binding.autotuneResultsCard.visibility = if (autotunePlugin.calculationRunning && autotunePlugin.result.isEmpty()) View.GONE else View.VISIBLE
}
}.start()
}
private fun toTableRowHeader(basal:Boolean = false): TableRow =
TableRow(context).also { header ->
val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT).apply { weight = 1f }
header.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL }
header.addView(TextView(context).apply {
layoutParams = lp.apply { column = 0 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = if (basal) rh.gs(R.string.time) else rh.gs(R.string.autotune_param)
})
header.addView(TextView(context).apply {
layoutParams = lp.apply { column = 1 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = rh.gs(R.string.profile)
})
header.addView(TextView(context).apply {
layoutParams = lp.apply { column = 2 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = rh.gs(R.string.autotune_tunedprofile_name)
})
header.addView(TextView(context).apply {
layoutParams = lp.apply { column = 3 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = rh.gs(R.string.autotune_percent)
})
header.addView(TextView(context).apply {
layoutParams = lp.apply { column = 4 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = if (basal) rh.gs(R.string.autotune_missing) else " "
})
}
private fun toTableRowValue(hour: String, inputValue: Double, tunedValue: Double, format:String = "%.3f", missing: String = ""): TableRow =
TableRow(context).also { row ->
val percentValue = Round.roundTo(tunedValue / inputValue * 100 - 100, 1.0).toInt().toString() + "%"
val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT).apply { weight = 1f }
row.layoutParams = TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL }
row.addView(TextView(context).apply {
layoutParams = lp.apply { column = 0 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = hour
})
row.addView(TextView(context).apply {
layoutParams = lp.apply { column = 1 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = String.format(format, inputValue)
})
row.addView(TextView(context).apply {
layoutParams = lp.apply { column = 2 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = String.format(format, tunedValue)
})
row.addView(TextView(context).apply {
layoutParams = lp.apply { column = 3 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = percentValue
})
row.addView(TextView(context).apply {
layoutParams = lp.apply { column = 4 }
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
text = missing
})
}
private fun log(message: String) {
autotuneFS.atLog("[Fragment] $message")
}
}

View file

@ -0,0 +1,386 @@
package info.nightscout.androidaps.plugins.general.autotune
import info.nightscout.androidaps.Constants
import info.nightscout.androidaps.R
import info.nightscout.androidaps.data.*
import info.nightscout.androidaps.database.AppRepository
import info.nightscout.androidaps.database.embedments.InterfaceIDs
import info.nightscout.androidaps.interfaces.ActivePlugin
import info.nightscout.androidaps.interfaces.ProfileFunction
import info.nightscout.androidaps.database.entities.*
import info.nightscout.androidaps.extensions.durationInMinutes
import info.nightscout.androidaps.extensions.iobCalc
import info.nightscout.androidaps.extensions.toJson
import info.nightscout.androidaps.extensions.toTemporaryBasal
import info.nightscout.androidaps.interfaces.Profile
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.Round
import info.nightscout.androidaps.utils.T
import info.nightscout.shared.logging.AAPSLogger
import info.nightscout.shared.sharedPreferences.SP
import org.json.JSONArray
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.roundToInt
@Singleton
class AutotuneIob @Inject constructor(
private val aapsLogger: AAPSLogger,
private val repository: AppRepository,
private val profileFunction: ProfileFunction,
private val sp: SP,
private val dateUtil: DateUtil,
private val activePlugin: ActivePlugin,
private val autotuneFS: AutotuneFS
) {
private val nsTreatments = ArrayList<NsTreatment>()
private var dia: Double = Constants.defaultDIA
var boluses: MutableList<Bolus> = ArrayList()
var meals = ArrayList<Carbs>()
lateinit var glucose: List<GlucoseValue> // newest at index 0
private lateinit var tempBasals: MutableList<TemporaryBasal>
var startBG: Long = 0
var endBG: Long = 0
private fun range(): Long = (60 * 60 * 1000L * dia + T.hours(2).msecs()).toLong()
fun initializeData(from: Long, to: Long, tunedProfile: ATProfile) {
dia = tunedProfile.dia
startBG = from
endBG = to
nsTreatments.clear()
tempBasals = ArrayList<TemporaryBasal>()
initializeBgreadings(from, to)
initializeTreatmentData(from - range(), to)
initializeTempBasalData(from - range(), to, tunedProfile)
initializeExtendedBolusData(from - range(), to, tunedProfile)
Collections.sort(tempBasals) { o1: TemporaryBasal, o2: TemporaryBasal -> (o2.timestamp - o1.timestamp).toInt() }
// Without Neutral TBR, Autotune Web will ignore iob for periods without TBR running
addNeutralTempBasal(from - range(), to, tunedProfile)
Collections.sort(nsTreatments) { o1: NsTreatment, o2: NsTreatment -> (o2.date - o1.date).toInt() }
Collections.sort(boluses) { o1: Bolus, o2: Bolus -> (o2.timestamp - o1.timestamp).toInt() }
log.debug("D/AutotunePlugin: Nb Treatments: " + nsTreatments.size + " Nb meals: " + meals.size)
}
private fun initializeBgreadings(from: Long, to: Long) {
glucose = repository.compatGetBgReadingsDataFromTime(from, to, false).blockingGet();
}
//nsTreatment is used only for export data, meals is used in AutotunePrep
private fun initializeTreatmentData(from: Long, to: Long) {
val oldestBgDate = if (glucose.size > 0) glucose[glucose.size - 1].timestamp else from
log.debug("AutotunePlugin Check BG date: BG Size: " + glucose.size + " OldestBG: " + dateUtil.dateAndTimeAndSecondsString(oldestBgDate) + " to: " + dateUtil.dateAndTimeAndSecondsString(to))
val tmpCarbs = repository.getCarbsDataFromTimeToTimeExpanded(from, to, false).blockingGet()
log.debug("AutotunePlugin Nb treatments after query: " + tmpCarbs.size)
meals.clear()
boluses.clear()
var nbCarbs = 0
for (i in tmpCarbs.indices) {
val tp = tmpCarbs[i]
if (tp.isValid) {
nsTreatments.add(NsTreatment(tp))
//only carbs after first BGReadings are taken into account in calculation of Autotune
if (tp.amount > 0.0 && tp.timestamp >= oldestBgDate) meals.add(tmpCarbs[i])
if (tp.timestamp < to && tp.amount > 0.0)
nbCarbs++
}
}
val tmpBolus = repository.getBolusesDataFromTimeToTime(from, to, false).blockingGet()
var nbSMB = 0
var nbBolus = 0
for (i in tmpBolus.indices) {
val tp = tmpBolus[i]
if (tp.isValid && tp.type != Bolus.Type.PRIMING) {
boluses.add(tp)
nsTreatments.add(NsTreatment(tp))
//only carbs after first BGReadings are taken into account in calculation of Autotune
if (tp.timestamp < to) {
if (tp.type == Bolus.Type.SMB)
nbSMB++
else if (tp.amount > 0.0)
nbBolus++
}
}
}
//log.debug("AutotunePlugin Nb Meals: $nbCarbs Nb Bolus: $nbBolus Nb SMB: $nbSMB")
}
//nsTreatment is used only for export data
private fun initializeTempBasalData(from: Long, to: Long, tunedProfile: ATProfile) {
val tBRs = repository.getTemporaryBasalsDataFromTimeToTime(from, to, false).blockingGet()
//log.debug("D/AutotunePlugin tempBasal size before cleaning:" + tBRs.size);
for (i in tBRs.indices) {
if (tBRs[i].isValid)
toSplittedTimestampTB(tBRs[i], tunedProfile)
}
//log.debug("D/AutotunePlugin: tempBasal size: " + tempBasals.size)
}
//nsTreatment is used only for export data
private fun initializeExtendedBolusData(from: Long, to: Long, tunedProfile: ATProfile) {
val extendedBoluses = repository.getExtendedBolusDataFromTimeToTime(from, to, false).blockingGet()
val pumpInterface = activePlugin.activePump
if (pumpInterface.isFakingTempsByExtendedBoluses) {
for (i in extendedBoluses.indices) {
val eb = extendedBoluses[i]
if (eb.isValid)
profileFunction.getProfile(eb.timestamp)?.let {
toSplittedTimestampTB(eb.toTemporaryBasal(it), tunedProfile)
}
}
} else {
for (i in extendedBoluses.indices) {
val eb = extendedBoluses[i]
if (eb.isValid) {
nsTreatments.add(NsTreatment(eb))
boluses.addAll(convertToBoluses(eb))
}
}
}
}
// addNeutralTempBasal will add a fake neutral TBR (100%) to have correct basal rate in exported file for periods without TBR running
// to be able to compare results between oref0 algo and aaps
private fun addNeutralTempBasal(from: Long, to: Long, tunedProfile: ATProfile) {
var previousStart = to
for (i in tempBasals.indices) {
val newStart = tempBasals[i].timestamp + tempBasals[i].duration
if (previousStart - newStart > T.mins(1).msecs()) { // fill neutral only if more than 1 min
val neutralTbr = TemporaryBasal(
isValid = true,
isAbsolute = false,
timestamp = newStart,
rate = 100.0,
duration = previousStart - newStart,
interfaceIDs_backing = InterfaceIDs(nightscoutId = "neutral_" + newStart.toString()),
type = TemporaryBasal.Type.NORMAL
)
toSplittedTimestampTB(neutralTbr, tunedProfile)
}
previousStart = tempBasals[i].timestamp
}
if (previousStart - from > T.mins(1).msecs()) { // fill neutral only if more than 1 min
val neutralTbr = TemporaryBasal(
isValid = true,
isAbsolute = false,
timestamp = from,
rate = 100.0,
duration = previousStart - from,
interfaceIDs_backing = InterfaceIDs(nightscoutId = "neutral_" + from.toString()),
type = TemporaryBasal.Type.NORMAL
)
toSplittedTimestampTB(neutralTbr, tunedProfile)
}
}
// toSplittedTimestampTB will split all TBR across hours in different TBR with correct absolute value to be sure to have correct basal rate
// even if profile rate is not the same
private fun toSplittedTimestampTB(tb: TemporaryBasal, tunedProfile: ATProfile) {
var splittedTimestamp = tb.timestamp
val cutInMilliSec = T.mins(30).msecs() //30 min to compare with oref0
var splittedDuration = tb.duration
if (tb.isValid && tb.durationInMinutes > 0) {
val endTimestamp = splittedTimestamp + splittedDuration
while (splittedDuration > 0) {
if (Profile.milliSecFromMidnight(splittedTimestamp) / cutInMilliSec == Profile.milliSecFromMidnight(endTimestamp) / cutInMilliSec) {
val newtb = TemporaryBasal(
isValid = true,
isAbsolute = tb.isAbsolute,
timestamp = splittedTimestamp,
rate = tb.rate,
duration = splittedDuration,
interfaceIDs_backing = tb.interfaceIDs_backing,
type = tb.type
)
tempBasals.add(newtb)
nsTreatments.add(NsTreatment(newtb))
splittedDuration = 0
val profile = profileFunction.getProfile(newtb.timestamp) ?:continue
boluses.addAll(convertToBoluses(newtb, profile, tunedProfile.profile)) //
// required for correct iob calculation with oref0 algo
} else {
val durationFilled = (cutInMilliSec - Profile.milliSecFromMidnight(splittedTimestamp) % cutInMilliSec)
val newtb = TemporaryBasal(
isValid = true,
isAbsolute = tb.isAbsolute,
timestamp = splittedTimestamp,
rate = tb.rate,
duration = durationFilled,
interfaceIDs_backing = tb.interfaceIDs_backing,
type = tb.type
)
tempBasals.add(newtb)
nsTreatments.add(NsTreatment(newtb))
splittedTimestamp += durationFilled
splittedDuration = splittedDuration - durationFilled
val profile = profileFunction.getProfile(newtb.timestamp) ?:continue
boluses.addAll(convertToBoluses(newtb, profile, tunedProfile.profile)) // required for correct iob calculation with oref0 algo
}
}
}
}
fun getIOB(time: Long, localInsulin: LocalInsulin): IobTotal {
val bolusIob = getCalculationToTimeTreatments(time, localInsulin).round()
return bolusIob
}
fun getCalculationToTimeTreatments(time: Long, localInsulin: LocalInsulin): IobTotal {
val total = IobTotal(time)
val detailedLog = sp.getBoolean(R.string.key_autotune_additional_log, false)
for (pos in boluses.indices) {
val t = boluses[pos]
if (!t.isValid) continue
if (t.timestamp > time || t.timestamp < time - localInsulin.duration) continue
val tIOB = t.iobCalc(time, localInsulin)
if (detailedLog)
log("iobCalc;${t.interfaceIDs.nightscoutId};$time;${t.timestamp};${tIOB.iobContrib};${tIOB.activityContrib};${dateUtil.dateAndTimeAndSecondsString(time)};${dateUtil.dateAndTimeAndSecondsString(t.timestamp)}")
total.iob += tIOB.iobContrib
total.activity += tIOB.activityContrib
}
return total
}
fun convertToBoluses(eb: ExtendedBolus): MutableList<Bolus> {
val result: MutableList<Bolus> = ArrayList()
val tempBolusSize = 0.05
val tempBolusCount : Int = (eb.amount / tempBolusSize).roundToInt()
if(tempBolusCount > 0) {
val tempBolusSpacing = eb.duration / tempBolusCount
for (j in 0L until tempBolusCount) {
val calcDate = eb.timestamp + j * tempBolusSpacing
val bolusInterfaceIDs = InterfaceIDs().also { it.nightscoutId = eb.interfaceIDs.nightscoutId + "_eb_$j" }
val tempBolusPart = Bolus(
interfaceIDs_backing = bolusInterfaceIDs,
timestamp = calcDate,
amount = tempBolusSize,
type = Bolus.Type.NORMAL
)
result.add(tempBolusPart)
}
}
return result
}
fun convertToBoluses(tbr: TemporaryBasal, profile: Profile, tunedProfile: Profile): MutableList<Bolus> {
val result: MutableList<Bolus> = ArrayList()
val realDuration = tbr.durationInMinutes
val basalRate = profile.getBasal(tbr.timestamp)
val tunedRate = tunedProfile.getBasal(tbr.timestamp)
val netBasalRate = Round.roundTo(if (tbr.isAbsolute) {
tbr.rate - tunedRate
} else {
tbr.rate / 100.0 * basalRate - tunedRate
}, 0.001)
val tempBolusSize = if (netBasalRate < 0 ) -0.05 else 0.05
val netBasalAmount: Double = Round.roundTo(netBasalRate * realDuration / 60.0, 0.01)
val tempBolusCount : Int = (netBasalAmount / tempBolusSize).roundToInt()
if(tempBolusCount > 0) {
val tempBolusSpacing = realDuration * 60 * 1000 / tempBolusCount
for (j in 0L until tempBolusCount) {
val calcDate = tbr.timestamp + j * tempBolusSpacing
val bolusInterfaceIDs = InterfaceIDs().also { it.nightscoutId = tbr.interfaceIDs.nightscoutId + "_tbr_$j" }
val tempBolusPart = Bolus(
interfaceIDs_backing = bolusInterfaceIDs,
timestamp = calcDate,
amount = tempBolusSize,
type = Bolus.Type.NORMAL
)
result.add(tempBolusPart)
}
}
return result
}
fun glucoseToJSON(): String {
val glucoseJson = JSONArray()
for (bgreading in glucose)
glucoseJson.put(bgreading.toJson(true, dateUtil))
return glucoseJson.toString(2)
}
fun bolusesToJSON(): String {
val bolusesJson = JSONArray()
for (bolus in boluses)
bolusesJson.put(bolus.toJson(true, dateUtil))
return bolusesJson.toString(2)
}
fun nsHistoryToJSON(): String {
val json = JSONArray()
for (t in nsTreatments) {
json.put(t.toJson())
}
return json.toString(2).replace("\\/", "/")
}
//I add this internal class to be able to export easily ns-treatment files with same contain and format than NS query used by oref0-autotune
private inner class NsTreatment {
var date: Long = 0
var eventType: TherapyEvent.Type? = null
var carbsTreatment: Carbs? = null
var bolusTreatment: Bolus? = null
var temporaryBasal: TemporaryBasal? = null
var extendedBolus: ExtendedBolus? = null
constructor(t: Carbs) {
carbsTreatment = t
date = t.timestamp
eventType = TherapyEvent.Type.CARBS_CORRECTION
}
constructor(t: Bolus) {
bolusTreatment = t
date = t.timestamp
eventType = TherapyEvent.Type.CORRECTION_BOLUS
}
constructor(t: TemporaryBasal) {
temporaryBasal = t
date = t.timestamp
eventType = TherapyEvent.Type.TEMPORARY_BASAL
}
constructor(t: ExtendedBolus) {
extendedBolus = t
date = t.timestamp
eventType = TherapyEvent.Type.COMBO_BOLUS
}
fun toJson(): JSONObject? {
val cPjson = JSONObject()
return when (eventType) {
TherapyEvent.Type.TEMPORARY_BASAL ->
temporaryBasal?.let { tbr ->
val profile = profileFunction.getProfile(tbr.timestamp)
profile?.let {
tbr.toJson(true, it, dateUtil)
}
}
TherapyEvent.Type.COMBO_BOLUS ->
extendedBolus?.let {
val profile = profileFunction.getProfile(it.timestamp)
it.toJson(true, profile!!, dateUtil)
}
TherapyEvent.Type.CORRECTION_BOLUS -> bolusTreatment?.toJson(true, dateUtil)
TherapyEvent.Type.CARBS_CORRECTION -> carbsTreatment?.toJson(true, dateUtil)
else -> cPjson
}
}
}
private fun log(message: String) {
autotuneFS.atLog("[iob] $message")
}
companion object {
private val log = LoggerFactory.getLogger(AutotunePlugin::class.java)
}
}

View file

@ -0,0 +1,309 @@
package info.nightscout.androidaps.plugins.general.autotune
import android.view.View
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.R
import info.nightscout.androidaps.data.LocalInsulin
import info.nightscout.androidaps.data.ProfileSealed
import info.nightscout.androidaps.database.entities.UserEntry
import info.nightscout.androidaps.database.entities.ValueWithUnit
import info.nightscout.androidaps.interfaces.*
import info.nightscout.androidaps.logging.UserEntryLogger
import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.plugins.general.autotune.data.ATProfile
import info.nightscout.androidaps.plugins.general.autotune.data.PreppedGlucose
import info.nightscout.androidaps.plugins.general.autotune.events.EventAutotuneUpdateGui
import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin
import info.nightscout.androidaps.plugins.profile.local.events.EventLocalProfileChanged
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.MidnightTime
import info.nightscout.androidaps.utils.T
import info.nightscout.androidaps.utils.buildHelper.BuildHelper
import info.nightscout.shared.logging.AAPSLogger
import info.nightscout.shared.sharedPreferences.SP
import org.json.JSONException
import org.json.JSONObject
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
/**
* adaptation from oref0 autotune started by philoul on 2020 (complete refactoring of AutotunePlugin initialised by Rumen Georgiev on 1/29/2018.)
*
* TODO: replace Thread by Worker
* TODO: future version: Allow day of the week selection to tune specifics days (training days, working days, WE days)
*/
@Singleton
class AutotunePlugin @Inject constructor(
injector: HasAndroidInjector,
resourceHelper: ResourceHelper,
private val sp: SP,
private val rxBus: RxBus,
private val profileFunction: ProfileFunction,
private val dateUtil: DateUtil,
private val activePlugin: ActivePlugin,
private val localProfilePlugin: LocalProfilePlugin,
private val autotuneFS: AutotuneFS,
private val autotuneIob: AutotuneIob,
private val autotunePrep: AutotunePrep,
private val autotuneCore: AutotuneCore,
private val buildHelper:BuildHelper,
private val uel: UserEntryLogger,
aapsLogger: AAPSLogger
) : PluginBase(PluginDescription()
.mainType(PluginType.GENERAL)
.fragmentClass(AutotuneFragment::class.qualifiedName)
.pluginIcon(R.drawable.ic_autotune)
.pluginName(R.string.autotune)
.shortName(R.string.autotune_shortname)
.preferencesId(R.xml.pref_autotune)
.description(R.string.autotune_description),
aapsLogger, resourceHelper, injector
), Autotune {
@Volatile override var lastRunSuccess: Boolean = false
@Volatile var result: String = ""
@Volatile var calculationRunning: Boolean = false
@Volatile var lastRun: Long = 0
@Volatile var selectedProfile = ""
@Volatile var lastNbDays: String = ""
@Volatile var updateButtonVisibility: Int = 0
@Volatile lateinit var pumpProfile: ATProfile
@Volatile var tunedProfile: ATProfile? = null
private var preppedGlucose: PreppedGlucose? = null
private lateinit var profile: Profile
val autotuneStartHour: Int = 4
override fun aapsAutotune(daysBack: Int, autoSwitch: Boolean, profileToTune: String): String {
tunedProfile = null
updateButtonVisibility = View.GONE
lastRunSuccess = false
var logResult = ""
result = ""
if (profileFunction.getProfile() == null) {
result = rh.gs(R.string.profileswitch_ismissing)
return result
}
val detailedLog = sp.getBoolean(R.string.key_autotune_additional_log, false)
calculationRunning = true
lastNbDays = "" + daysBack
lastRun = dateUtil.now()
val profileStore = activePlugin.activeProfileSource.profile ?: return rh.gs(R.string.profileswitch_ismissing)
selectedProfile = if (profileToTune.isEmpty()) profileFunction.getProfileName() else profileToTune
profileFunction.getProfile()?.let { currentProfile ->
profile = profileStore.getSpecificProfile(profileToTune)?.let { ProfileSealed.Pure(it) } ?: currentProfile
}
val localInsulin = LocalInsulin("PumpInsulin", activePlugin.activeInsulin.peak, profile.dia) // var because localInsulin could be updated later with Tune Insulin peak/dia
log("Start Autotune with $daysBack days back")
autotuneFS.createAutotuneFolder() //create autotune subfolder for autotune files if not exists
autotuneFS.deleteAutotuneFiles() //clean autotune folder before run
// Today at 4 AM
var endTime = MidnightTime.calc(lastRun) + autotuneStartHour * 60 * 60 * 1000L
if (endTime > lastRun) endTime -= 24 * 60 * 60 * 1000L // Check if 4 AM is before now
val starttime = endTime - daysBack * 24 * 60 * 60 * 1000L
autotuneFS.exportSettings(settings(lastRun, daysBack, starttime, endTime))
tunedProfile = ATProfile(profile, localInsulin, injector).also {
it.profilename = rh.gs(R.string.autotune_tunedprofile_name)
}
pumpProfile = ATProfile(profile, localInsulin, injector).also {
it.profilename = selectedProfile
}
autotuneFS.exportPumpProfile(pumpProfile)
for (i in 0 until daysBack) {
val from = starttime + i * 24 * 60 * 60 * 1000L // get 24 hours BG values from 4 AM to 4 AM next day
val to = from + 24 * 60 * 60 * 1000L
log("Tune day " + (i + 1) + " of " + daysBack)
tunedProfile?.let { tunedProfile ->
autotuneIob.initializeData(from, to, tunedProfile) //autotuneIob contains BG and Treatments data from history (<=> query for ns-treatments and ns-entries)
autotuneFS.exportEntries(autotuneIob) //<=> ns-entries.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine
autotuneFS.exportTreatments(autotuneIob) //<=> ns-treatments.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine (include treatments ,tempBasal and extended
preppedGlucose = autotunePrep.categorize(tunedProfile) //<=> autotune.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine
}
if (preppedGlucose == null || tunedProfile == null) {
result = rh.gs(R.string.autotune_error)
log(result)
calculationRunning = false
rxBus.send(EventAutotuneUpdateGui())
tunedProfile = null
autotuneFS.exportResult(result)
autotuneFS.exportLogAndZip(lastRun)
return result
}
preppedGlucose?.let { preppedGlucose -> //preppedGlucose and tunedProfile should never be null here
autotuneFS.exportPreppedGlucose(preppedGlucose)
tunedProfile = autotuneCore.tuneAllTheThings(preppedGlucose, tunedProfile!!, pumpProfile)
}
// localInsulin = LocalInsulin("TunedInsulin", tunedProfile!!.peak, tunedProfile!!.dia) // Todo: Add tune Insulin option
autotuneFS.exportTunedProfile(tunedProfile!!) //<=> newprofile.yyyymmdd.json files exported for results compare with oref0 autotune on virtual machine
if (i < daysBack - 1) {
log("Partial result for day ${i + 1}".trimIndent())
result = rh.gs(R.string.autotune_partial_result, i + 1, daysBack)
rxBus.send(EventAutotuneUpdateGui())
}
logResult = showResults(tunedProfile, pumpProfile)
if (detailedLog)
autotuneFS.exportLog(lastRun, i + 1)
}
result = rh.gs(R.string.autotune_result, dateUtil.dateAndTimeString(lastRun))
if (!detailedLog)
autotuneFS.exportLog(lastRun)
autotuneFS.exportResult(logResult)
autotuneFS.zipAutotune(lastRun)
updateButtonVisibility = View.VISIBLE
if (autoSwitch) {
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
tunedProfile?.let { tunedP ->
tunedP.profilename = pumpProfile.profilename
updateProfile(tunedP)
uel.log(
UserEntry.Action.STORE_PROFILE,
UserEntry.Sources.Autotune,
ValueWithUnit.SimpleString(tunedP.profilename)
)
updateButtonVisibility = View.GONE
tunedP.profileStore(circadian)?.let { profilestore ->
if (profileFunction.createProfileSwitch(
profilestore,
profileName = tunedP.profilename,
durationInMinutes = 0,
percentage = 100,
timeShiftInHours = 0,
timestamp = dateUtil.now()
)
) {
log("Profile Switch succeed ${tunedP.profilename}")
uel.log(
UserEntry.Action.PROFILE_SWITCH,
UserEntry.Sources.Autotune,
"Autotune AutoSwitch",
ValueWithUnit.SimpleString(tunedP.profilename))
}
rxBus.send(EventLocalProfileChanged())
}
}
}
lastRunSuccess = true
sp.putLong(R.string.key_autotune_last_run, lastRun)
rxBus.send(EventAutotuneUpdateGui())
calculationRunning = false
tunedProfile?.let {
return result
}
return "No Result" // should never occurs
}
private fun showResults(tunedProfile: ATProfile?, pumpProfile: ATProfile): String {
if (tunedProfile == null)
return "No Result" // should never occurs
val line = rh.gs(R.string.autotune_log_separator)
var strResult = line
strResult += rh.gs(R.string.autotune_log_title)
strResult += line
val tuneInsulin = sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false)
if (tuneInsulin) {
strResult += rh.gs(R.string.autotune_log_peak, rh.gs(R.string.insulin_peak), pumpProfile.localInsulin.peak, tunedProfile.localInsulin.peak)
strResult += rh.gs(R.string.autotune_log_dia, rh.gs(R.string.ic_short), pumpProfile.localInsulin.dia, tunedProfile.localInsulin.dia)
}
// show ISF and CR
strResult += rh.gs(R.string.autotune_log_ic_isf, rh.gs(R.string.isf_short), pumpProfile.isf, tunedProfile.isf)
strResult += rh.gs(R.string.autotune_log_ic_isf, rh.gs(R.string.ic_short), pumpProfile.ic, tunedProfile.ic)
strResult += line
var totalBasal = 0.0
var totalTuned = 0.0
for (i in 0..23) {
totalBasal += pumpProfile.basal[i]
totalTuned += tunedProfile.basal[i]
val percentageChangeValue = tunedProfile.basal[i] / pumpProfile.basal[i] * 100 - 100
strResult += rh.gs(R.string.autotune_log_basal, i.toDouble(), pumpProfile.basal[i], tunedProfile.basal[i], tunedProfile.basalUntuned[i], percentageChangeValue)
}
strResult += line
strResult += rh.gs(R.string.autotune_log_sum_basal, totalBasal, totalTuned)
strResult += line
log(strResult)
return strResult
}
private fun settings(runDate: Long, nbDays: Int, firstloopstart: Long, lastloopend: Long): String {
var jsonString = ""
val jsonSettings = JSONObject()
val insulinInterface = activePlugin.activeInsulin
val utcOffset = T.msecs(TimeZone.getDefault().getOffset(dateUtil.now()).toLong()).hours()
val startDateString = dateUtil.toISOString(firstloopstart).substring(0,10)
val endDateString = dateUtil.toISOString(lastloopend - 24 * 60 * 60 * 1000L).substring(0,10)
val nsUrl = sp.getString(R.string.key_nsclientinternal_url, "")
val optCategorizeUam = if (sp.getBoolean(R.string.key_autotune_categorize_uam_as_basal, false)) "-c=true" else ""
val optInsulinCurve = if (sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false)) "-i=true" else ""
try {
jsonSettings.put("datestring", dateUtil.toISOString(runDate))
jsonSettings.put("dateutc", dateUtil.toISOAsUTC(runDate))
jsonSettings.put("utcOffset", utcOffset)
jsonSettings.put("units", profileFunction.getUnits().asText)
jsonSettings.put("timezone", TimeZone.getDefault().id)
jsonSettings.put("url_nightscout", sp.getString(R.string.key_nsclientinternal_url, ""))
jsonSettings.put("nbdays", nbDays)
jsonSettings.put("startdate", startDateString)
jsonSettings.put("enddate", endDateString)
// command to change timezone
jsonSettings.put("timezone_command", "sudo ln -sf /usr/share/zoneinfo/" + TimeZone.getDefault().id + " /etc/localtime")
// oref0_command is for running oref0-autotune on a virtual machine in a dedicated ~/aaps subfolder
jsonSettings.put("oref0_command", "oref0-autotune -d=~/aaps -n=$nsUrl -s=$startDateString -e=$endDateString $optCategorizeUam $optInsulinCurve")
// aaps_command is for running modified oref0-autotune with exported data from aaps (ns-entries and ns-treatment json files copied in ~/aaps/autotune folder and pumpprofile.json copied in ~/aaps/settings/
jsonSettings.put("aaps_command", "aaps-autotune -d=~/aaps -s=$startDateString -e=$endDateString $optCategorizeUam $optInsulinCurve")
jsonSettings.put("categorize_uam_as_basal", sp.getBoolean(R.string.key_autotune_categorize_uam_as_basal, false))
jsonSettings.put("tune_insulin_curve", false)
val peaktime: Int = insulinInterface.peak
if (insulinInterface.id === Insulin.InsulinType.OREF_ULTRA_RAPID_ACTING)
jsonSettings.put("curve","ultra-rapid")
else if (insulinInterface.id === Insulin.InsulinType.OREF_RAPID_ACTING)
jsonSettings.put("curve", "rapid-acting")
else if (insulinInterface.id === Insulin.InsulinType.OREF_LYUMJEV) {
jsonSettings.put("curve", "ultra-rapid")
jsonSettings.put("useCustomPeakTime", true)
jsonSettings.put("insulinPeakTime", peaktime)
} else if (insulinInterface.id === Insulin.InsulinType.OREF_FREE_PEAK) {
jsonSettings.put("curve", if (peaktime > 55) "rapid-acting" else "ultra-rapid")
jsonSettings.put("useCustomPeakTime", true)
jsonSettings.put("insulinPeakTime", peaktime)
}
jsonString = jsonSettings.toString(4).replace("\\/", "/")
} catch (e: JSONException) { }
return jsonString
}
fun updateProfile(newProfile: ATProfile?) {
if (newProfile == null) return
val circadian = sp.getBoolean(R.string.key_autotune_circadian_ic_isf, false)
val profileStore = activePlugin.activeProfileSource.profile ?: ProfileStore(injector, JSONObject(), dateUtil)
val profileList: ArrayList<CharSequence> = profileStore.getProfileList()
var indexLocalProfile = -1
for (p in profileList.indices)
if (profileList[p] == newProfile.profilename)
indexLocalProfile = p
if (indexLocalProfile == -1) {
localProfilePlugin.addProfile(localProfilePlugin.copyFrom(newProfile.getProfile(circadian), newProfile.profilename))
return
}
localProfilePlugin.currentProfileIndex = indexLocalProfile
localProfilePlugin.currentProfile()?.dia = newProfile.dia
localProfilePlugin.currentProfile()?.basal = newProfile.basal()
localProfilePlugin.currentProfile()?.ic = newProfile.ic(circadian)
localProfilePlugin.currentProfile()?.isf = newProfile.isf(circadian)
localProfilePlugin.storeSettings()
}
private fun log(message: String) {
atLog("[Plugin] $message")
}
override fun specialEnableCondition(): Boolean = buildHelper.isEngineeringMode() && buildHelper.isDev()
override fun atLog(message: String) {
autotuneFS.atLog(message)
}
}

View file

@ -0,0 +1,536 @@
package info.nightscout.androidaps.plugins.general.autotune
import info.nightscout.androidaps.R
import info.nightscout.androidaps.data.LocalInsulin
import info.nightscout.androidaps.database.entities.GlucoseValue
import info.nightscout.androidaps.plugins.general.autotune.data.*
import info.nightscout.androidaps.database.entities.Bolus
import info.nightscout.androidaps.database.entities.Carbs
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.MidnightTime
import info.nightscout.androidaps.utils.Round
import info.nightscout.androidaps.utils.T
import info.nightscout.shared.sharedPreferences.SP
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AutotunePrep @Inject constructor(
private val sp: SP,
private val dateUtil: DateUtil,
private val autotuneFS: AutotuneFS,
private val autotuneIob: AutotuneIob
) {
fun categorize(tunedprofile: ATProfile): PreppedGlucose? {
val preppedGlucose = categorizeBGDatums(tunedprofile, tunedprofile.localInsulin)
val tuneInsulin = sp.getBoolean(R.string.key_autotune_tune_insulin_curve, false)
if (tuneInsulin) {
var minDeviations = 1000000.0
val diaDeviations: MutableList<DiaDeviation> = ArrayList()
val peakDeviations: MutableList<PeakDeviation> = ArrayList()
val currentDIA = tunedprofile.localInsulin.dia
val currentPeak = tunedprofile.localInsulin.peak
var dia = currentDIA - 2
val endDIA = currentDIA + 2
while (dia <= endDIA)
{
var sqrtDeviations = 0.0
var deviations = 0.0
var deviationsSq = 0.0
val localInsulin = LocalInsulin("Ins_$currentPeak-$dia", currentPeak, dia)
val curve_output = categorizeBGDatums(tunedprofile, localInsulin, false)
val basalGlucose = curve_output?.basalGlucoseData
basalGlucose?.let {
for (hour in 0..23) {
for (i in 0..(basalGlucose.size-1)) {
val myHour = ((basalGlucose[i].date - MidnightTime.calc(basalGlucose[i].date)) / T.hours(1).msecs()).toInt()
if (hour == myHour) {
sqrtDeviations += Math.pow(Math.abs(basalGlucose[i].deviation), 0.5)
deviations += Math.abs(basalGlucose[i].deviation)
deviationsSq += Math.pow(basalGlucose[i].deviation, 2.0)
}
}
}
val meanDeviation = Round.roundTo(Math.abs(deviations / basalGlucose.size), 0.001)
val smrDeviation = Round.roundTo(Math.pow(sqrtDeviations / basalGlucose.size, 2.0), 0.001)
val rmsDeviation = Round.roundTo(Math.pow(deviationsSq / basalGlucose.size, 0.5), 0.001)
log("insulinEndTime $dia meanDeviation: $meanDeviation SMRDeviation: $smrDeviation RMSDeviation: $rmsDeviation (mg/dL)")
diaDeviations.add(
DiaDeviation(
dia = dia,
meanDeviation = meanDeviation,
smrDeviation = smrDeviation,
rmsDeviation = rmsDeviation
)
)
}
preppedGlucose?.diaDeviations = diaDeviations
deviations = Round.roundTo(deviations, 0.001)
if (deviations < minDeviations)
minDeviations = Round.roundTo(deviations, 0.001)
dia += 1.0
}
// consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', JSMath.Round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)');
//consoleError(diaDeviations);
minDeviations = 1000000.0
var peak = currentPeak - 10
val endPeak = currentPeak + 10
while (peak <= endPeak)
{
var sqrtDeviations = 0.0
var deviations = 0.0
var deviationsSq = 0.0
val localInsulin = LocalInsulin("Ins_$peak-$currentDIA", peak, currentDIA)
val curve_output = categorizeBGDatums(tunedprofile, localInsulin, false)
val basalGlucose = curve_output?.basalGlucoseData
basalGlucose?.let {
for (hour in 0..23) {
for (i in 0..(basalGlucose.size - 1)) {
val myHour = ((basalGlucose[i].date - MidnightTime.calc(basalGlucose[i].date)) / T.hours(1).msecs()).toInt()
if (hour == myHour) {
//console.error(basalGlucose[i].deviation);
sqrtDeviations += Math.pow(Math.abs(basalGlucose[i].deviation), 0.5)
deviations += Math.abs(basalGlucose[i].deviation)
deviationsSq += Math.pow(basalGlucose[i].deviation, 2.0)
}
}
}
val meanDeviation = Round.roundTo(deviations / basalGlucose.size, 0.001)
val smrDeviation = Round.roundTo(Math.pow(sqrtDeviations / basalGlucose.size, 2.0), 0.001)
val rmsDeviation = Round.roundTo(Math.pow(deviationsSq / basalGlucose.size, 0.5), 0.001)
log("insulinPeakTime $peak meanDeviation: $meanDeviation SMRDeviation: $smrDeviation RMSDeviation: $rmsDeviation (mg/dL)")
peakDeviations.add(
PeakDeviation
(
peak = peak,
meanDeviation = meanDeviation,
smrDeviation = smrDeviation,
rmsDeviation = rmsDeviation,
)
)
}
deviations = Round.roundTo(deviations, 0.001);
if (deviations < minDeviations)
minDeviations = Round.roundTo(deviations, 0.001)
peak += 5
}
//consoleError($"Optimum insulinPeakTime {newPeak} mean deviation: {JSMath.Round(minDeviations/basalGlucose.Count, 3)} (mg/dL)");
//consoleError(peakDeviations);
preppedGlucose?.peakDeviations = peakDeviations
}
return preppedGlucose
}
// private static Logger log = LoggerFactory.getLogger(AutotunePlugin.class);
fun categorizeBGDatums(tunedprofile: ATProfile, localInsulin: LocalInsulin, verbose: Boolean = true): PreppedGlucose? {
//lib/meals is called before to get only meals data (in AAPS it's done in AutotuneIob)
val treatments: MutableList<Carbs> = autotuneIob.meals
val boluses: MutableList<Bolus> = autotuneIob.boluses
// Bloc between #21 and # 54 replaced by bloc below (just remove BG value below 39, Collections.sort probably not necessary because BG values already sorted...)
val glucose = autotuneIob.glucose
val glucoseData: MutableList<GlucoseValue> = ArrayList()
for (i in glucose.indices) {
if (glucose[i].value > 39) {
glucoseData.add(glucose[i])
}
}
if (glucose.size == 0 || glucoseData.size == 0 ) {
if (verbose)
log("No BG value received")
return null
}
glucoseData.sortWith(object: Comparator<GlucoseValue>{ override fun compare(o1: GlucoseValue, o2: GlucoseValue): Int = (o2.timestamp - o1.timestamp).toInt() })
// Bloc below replace bloc between #55 and #71
// boluses and maxCarbs not used here ?,
// IOBInputs are for iob calculation (done here in AutotuneIob Class)
//val boluses = 0
//val maxCarbs = 0
if (treatments.size == 0) {
if (verbose)
log("No Carbs entries")
//return null
}
if (autotuneIob.boluses.size == 0) {
if (verbose)
log("No treatment received")
return null
}
var csfGlucoseData: MutableList<BGDatum> = ArrayList()
var isfGlucoseData: MutableList<BGDatum> = ArrayList()
var basalGlucoseData: MutableList<BGDatum> = ArrayList()
val uamGlucoseData: MutableList<BGDatum> = ArrayList()
val crData: MutableList<CRDatum> = ArrayList()
//Bloc below replace bloc between #72 and #93
// I keep it because BG lines in log are consistent between AAPS and Oref0
val bucketedData: MutableList<BGDatum> = ArrayList()
bucketedData.add(BGDatum(glucoseData[0], dateUtil))
//int j=0;
var k = 0 // index of first value used by bucket
//for loop to validate and bucket the data
for (i in 1 until glucoseData.size) {
val BGTime = glucoseData[i].timestamp
val lastBGTime = glucoseData[k].timestamp
val elapsedMinutes = (BGTime - lastBGTime) / (60 * 1000)
if (Math.abs(elapsedMinutes) >= 2) {
//j++; // move to next bucket
k = i // store index of first value used by bucket
bucketedData.add(BGDatum(glucoseData[i], dateUtil))
} else {
// average all readings within time deadband
val average = glucoseData[k]
for (l in k + 1 until i + 1) {
average.value += glucoseData[l].value
}
average.value = average.value / (i - k + 1)
bucketedData.add(BGDatum(average, dateUtil))
}
}
// Here treatments contains only meals data
// bloc between #94 and #114 remove meals before first BG value
// Bloc below replace bloc between #115 and #122 (initialize data before main loop)
// crInitialxx are declaration to be able to use these data in whole loop
var calculatingCR = false
var absorbing = false
var uam = false // unannounced meal
var mealCOB = 0.0
var mealCarbs = 0.0
var crCarbs = 0.0
var type = ""
var crInitialIOB = 0.0
var crInitialBG = 0.0
var crInitialCarbTime = 0L
//categorize.js#123 (Note: don't need fullHistory because data are managed in AutotuneIob Class)
//Here is main loop between #125 and #366
// main for loop
for (i in bucketedData.size - 5 downTo 1) {
val glucoseDatum = bucketedData[i]
//log.debug(glucoseDatum);
val BGTime = glucoseDatum.date
// 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.
val treatment = if (treatments.size > 0) treatments[treatments.size - 1] else null
var myCarbs = 0.0
if (treatment != null) {
if (treatment.timestamp < BGTime) {
if (treatment.amount > 0.0) {
mealCOB += treatment.amount
mealCarbs += treatment.amount
myCarbs = treatment.amount
}
treatments.removeAt(treatments.size - 1)
}
}
var bg = 0.0
var avgDelta = 0.0
// TODO: re-implement interpolation to avoid issues here with gaps
// calculate avgDelta as last 4 datapoints to better catch more rises after COB hits zero
if (bucketedData[i].value != 0.0 && bucketedData[i + 4].value != 0.0) {
//log.debug(bucketedData[i]);
bg = bucketedData[i].value
if (bg < 40 || bucketedData[i + 4].value < 40) {
//process.stderr.write("!");
continue
}
avgDelta = (bg - bucketedData[i + 4].value) / 4
} else {
if (verbose)
log("Could not find glucose data")
}
avgDelta = Round.roundTo(avgDelta, 0.01)
glucoseDatum.avgDelta = avgDelta
//sens = ISF
val sens = tunedprofile.isf
// for IOB calculations, 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 pumpbasalprofile to properly calculate IOB during periods where no temp basal is set
/* Note Philoul currentPumpBasal never used in oref0 Autotune code
var currentPumpBasal = pumpprofile.profile.getBasal(BGTime)
currentPumpBasal += pumpprofile.profile.getBasal(BGTime - 1 * 60 * 60 * 1000)
currentPumpBasal += pumpprofile.profile.getBasal(BGTime - 2 * 60 * 60 * 1000)
currentPumpBasal += pumpprofile.profile.getBasal(BGTime - 3 * 60 * 60 * 1000)
currentPumpBasal = Round.roundTo(currentPumpBasal / 4, 0.001) //CurrentPumpBasal for iob calculation is average of 4 last pumpProfile Basal rate
*/
// this is the current autotuned basal, used for everything else besides IOB calculations
val currentBasal = tunedprofile.getBasal(BGTime)
// basalBGI is BGI of basal insulin activity.
val basalBGI = Round.roundTo(currentBasal * sens / 60 * 5, 0.01) // 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];
// in autotune iob is calculated with 6 hours of history data, tunedProfile and average pumpProfile basal rate...
//log("currentBasal: " + currentBasal + " BGTime: " + BGTime + " / " + dateUtil!!.timeStringWithSeconds(BGTime) + "******************************************************************************************")
val iob = autotuneIob.getIOB(BGTime, localInsulin) // add localInsulin to be independent to InsulinPlugin
// activity times ISF times 5 minutes is BGI
val BGI = Round.roundTo(-iob.activity * sens * 5, 0.01)
// datum = one glucose data point (being prepped to store in output)
glucoseDatum.bgi = BGI
// calculating deviation
var deviation = avgDelta - BGI
// set positive deviations to zero if BG is below 80
if (bg < 80 && deviation > 0) {
deviation = 0.0
}
// rounding and storing deviation
deviation = Round.roundTo(deviation, 0.01)
glucoseDatum.deviation = deviation
// Then, calculate carb absorption for that 5m interval using the deviation.
if (mealCOB > 0) {
val ci = Math.max(deviation, sp.getDouble("openapsama_min_5m_carbimpact", 3.0))
val absorbed = ci * tunedprofile.ic / sens
// Store the COB, and use it as the starting point for the next data point.
mealCOB = Math.max(0.0, mealCOB - absorbed)
}
// 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 == false) {
crInitialIOB = iob.iob
crInitialBG = glucoseDatum.value
crInitialCarbTime = glucoseDatum.date
if (verbose)
log("CRInitialIOB: " + crInitialIOB + " CRInitialBG: " + crInitialBG + " CRInitialCarbTime: " + dateUtil.toISOString(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 {
val crEndIOB = iob.iob
val crEndBG = glucoseDatum.value
val crEndTime = glucoseDatum.date
if (verbose)
log("CREndIOB: " + crEndIOB + " CREndBG: " + crEndBG + " CREndTime: " + dateUtil.toISOString(crEndTime))
val crDatum = CRDatum(dateUtil)
crDatum.crInitialBG = crInitialBG
crDatum.crInitialIOB = crInitialIOB
crDatum.crInitialCarbTime = crInitialCarbTime
crDatum.crEndBG = crEndBG
crDatum.crEndIOB = crEndIOB
crDatum.crEndTime = crEndTime
crDatum.crCarbs = crCarbs
//log.debug(CRDatum);
//String crDataString = "{\"CRInitialIOB\": " + CRInitialIOB + ", \"CRInitialBG\": " + CRInitialBG + ", \"CRInitialCarbTime\": " + CRInitialCarbTime + ", \"CREndIOB\": " + CREndIOB + ", \"CREndBG\": " + CREndBG + ", \"CREndTime\": " + CREndTime + ", \"CRCarbs\": " + CRCarbs + "}";
val CRElapsedMinutes = Math.round((crEndTime - crInitialCarbTime) / (1000 * 60).toFloat())
//log.debug(CREndTime - CRInitialCarbTime, CRElapsedMinutes);
if (CRElapsedMinutes < 60 || i == 1 && mealCOB > 0) {
if (verbose)
log("Ignoring $CRElapsedMinutes m CR period.")
} else {
crData.add(crDatum)
}
crCarbs = 0.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 meal IOB has decayed, then end absorption after this data point unless COB > 0
absorbing = if (iob.iob < currentBasal / 2) {
false
// otherwise, as long as deviations are positive, keep tracking carb deviations
} else if (deviation > 0) {
true
} else {
false
}
if (!absorbing && mealCOB == 0.0) {
mealCarbs = 0.0
}
// check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag
//log.debug(type);
if (type != "csf") {
glucoseDatum.mealAbsorption = "start"
if (verbose)
log(glucoseDatum.mealAbsorption + " carb absorption")
}
type = "csf"
glucoseDatum.mealCarbs = mealCarbs.toInt()
//if (i == 0) { glucoseDatum.mealAbsorption = "end"; }
csfGlucoseData.add(glucoseDatum)
} else {
// check previous "type" value, and if it was csf, set a mealAbsorption end flag
if (type == "csf") {
csfGlucoseData[csfGlucoseData.size - 1].mealAbsorption = "end"
if (verbose)
log(csfGlucoseData[csfGlucoseData.size - 1].mealAbsorption + " carb absorption")
}
if (iob.iob > 2 * currentBasal || deviation > 6 || uam) {
uam = if (deviation > 0) {
true
} else {
false
}
if (type != "uam") {
glucoseDatum.uamAbsorption = "start"
if (verbose)
log(glucoseDatum.uamAbsorption + " unannnounced meal absorption")
}
type = "uam"
uamGlucoseData.add(glucoseDatum)
} else {
if (type == "uam") {
if (verbose)
log("end unannounced meal absorption")
}
// Go through the remaining time periods and divide them into periods where scheduled basal insulin activity dominates. This would be determined by calculating the BG impact of scheduled basal insulin
// (for example 1U/hr * 48 mg/dL/U ISF = 48 mg/dL/hr = 5 mg/dL/5m), and comparing that to BGI from bolus and net basal insulin activity.
// When BGI is positive (insulin activity is negative), we want to use that data to tune basals
// When BGI is smaller than about 1/4 of basalBGI, we want to use that data to tune basals
// When BGI is negative and more than about 1/4 of basalBGI, we can use that data to tune ISF,
// unless avgDelta is positive: then that's some sort of unexplained rise we don't want to use for ISF, so that means basals
if (basalBGI > -4 * BGI) {
type = "basal"
basalGlucoseData.add(glucoseDatum)
} else {
if (avgDelta > 0 && avgDelta > -2 * BGI) {
//type="unknown"
type = "basal"
basalGlucoseData.add(glucoseDatum)
} else {
type = "ISF"
isfGlucoseData.add(glucoseDatum)
}
}
}
}
// debug line to print out all the things
if (verbose)
log((if (absorbing) 1 else 0).toString() + " mealCOB: " + Round.roundTo(mealCOB, 0.1) + " mealCarbs: " + Math.round(mealCarbs) + " basalBGI: " + Round.roundTo(basalBGI, 0.1) + " BGI: " + Round.roundTo(BGI, 0.1) + " IOB: " + iob.iob+ " Activity: " + iob.activity + " at " + dateUtil.timeStringWithSeconds(BGTime) + " dev: " + deviation + " avgDelta: " + avgDelta + " " + type)
}
//****************************************************************************************************************************************
// categorize.js Lines 372-383
for (crDatum in crData) {
crDatum.crInsulin = dosed(crDatum.crInitialCarbTime, crDatum.crEndTime, boluses)
}
// categorize.js Lines 384-436
val CSFLength = csfGlucoseData.size
var ISFLength = isfGlucoseData.size
val UAMLength = uamGlucoseData.size
var basalLength = basalGlucoseData.size
if (sp.getBoolean(R.string.key_autotune_categorize_uam_as_basal, false)) {
if (verbose)
log("Categorizing all UAM data as basal.")
basalGlucoseData.addAll(uamGlucoseData)
} else if (CSFLength > 12) {
if (verbose)
log("Found at least 1h of carb: assuming meals were announced, and categorizing UAM data as basal.")
basalGlucoseData.addAll(uamGlucoseData)
} else {
if (2 * basalLength < UAMLength) {
//log.debug(basalGlucoseData, UAMGlucoseData);
if (verbose) {
log("Warning: too many deviations categorized as UnAnnounced Meals")
log("Adding $UAMLength UAM deviations to $basalLength basal ones")
}
basalGlucoseData.addAll(uamGlucoseData)
//log.debug(basalGlucoseData);
// if too much data is excluded as UAM, add in the UAM deviations, but then discard the highest 50%
basalGlucoseData.sortWith(object: Comparator<BGDatum>{ override fun compare(o1: BGDatum, o2: BGDatum): Int = (100 * o1.deviation - 100 * o2.deviation).toInt() }) //deviation rouded to 0.01, so *100 to avoid crash during sort
val newBasalGlucose: MutableList<BGDatum> = ArrayList()
for (i in 0 until basalGlucoseData.size / 2) {
newBasalGlucose.add(basalGlucoseData[i])
}
//log.debug(newBasalGlucose);
basalGlucoseData = newBasalGlucose
if (verbose)
log("and selecting the lowest 50%, leaving " + basalGlucoseData.size + " basal+UAM ones")
}
if (2 * ISFLength < UAMLength) {
if (verbose)
log("Adding $UAMLength UAM deviations to $ISFLength ISF ones")
isfGlucoseData.addAll(uamGlucoseData)
// if too much data is excluded as UAM, add in the UAM deviations to ISF, but then discard the highest 50%
isfGlucoseData.sortWith(object: Comparator<BGDatum>{ override fun compare(o1: BGDatum, o2: BGDatum): Int = (100 * o1.deviation - 100 * o2.deviation).toInt() }) //deviation rouded to 0.01, so *100 to avoid crash during sort
val newISFGlucose: MutableList<BGDatum> = ArrayList()
for (i in 0 until isfGlucoseData.size / 2) {
newISFGlucose.add(isfGlucoseData[i])
}
//console.error(newISFGlucose);
isfGlucoseData = newISFGlucose
if (verbose)
log("and selecting the lowest 50%, leaving " + isfGlucoseData.size + " ISF+UAM ones")
//log.error(ISFGlucoseData.length, UAMLength);
}
}
basalLength = basalGlucoseData.size
ISFLength = isfGlucoseData.size
if (4 * basalLength + ISFLength < CSFLength && ISFLength < 10) {
if (verbose) {
log("Warning: too many deviations categorized as meals")
//log.debug("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones");
//var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData);
log("Adding $CSFLength CSF deviations to $ISFLength ISF ones")
}
isfGlucoseData.addAll(csfGlucoseData)
csfGlucoseData = ArrayList()
}
// categorize.js Lines 437-444
if (verbose)
log("CRData: " + crData.size + " CSFGlucoseData: " + csfGlucoseData.size + " ISFGlucoseData: " + isfGlucoseData.size + " BasalGlucoseData: " + basalGlucoseData.size)
return PreppedGlucose(autotuneIob.startBG, crData, csfGlucoseData, isfGlucoseData, basalGlucoseData, dateUtil)
}
//dosed.js full
private fun dosed(start: Long, end: Long, treatments: List<Bolus>): Double {
var insulinDosed = 0.0
if (treatments.size == 0) {
log("No treatments to process.")
return 0.0
}
for (treatment in treatments) {
if (treatment.amount != 0.0 && treatment.timestamp > start && treatment.timestamp <= end) {
insulinDosed += treatment.amount
//log("CRDATA;${dateUtil.toISOString(start)};${dateUtil.toISOString(end)};${treatment.timestamp};${treatment.amount};$insulinDosed")
}
}
//log("insulin dosed: " + insulinDosed);
return Round.roundTo(insulinDosed, 0.001)
}
private fun log(message: String) {
autotuneFS.atLog("[Prep] $message")
}
}

View file

@ -0,0 +1,250 @@
package info.nightscout.androidaps.plugins.general.autotune.data
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.core.R
import info.nightscout.androidaps.data.LocalInsulin
import info.nightscout.androidaps.data.ProfileSealed
import info.nightscout.androidaps.data.PureProfile
import info.nightscout.androidaps.database.data.Block
import info.nightscout.androidaps.extensions.blockValueBySeconds
import info.nightscout.androidaps.extensions.pureProfileFromJson
import info.nightscout.androidaps.interfaces.*
import info.nightscout.androidaps.plugins.bus.RxBus
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.Round
import info.nightscout.androidaps.utils.T
import info.nightscout.shared.SafeParse
import info.nightscout.shared.sharedPreferences.SP
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.text.DecimalFormat
import java.util.*
import javax.inject.Inject
class ATProfile(profile: Profile, var localInsulin: LocalInsulin, val injector: HasAndroidInjector) {
@Inject lateinit var activePlugin: ActivePlugin
@Inject lateinit var sp: SP
@Inject lateinit var profileFunction: ProfileFunction
@Inject lateinit var dateUtil: DateUtil
@Inject lateinit var config: Config
@Inject lateinit var rxBus: RxBus
@Inject lateinit var rh: ResourceHelper
var profile: ProfileSealed
var circadianProfile: ProfileSealed
lateinit var pumpProfile: ProfileSealed
var profilename: String = ""
var basal = DoubleArray(24)
var basalUntuned = IntArray(24)
var ic = 0.0
var isf = 0.0
var dia = 0.0
var peak = 0
var isValid: Boolean = false
var from: Long = 0
var pumpProfileAvgISF = 0.0
var pumpProfileAvgIC = 0.0
val icSize: Int
get() = profile.getIcsValues().size
val isfSize: Int
get() = profile.getIsfsMgdlValues().size
val avgISF: Double
get() = if (profile.getIsfsMgdlValues().size == 1) profile.getIsfsMgdlValues().get(0).value else Round.roundTo(averageProfileValue(profile.getIsfsMgdlValues()), 0.01)
val avgIC: Double
get() = if (profile.getIcsValues().size == 1) profile.getIcsValues().get(0).value else Round.roundTo(averageProfileValue(profile.getIcsValues()), 0.01)
fun getBasal(timestamp: Long): Double = basal[Profile.secondsFromMidnight(timestamp)/3600]
// for localProfilePlugin Synchronisation
fun basal() = jsonArray(basal)
fun ic(circadian: Boolean = false): JSONArray {
if(circadian)
return jsonArray(pumpProfile.icBlocks, avgIC/pumpProfileAvgIC)
return jsonArray(ic)
}
fun isf(circadian: Boolean = false): JSONArray {
if(circadian)
return jsonArray(pumpProfile.isfBlocks, avgISF/pumpProfileAvgISF)
return jsonArray(Profile.fromMgdlToUnits(isf, profile.units))
}
fun getProfile(circadian: Boolean = false): PureProfile {
return if (circadian)
circadianProfile.convertToNonCustomizedProfile(dateUtil)
else
profile.convertToNonCustomizedProfile(dateUtil)
}
fun updateProfile() {
data()?.let { profile = ProfileSealed.Pure(it) }
data(true)?.let { circadianProfile = ProfileSealed.Pure(it) }
}
//Export json string with oref0 format used for autotune
// Include min_5m_carbimpact, insulin type, single value for carb_ratio and isf
fun profiletoOrefJSON(): String {
var jsonString = ""
val json = JSONObject()
val insulinInterface: Insulin = activePlugin.activeInsulin
try {
json.put("name", profilename)
json.put("min_5m_carbimpact", sp.getDouble("openapsama_min_5m_carbimpact", 3.0))
json.put("dia", dia)
if (insulinInterface.id === Insulin.InsulinType.OREF_ULTRA_RAPID_ACTING) json.put(
"curve",
"ultra-rapid"
) else if (insulinInterface.id === Insulin.InsulinType.OREF_RAPID_ACTING) json.put("curve", "rapid-acting") else if (insulinInterface.id === Insulin.InsulinType.OREF_LYUMJEV) {
json.put("curve", "ultra-rapid")
json.put("useCustomPeakTime", true)
json.put("insulinPeakTime", 45)
} else if (insulinInterface.id === Insulin.InsulinType.OREF_FREE_PEAK) {
val peaktime: Int = sp.getInt(rh.gs(R.string.key_insulin_oref_peak), 75)
json.put("curve", if (peaktime > 50) "rapid-acting" else "ultra-rapid")
json.put("useCustomPeakTime", true)
json.put("insulinPeakTime", peaktime)
}
val basals = JSONArray()
for (h in 0..23) {
val secondfrommidnight = h * 60 * 60
var time: String
time = DecimalFormat("00").format(h) + ":00:00"
basals.put(
JSONObject()
.put("start", time)
.put("minutes", h * 60)
.put("rate", profile.getBasalTimeFromMidnight(secondfrommidnight)
)
)
}
json.put("basalprofile", basals)
val isfvalue = Round.roundTo(avgISF, 0.001)
json.put(
"isfProfile",
JSONObject().put(
"sensitivities",
JSONArray().put(JSONObject().put("i", 0).put("start", "00:00:00").put("sensitivity", isfvalue).put("offset", 0).put("x", 0).put("endoffset", 1440))
)
)
json.put("carb_ratio", avgIC)
json.put("autosens_max", SafeParse.stringToDouble(sp.getString(R.string.key_openapsama_autosens_max, "1.2")))
json.put("autosens_min", SafeParse.stringToDouble(sp.getString(R.string.key_openapsama_autosens_min, "0.7")))
json.put("units", GlucoseUnit.MGDL.asText)
json.put("timezone", TimeZone.getDefault().id)
jsonString = json.toString(2).replace("\\/", "/")
} catch (e: JSONException) {}
return jsonString
}
fun data(circadian: Boolean = false): PureProfile? {
val json: JSONObject = profile.toPureNsJson(dateUtil)
try {
json.put("dia", dia)
if (circadian) {
json.put("sens", jsonArray(pumpProfile.isfBlocks, avgISF/pumpProfileAvgISF))
json.put("carbratio", jsonArray(pumpProfile.icBlocks, avgIC/pumpProfileAvgIC))
} else {
json.put("sens", jsonArray(Profile.fromMgdlToUnits(isf, profile.units)))
json.put("carbratio", jsonArray(ic))
}
json.put("basal", jsonArray(basal))
} catch (e: JSONException) {
}
return pureProfileFromJson(json, dateUtil, profile.units.asText)
}
fun profileStore(circadian: Boolean = false): ProfileStore?
{
var profileStore: ProfileStore? = null
val json = JSONObject()
val store = JSONObject()
val tunedProfile = if (circadian) circadianProfile else profile
if (profilename.isEmpty())
profilename = rh.gs(R.string.autotune_tunedprofile_name)
try {
store.put(profilename, tunedProfile.toPureNsJson(dateUtil))
json.put("defaultProfile", profilename)
json.put("store", store)
json.put("startDate", dateUtil.toISOAsUTC(dateUtil.now()))
profileStore = ProfileStore(injector, json, dateUtil)
} catch (e: JSONException) { }
return profileStore
}
fun jsonArray(values: DoubleArray): JSONArray {
val json = JSONArray()
for (h in 0..23) {
val secondfrommidnight = h * 60 * 60
val df = DecimalFormat("00")
val time = df.format(h.toLong()) + ":00"
json.put(
JSONObject()
.put("time", time)
.put("timeAsSeconds", secondfrommidnight)
.put("value", values[h])
)
}
return json
}
fun jsonArray(value: Double) =
JSONArray().put(
JSONObject()
.put("time", "00:00")
.put("timeAsSeconds", 0)
.put("value", value)
)
fun jsonArray(values: List<Block>, multiplier: Double = 1.0): JSONArray {
val json = JSONArray()
var elapsedHours = 0L
values.forEach {
val value = values.blockValueBySeconds(T.hours(elapsedHours).secs().toInt(), multiplier, 0)
json.put(
JSONObject()
.put("time", DecimalFormat("00").format(elapsedHours) + ":00")
.put("timeAsSeconds", T.hours(elapsedHours).secs())
.put("value", value)
)
elapsedHours += T.msecs(it.duration).hours()
}
return json
}
companion object {
fun averageProfileValue(pf: Array<Profile.ProfileValue>?): Double {
var avgValue = 0.0
val secondPerDay = 24 * 60 * 60
if (pf == null) return avgValue
for (i in pf.indices) {
avgValue += pf[i].value * ((if (i == pf.size - 1) secondPerDay else pf[i + 1].timeAsSeconds) - pf[i].timeAsSeconds)
}
avgValue /= secondPerDay.toDouble()
return avgValue
}
}
init {
injector.androidInjector().inject(this)
this.profile = profile as ProfileSealed
circadianProfile = profile
isValid = profile.isValid
if (isValid) {
//initialize tuned value with current profile values
for (h in 0..23) {
basal[h] = Round.roundTo(profile.basalBlocks.blockValueBySeconds(T.hours(h.toLong()).secs().toInt(), 1.0, 0), 0.001)
}
ic = avgIC
isf = avgISF
pumpProfile = profile
pumpProfileAvgIC = avgIC
pumpProfileAvgISF = avgISF
}
dia = localInsulin.dia
peak = localInsulin.peak
}
}

View file

@ -0,0 +1,80 @@
package info.nightscout.androidaps.plugins.general.autotune.data
import info.nightscout.androidaps.database.entities.GlucoseValue.TrendArrow
import info.nightscout.androidaps.database.entities.GlucoseValue
import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.T
import org.json.JSONException
import org.json.JSONObject
import java.util.*
/**
* Created by Rumen Georgiev on 2/24/2018.
*/
class BGDatum {
//Added by Rumen for autotune
var id: Long = 0
var date = 0L
var value = 0.0
var direction: TrendArrow? = null
var deviation = 0.0
var bgi = 0.0
var mealAbsorption = ""
var mealCarbs = 0
var uamAbsorption = ""
var avgDelta = 0.0
var bgReading: GlucoseValue? = null
private set
var dateUtil: DateUtil
constructor(dateUtil: DateUtil) { this.dateUtil = dateUtil}
constructor(json: JSONObject, dateUtil: DateUtil) {
this.dateUtil = dateUtil
try {
if (json.has("_id")) id = json.getLong("_id")
if (json.has("date")) date = json.getLong("date")
if (json.has("sgv")) value = json.getDouble("sgv")
if (json.has("direction")) direction = TrendArrow.fromString(json.getString("direction"))
if (json.has("deviation")) deviation = json.getDouble("deviation")
if (json.has("BGI")) bgi = json.getDouble("BGI")
if (json.has("avgDelta")) avgDelta = json.getDouble("avgDelta")
if (json.has("mealAbsorption")) mealAbsorption = json.getString("mealAbsorption")
if (json.has("mealCarbs")) mealCarbs = json.getInt("mealCarbs")
} catch (e: JSONException) {
}
}
constructor(glucoseValue: GlucoseValue, dateUtil: DateUtil) {
this.dateUtil = dateUtil
date = glucoseValue.timestamp
value = glucoseValue.value
direction = glucoseValue.trendArrow
id = glucoseValue.id
this.bgReading = glucoseValue
}
fun toJSON(mealData: Boolean): JSONObject {
val bgjson = JSONObject()
val utcOffset = T.msecs(TimeZone.getDefault().getOffset(dateUtil.now()).toLong()).hours()
try {
bgjson.put("_id", id)
bgjson.put("date", date)
bgjson.put("dateString", dateUtil.toISOAsUTC(date))
bgjson.put("sgv", value)
bgjson.put("direction", direction)
bgjson.put("type", "sgv")
bgjson.put("sysTime", dateUtil.toISOAsUTC(date))
bgjson.put("utcOffset", utcOffset)
bgjson.put("glucose", value)
bgjson.put("avgDelta", avgDelta)
bgjson.put("BGI", bgi)
bgjson.put("deviation", deviation)
if (mealData) {
bgjson.put("mealAbsorption", mealAbsorption)
bgjson.put("mealCarbs", mealCarbs)
}
} catch (e: JSONException) {
}
return bgjson
}
}

View file

@ -0,0 +1,54 @@
package info.nightscout.androidaps.plugins.general.autotune.data
import info.nightscout.androidaps.utils.DateUtil
import org.json.JSONException
import org.json.JSONObject
/**
* Created by Rumen Georgiev on 2/26/2018.
*/
class CRDatum {
var crInitialIOB = 0.0
var crInitialBG = 0.0
var crInitialCarbTime = 0L
var crEndIOB = 0.0
var crEndBG = 0.0
var crEndTime = 0L
var crCarbs = 0.0
var crInsulin = 0.0
var crInsulinTotal = 0.0
var dateUtil: DateUtil
constructor(dateUtil: DateUtil) { this.dateUtil = dateUtil}
constructor(json: JSONObject, dateUtil: DateUtil) {
this.dateUtil = dateUtil
try {
if (json.has("CRInitialIOB")) crInitialIOB = json.getDouble("CRInitialIOB")
if (json.has("CRInitialBG")) crInitialBG = json.getDouble("CRInitialBG")
if (json.has("CRInitialCarbTime")) crInitialCarbTime = dateUtil.fromISODateString(json.getString("CRInitialCarbTime"))
if (json.has("CREndIOB")) crEndIOB = json.getDouble("CREndIOB")
if (json.has("CREndBG")) crEndBG = json.getDouble("CREndBG")
if (json.has("CREndTime")) crEndTime = dateUtil.fromISODateString(json.getString("CREndTime"))
if (json.has("CRCarbs")) crCarbs = json.getDouble("CRCarbs")
if (json.has("CRInsulin")) crInsulin = json.getDouble("CRInsulin")
} catch (e: JSONException) {
}
}
fun toJSON(): JSONObject {
val crjson = JSONObject()
try {
crjson.put("CRInitialIOB", crInitialIOB)
crjson.put("CRInitialBG", crInitialBG.toInt())
crjson.put("CRInitialCarbTime", dateUtil.toISOString(crInitialCarbTime))
crjson.put("CREndIOB", crEndIOB)
crjson.put("CREndBG", crEndBG.toInt())
crjson.put("CREndTime", dateUtil.toISOString(crEndTime))
crjson.put("CRCarbs", crCarbs.toInt())
crjson.put("CRInsulin", crInsulin)
} catch (e: JSONException) {
}
return crjson
}
}

View file

@ -0,0 +1,29 @@
package info.nightscout.androidaps.plugins.general.autotune.data
import org.json.JSONException
import org.json.JSONObject
class DiaDeviation(var dia: Double = 0.0, var meanDeviation: Double = 0.0, var smrDeviation: Double = 0.0, var rmsDeviation: Double = 0.0) {
constructor(json: JSONObject) : this() {
try {
if (json.has("dia")) dia = json.getDouble("dia")
if (json.has("meanDeviation")) meanDeviation = json.getDouble("meanDeviation")
if (json.has("SMRDeviation")) smrDeviation = json.getDouble("SMRDeviation")
if (json.has("RMSDeviation")) rmsDeviation = json.getDouble("RMSDeviation")
} catch (e: JSONException) {
}
}
fun toJSON(): JSONObject {
val crjson = JSONObject()
try {
crjson.put("dia", dia)
crjson.put("meanDeviation", meanDeviation.toInt())
crjson.put("SMRDeviation", smrDeviation)
crjson.put("RMSDeviation", rmsDeviation.toInt())
} catch (e: JSONException) {
}
return crjson
}
}

View file

@ -0,0 +1,29 @@
package info.nightscout.androidaps.plugins.general.autotune.data
import org.json.JSONException
import org.json.JSONObject
class PeakDeviation(var peak: Int = 0, var meanDeviation: Double = 0.0, var smrDeviation: Double = 0.0, var rmsDeviation: Double = 0.0) {
constructor(json: JSONObject) : this() {
try {
if (json.has("peak")) peak = json.getInt("peak")
if (json.has("meanDeviation")) meanDeviation = json.getDouble("meanDeviation")
if (json.has("SMRDeviation")) smrDeviation = json.getDouble("SMRDeviation")
if (json.has("RMSDeviation")) rmsDeviation = json.getDouble("RMSDeviation")
} catch (e: JSONException) {
}
}
fun toJSON(): JSONObject {
val crjson = JSONObject()
try {
crjson.put("peak", peak)
crjson.put("meanDeviation", meanDeviation.toInt())
crjson.put("SMRDeviation", smrDeviation)
crjson.put("RMSDeviation", rmsDeviation.toInt())
} catch (e: JSONException) {
}
return crjson
}
}

View file

@ -0,0 +1,117 @@
package info.nightscout.androidaps.plugins.general.autotune.data
import info.nightscout.androidaps.utils.DateUtil
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.util.*
class PreppedGlucose {
var crData: List<CRDatum> = ArrayList()
var csfGlucoseData: List<BGDatum> = ArrayList()
var isfGlucoseData: List<BGDatum> = ArrayList()
var basalGlucoseData: List<BGDatum> = ArrayList()
var diaDeviations: List<DiaDeviation> = ArrayList()
var peakDeviations: List<PeakDeviation> = ArrayList()
var from: Long = 0
lateinit var dateUtil: DateUtil
// to generate same king of json string than oref0-autotune-prep
override fun toString(): String {
return toString(0)
}
constructor(from: Long, crData: List<CRDatum>, csfGlucoseData: List<BGDatum>, isfGlucoseData: List<BGDatum>, basalGlucoseData: List<BGDatum>, dateUtil: DateUtil) {
this.from = from
this.crData = crData
this.csfGlucoseData = csfGlucoseData
this.isfGlucoseData = isfGlucoseData
this.basalGlucoseData = basalGlucoseData
this.dateUtil = dateUtil
}
constructor(json: JSONObject?, dateUtil: DateUtil) {
if (json == null) return
this.dateUtil = dateUtil
crData = ArrayList()
csfGlucoseData = ArrayList()
isfGlucoseData = ArrayList()
basalGlucoseData = ArrayList()
try {
crData = JsonCRDataToList(json.getJSONArray("CRData"))
csfGlucoseData = JsonGlucoseDataToList(json.getJSONArray("CSFGlucoseData"))
isfGlucoseData = JsonGlucoseDataToList(json.getJSONArray("ISFGlucoseData"))
basalGlucoseData = JsonGlucoseDataToList(json.getJSONArray("basalGlucoseData"))
} catch (e: JSONException) {
}
}
private fun JsonGlucoseDataToList(array: JSONArray): List<BGDatum> {
val bgData: MutableList<BGDatum> = ArrayList()
for (index in 0 until array.length()) {
try {
val o = array.getJSONObject(index)
bgData.add(BGDatum(o, dateUtil))
} catch (e: Exception) {
}
}
return bgData
}
private fun JsonCRDataToList(array: JSONArray): List<CRDatum> {
val crData: MutableList<CRDatum> = ArrayList()
for (index in 0 until array.length()) {
try {
val o = array.getJSONObject(index)
crData.add(CRDatum(o, dateUtil))
} catch (e: Exception) {
}
}
return crData
}
fun toString(indent: Int): String {
var jsonString = ""
val json = JSONObject()
try {
val crjson = JSONArray()
for (crd in crData) {
crjson.put(crd.toJSON())
}
val csfjson = JSONArray()
for (bgd in csfGlucoseData) {
csfjson.put(bgd.toJSON(true))
}
val isfjson = JSONArray()
for (bgd in isfGlucoseData) {
isfjson.put(bgd.toJSON(false))
}
val basaljson = JSONArray()
for (bgd in basalGlucoseData) {
basaljson.put(bgd.toJSON(false))
}
val diajson = JSONArray()
val peakjson = JSONArray()
if (diaDeviations.size > 0 || peakDeviations.size > 0) {
for (diad in diaDeviations) {
diajson.put(diad.toJSON())
}
for (peakd in peakDeviations) {
peakjson.put(peakd.toJSON())
}
}
json.put("CRData", crjson)
json.put("CSFGlucoseData", csfjson)
json.put("ISFGlucoseData", isfjson)
json.put("basalGlucoseData", basaljson)
if (diaDeviations.size > 0 || peakDeviations.size > 0) {
json.put("diaDeviations", diajson)
json.put("peakDeviations", peakjson)
}
jsonString = if (indent != 0) json.toString(indent) else json.toString()
} catch (e: JSONException) {
}
return jsonString
}
}

View file

@ -0,0 +1,5 @@
package info.nightscout.androidaps.plugins.general.autotune.events
import info.nightscout.androidaps.events.Event
class EventAutotuneUpdateGui : Event()

View file

@ -69,11 +69,24 @@ class MaintenancePlugin @Inject constructor(
val files = logDir.listFiles { _: File?, name: String ->
(name.startsWith("AndroidAPS") && name.endsWith(".zip"))
}
val autotunefiles = logDir.listFiles { _: File?, name: String ->
(name.startsWith("autotune") && name.endsWith(".zip"))
}
val amount = sp.getInt(R.string.key_logshipper_amount, keep)
val keepIndex = amount - 1
if (autotunefiles != null && autotunefiles.isNotEmpty()) {
Arrays.sort(autotunefiles) { f1: File, f2: File -> f2.name.compareTo(f1.name) }
var delAutotuneFiles = listOf(*autotunefiles)
if (keepIndex < delAutotuneFiles.size) {
delAutotuneFiles = delAutotuneFiles.subList(keepIndex, delAutotuneFiles.size)
for (file in delAutotuneFiles) {
file.delete()
}
}
}
if (files == null || files.isEmpty()) return
Arrays.sort(files) { f1: File, f2: File -> f2.name.compareTo(f1.name) }
var delFiles = listOf(*files)
val amount = sp.getInt(R.string.key_logshipper_amount, keep)
val keepIndex = amount - 1
if (keepIndex < delFiles.size) {
delFiles = delFiles.subList(keepIndex, delFiles.size)
for (file in delFiles) {

View file

@ -0,0 +1,343 @@
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".plugins.general.autotune.AutotuneFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.MaterialComponents.CardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_marginTop="4dp"
app:cardCornerRadius="4dp"
app:contentPadding="2dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="false"
android:layout_gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_weight="2"
android:gravity="end"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/autotune_profile"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/autotune_select_profile"
android:paddingStart="5dp"
android:paddingEnd="5dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/profileList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingTop="5dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_weight="2"
android:gravity="end"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/autotune_tune_days"
android:textSize="14sp" />
<info.nightscout.androidaps.utils.ui.NumberPicker
android:id="@+id/tune_days"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:layout_weight="1"
android:layout_marginBottom="2dp"
app:customContentDescription="@string/careportal_newnstreatment_duration_label" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.MaterialComponents.CardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_marginTop="4dp"
app:cardCornerRadius="4dp"
app:contentPadding="2dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="false"
android:layout_gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/autotune_last_run"
android:textSize="14sp" />
<TextView
android:id="@+id/tune_lastrun"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="end"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/autotune_warning"
android:textSize="14sp" />
<TextView
android:id="@+id/tune_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.MaterialComponents.CardView"
android:id="@+id/autotune_results_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_marginTop="4dp"
app:cardCornerRadius="4dp"
app:contentPadding="2dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="false"
android:layout_gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/autotune_results"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
</LinearLayout>
<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dip"
app:columnCount="2">
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_profileswitch"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/activate_profile"
app:icon="@drawable/ic_local_activate"
app:iconPadding="-6dp"
app:iconTint="@color/ic_local_activate"
app:layout_column="0"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_compare"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/autotune_compare_profile"
app:icon="@drawable/ic_compare_profiles"
app:iconPadding="-6dp"
app:iconTintMode="multiply"
app:layout_column="1"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_copylocal"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/autotune_copy_localprofile_button"
app:icon="@drawable/ic_clone_48"
app:iconPadding="-6dp"
app:iconTintMode="multiply"
app:layout_column="0"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_update_profile"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/autotune_update_input_profile_button"
app:icon="@drawable/ic_local_save"
app:iconPadding="-8dp"
app:iconTint="@color/ic_local_save"
app:layout_column="1"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_revert_profile"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/autotune_revert_input_profile_button"
android:visibility="gone"
app:icon="@drawable/ic_local_reset"
app:iconPadding="-6dp"
app:iconTint="@color/ic_local_reset"
app:layout_column="1"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_run"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/autotune_run"
app:icon="@drawable/ic_autotune"
app:iconPadding="-4dp"
app:iconTint="@color/ic_local_save"
app:layout_column="0"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/autotune_check_input_profile"
style="@style/GrayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/autotune_check_input_profile_button"
app:icon="@drawable/ic_home_profile"
app:iconPadding="-6dp"
app:iconTint="?attr/profileColor"
app:layout_column="1"
app:layout_columnWeight="1"
app:layout_gravity="fill"
app:layout_row="2" />
</androidx.gridlayout.widget.GridLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -533,6 +533,7 @@
<string name="key_insulin_oref_peak" translatable="false">insulin_oref_peak</string>
<string name="insulin_oref_peak">IOB Curve Peak Time</string>
<string name="insulin_peak_time">Peak Time [min]</string>
<string name="insulin_peak">Peak</string>
<string name="free_peak_oref">Free-Peak Oref</string>
<string name="rapid_acting_oref">Rapid-Acting Oref</string>
<string name="ultrarapid_oref">Ultra-Rapid Oref</string>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="@string/key_autotune_plugin"
android:title="@string/autotune_settings"
app:initialExpandedChildrenCount="10">
<SwitchPreference
android:defaultValue="false"
android:key="@string/key_autotune_auto"
android:summary="@string/autotune_auto_summary"
android:title="@string/autotune_auto_title" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/key_autotune_categorize_uam_as_basal"
android:summary="@string/autotune_categorize_uam_as_basal_summary"
android:title="@string/autotune_categorize_uam_as_basal_title" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/key_autotune_tune_insulin_curve"
android:summary="@string/autotune_tune_insulin_curve_summary"
android:title="@string/autotune_tune_insulin_curve_title" />
<EditTextPreference
android:defaultValue="5"
android:inputType="number"
android:key="@string/key_autotune_default_tune_days"
android:summary="@string/autotune_default_tune_days_summary"
android:title="@string/autotune_default_tune_days_title" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/key_autotune_circadian_ic_isf"
android:summary="@string/autotune_circadian_ic_isf_summary"
android:title="@string/autotune_circadian_ic_isf_title" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/key_autotune_additional_log"
android:summary="@string/autotune_additional_log_summary"
android:title="@string/autotune_additional_log_title" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View file

@ -42,6 +42,7 @@ abstract class AutomationModule {
@ContributesAndroidInjector abstract fun actionCarePortalEventInjector(): ActionCarePortalEvent
@ContributesAndroidInjector abstract fun actionProfileSwitchInjector(): ActionProfileSwitch
@ContributesAndroidInjector abstract fun actionProfileSwitchPercentInjector(): ActionProfileSwitchPercent
@ContributesAndroidInjector abstract fun actionRunAutotuneInjector(): ActionRunAutotune
@ContributesAndroidInjector abstract fun actionSendSMSInjector(): ActionSendSMS
@ContributesAndroidInjector abstract fun actionStartTempTargetInjector(): ActionStartTempTarget
@ContributesAndroidInjector abstract fun actionStopTempTargetInjector(): ActionStopTempTarget

View file

@ -332,6 +332,7 @@ class AutomationPlugin @Inject constructor(
ActionCarePortalEvent(injector),
ActionProfileSwitchPercent(injector),
ActionProfileSwitch(injector),
ActionRunAutotune(injector),
ActionSendSMS(injector)
)
}

View file

@ -71,6 +71,8 @@ abstract class Action(val injector: HasAndroidInjector) {
ActionProfileSwitch::class.java.simpleName -> ActionProfileSwitch(injector).fromJSON(data.toString())
ActionProfileSwitchPercent::class.java.name,
ActionProfileSwitchPercent::class.java.simpleName -> ActionProfileSwitchPercent(injector).fromJSON(data.toString())
ActionRunAutotune::class.java.name,
ActionRunAutotune::class.java.simpleName -> ActionRunAutotune(injector).fromJSON(data.toString())
ActionSendSMS::class.java.name,
ActionSendSMS::class.java.simpleName -> ActionSendSMS(injector).fromJSON(data.toString())
ActionStartTempTarget::class.java.name,

View file

@ -0,0 +1,89 @@
package info.nightscout.androidaps.plugins.general.automation.actions
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.automation.R
import info.nightscout.androidaps.data.PumpEnactResult
import info.nightscout.androidaps.interfaces.ActivePlugin
import info.nightscout.androidaps.interfaces.Autotune
import info.nightscout.androidaps.interfaces.ProfileFunction
import info.nightscout.androidaps.interfaces.ResourceHelper
import info.nightscout.androidaps.logging.UserEntryLogger
import info.nightscout.androidaps.plugins.general.automation.elements.InputDuration
import info.nightscout.androidaps.plugins.general.automation.elements.InputProfileName
import info.nightscout.androidaps.plugins.general.automation.elements.LabelWithElement
import info.nightscout.androidaps.plugins.general.automation.elements.LayoutBuilder
import info.nightscout.androidaps.queue.Callback
import info.nightscout.androidaps.utils.JsonHelper
import info.nightscout.shared.logging.LTag
import info.nightscout.shared.sharedPreferences.SP
import org.json.JSONObject
import javax.inject.Inject
class ActionRunAutotune(injector: HasAndroidInjector) : Action(injector) {
@Inject lateinit var resourceHelper: ResourceHelper
@Inject lateinit var autotunePlugin: Autotune
@Inject lateinit var profileFunction: ProfileFunction
@Inject lateinit var activePlugin: ActivePlugin
@Inject lateinit var sp: SP
@Inject lateinit var uel: UserEntryLogger
var defaultValue = 0
private var inputProfileName = InputProfileName(rh, activePlugin, "", true)
private var daysBack = InputDuration(0, InputDuration.TimeUnit.DAYS)
override fun friendlyName(): Int = R.string.autotune_run
override fun shortDescription(): String = resourceHelper.gs(R.string.autotune_profile_name, inputProfileName.value)
@DrawableRes override fun icon(): Int = R.drawable.ic_actions_profileswitch
override fun doAction(callback: Callback) {
val autoSwitch = sp.getBoolean(R.string.key_autotune_auto, false)
val profileName = if (inputProfileName.value == rh.gs(R.string.active)) "" else inputProfileName.value
var message = if (autoSwitch) R.string.autotune_run_with_autoswitch else R.string.autotune_run_without_autoswitch
Thread {
autotunePlugin.atLog("[Automation] Run Autotune $profileName, ${daysBack.value} days, Autoswitch $autoSwitch")
autotunePlugin.aapsAutotune(daysBack.value, autoSwitch, profileName)
if (!autotunePlugin.lastRunSuccess) {
message = R.string.autotune_run_with_error
aapsLogger.error(LTag.AUTOMATION, "Error during Autotune Run")
}
callback.result(PumpEnactResult(injector).success(autotunePlugin.lastRunSuccess).comment(message))?.run()
}.start()
return
}
override fun generateDialog(root: LinearLayout) {
if (defaultValue == 0)
defaultValue = sp.getInt(R.string.key_autotune_default_tune_days, 5)
daysBack.value = defaultValue
LayoutBuilder()
.add(LabelWithElement(rh, rh.gs(R.string.autotune_select_profile), "", inputProfileName))
.add(LabelWithElement(rh, rh.gs(R.string.autotune_tune_days), "", daysBack))
.build(root)
}
override fun hasDialog(): Boolean = true
override fun toJSON(): String {
val data = JSONObject()
.put("profileToTune", inputProfileName.value)
.put("tunedays", daysBack.value)
return JSONObject()
.put("type", this.javaClass.name)
.put("data", data)
.toString()
}
override fun fromJSON(data: String): Action {
val o = JSONObject(data)
inputProfileName.value = JsonHelper.safeGetString(o, "profileToTune", "")
defaultValue = JsonHelper.safeGetInt(o, "tunedays")
if (defaultValue == 0)
defaultValue = sp.getInt(R.string.key_autotune_default_tune_days, 5)
daysBack.value = defaultValue
return this
}
override fun isValid(): Boolean = profileFunction.getProfile() != null && activePlugin.getSpecificPluginsListByInterface(Autotune::class.java).first().isEnabled()
}

View file

@ -13,7 +13,7 @@ class InputDuration(
) : Element() {
enum class TimeUnit {
MINUTES, HOURS
MINUTES, HOURS, DAYS
}
override fun addToLayout(root: LinearLayout) {
@ -21,6 +21,9 @@ class InputDuration(
if (unit == TimeUnit.MINUTES) {
numberPicker = MinutesNumberPicker(root.context, null)
numberPicker.setParams(value.toDouble(), 5.0, 24 * 60.0, 10.0, DecimalFormat("0"), false, root.findViewById(R.id.ok))
} else if (unit == TimeUnit.DAYS) {
numberPicker = MinutesNumberPicker(root.context, null)
numberPicker.setParams(value.toDouble(), 1.0, 30.0, 1.0, DecimalFormat("0"), false, root.findViewById(R.id.ok))
} else {
numberPicker = NumberPicker(root.context, null)
numberPicker.setParams(value.toDouble(), 1.0, 24.0, 1.0, DecimalFormat("0"), false, root.findViewById(R.id.ok))

View file

@ -10,14 +10,15 @@ import info.nightscout.androidaps.automation.R
import info.nightscout.androidaps.interfaces.ActivePlugin
import info.nightscout.androidaps.interfaces.ResourceHelper
class InputProfileName(private val rh: ResourceHelper, private val activePlugin: ActivePlugin, val name: String = "") : Element() {
class InputProfileName(private val rh: ResourceHelper, private val activePlugin: ActivePlugin, val name: String = "", val addActive: Boolean = false) : Element() {
var value: String = name
override fun addToLayout(root: LinearLayout) {
val profileStore = activePlugin.activeProfileSource.profile ?: return
val profileList = profileStore.getProfileList()
if (addActive)
profileList.add(0, rh.gs(R.string.active))
root.addView(
Spinner(root.context).apply {
adapter = ArrayAdapter(root.context, R.layout.spinner_centered, profileList).apply {

View file

@ -0,0 +1,45 @@
package info.nightscout.androidaps.data
import info.nightscout.androidaps.database.entities.Bolus
import kotlin.math.exp
import kotlin.math.pow
class LocalInsulin constructor(val name:String?, val peak:Int = DEFAULT_PEAK, private val userDefinedDia: Double = DEFAULT_DIA) {
val dia
get(): Double {
val dia = userDefinedDia
return if (dia >= MIN_DIA) {
dia
} else {
MIN_DIA
}
}
val duration
get() = (60 * 60 * 1000L * dia).toLong()
fun iobCalcForTreatment(bolus: Bolus, time: Long): Iob {
val result = Iob()
if (bolus.amount != 0.0) {
val bolusTime = bolus.timestamp
val t = (time - bolusTime) / 1000.0 / 60.0
val td = dia * 60 //getDIA() always >= MIN_DIA
val tp = peak.toDouble()
// force the IOB to 0 if over DIA hours have passed
if (t < td) {
val tau = tp * (1 - tp / td) / (1 - 2 * tp / td)
val a = 2 * tau / td
val S = 1 / (1 - a + (1 + a) * exp(-td / tau))
result.activityContrib = bolus.amount * (S / tau.pow(2.0)) * t * (1 - t / td) * exp(-t / tau)
result.iobContrib = bolus.amount * (1 - S * (1 - a) * ((t.pow(2.0) / (tau * td * (1 - a)) - t / tau - 1) * Math.exp(-t / tau) + 1))
}
}
return result
}
companion object {
private const val MIN_DIA = 5.0
private const val DEFAULT_DIA = 6.0
private const val DEFAULT_PEAK = 75
}
}

View file

@ -1,6 +1,7 @@
package info.nightscout.androidaps.extensions
import info.nightscout.androidaps.data.Iob
import info.nightscout.androidaps.data.LocalInsulin
import info.nightscout.androidaps.database.embedments.InterfaceIDs
import info.nightscout.androidaps.database.entities.Bolus
import info.nightscout.androidaps.database.entities.TherapyEvent
@ -16,6 +17,12 @@ fun Bolus.iobCalc(activePlugin: ActivePlugin, time: Long, dia: Double): Iob {
return insulinInterface.iobCalcForTreatment(this, time, dia)
}
// Add specific calculation for Autotune (reference localInsulin for Peak/dia)
fun Bolus.iobCalc(time: Long, localInsulin: LocalInsulin): Iob {
if (!isValid || type == Bolus.Type.PRIMING ) return Iob()
return localInsulin.iobCalcForTreatment(this, time)
}
fun Bolus.toJson(isAdd: Boolean, dateUtil: DateUtil): JSONObject =
JSONObject()
.put("eventType", if (type == Bolus.Type.SMB) TherapyEvent.Type.CORRECTION_BOLUS.text else TherapyEvent.Type.MEAL_BOLUS.text)

View file

@ -0,0 +1,9 @@
package info.nightscout.androidaps.interfaces
interface Autotune {
fun aapsAutotune(daysBack: Int, autoSwitch: Boolean, profileToTune: String = ""): String
fun atLog(message: String)
var lastRunSuccess: Boolean
}

View file

@ -148,6 +148,10 @@ interface Profile {
return (passed / 1000).toInt()
}
fun milliSecFromMidnight(date: Long): Long {
val passed = DateTime(date).millisOfDay.toLong()
return passed
}
/*
* Units conversion
*/

View file

@ -285,6 +285,7 @@ class Translator @Inject internal constructor(
Sources.Aaps -> TODO()
*/
Sources.Automation -> rh.gs(R.string.automation)
Sources.Autotune -> rh.gs(R.string.autotune)
Sources.Loop -> rh.gs(R.string.loop)
Sources.NSClient -> rh.gs(R.string.ns)
Sources.Pump -> rh.gs(R.string.pump)

View file

@ -108,6 +108,7 @@ class UserEntryMapper {
Announcement (UserEntry.Sources.Announcement),
Actions (UserEntry.Sources.Actions),
Automation (UserEntry.Sources.Automation),
Autotune (UserEntry.Sources.Autotune),
BG (UserEntry.Sources.BG),
Aidex (UserEntry.Sources.Aidex),
Dexcom (UserEntry.Sources.Dexcom),

View file

@ -62,6 +62,7 @@ class UserEntryPresentationHelper @Inject constructor(
Sources.Announcement -> R.drawable.ic_cp_announcement
Sources.Actions -> R.drawable.ic_action
Sources.Automation -> R.drawable.ic_automation
Sources.Autotune -> R.drawable.ic_autotune
Sources.BG -> R.drawable.ic_generic_cgm
Sources.Aidex -> R.drawable.ic_blooddrop_48
Sources.Dexcom -> R.drawable.ic_dexcom_g6

View file

@ -68,6 +68,7 @@
<string name="key_insulin_oref_peak" translatable="false">insulin_oref_peak</string>
<string name="key_autotune_auto" translatable="false">autotune_auto</string>
<string name="key_autotune_categorize_uam_as_basal" translatable="false">categorize_uam_as_basal</string>
<string name="key_autotune_tune_insulin_curve" translatable="false">autotune_tune_insulin_curve</string>
<string name="key_autotune_default_tune_days" translatable="false">autotune_default_tune_days</string>
<string name="key_autotune_circadian_ic_isf" translatable="false">autotune_circadian_ic_isf</string>
<string name="key_autotune_additional_log" translatable="false">autotune_additional_log</string>
@ -554,12 +555,14 @@
<string name="autotune_auto_summary">If enabled, Autotune will automatically update and switch to input profile after calculation from an automation rule.</string>
<string name="autotune_categorize_uam_as_basal_title">Categorize UAM as basal</string>
<string name="autotune_categorize_uam_as_basal_summary">Enable only if you have reliably entered all carbs eaten, with this option sudden rises seen by Autotune will be used to recommend changes to the basal rate.</string>
<string name="autotune_tune_insulin_curve_title">Tune insulin curve</string>
<string name="autotune_tune_insulin_curve_summary">Enable only if you use free peak. This option will tune peak and DIA durations</string>
<string name="autotune_default_tune_days_title">Number of days of data</string>
<string name="autotune_circadian_ic_isf_title">Apply average result in circadian IC/ISF</string>
<string name="autotune_circadian_ic_isf_summary">Autotune will not tune circadian variations, this option only apply the average tuning of IC and ISF to your circadian input profile</string>
<string name="autotune_additional_log_title">Include more log information for debugging</string>
<string name="autotune_additional_log_summary">Switch on only if requested by dev to send more log information to help debugging Autotune plugin</string>
<string name="autotune_default_tune_days_summary">Default number of days of data to be processed by Autotune (up to xx)</string>
<string name="autotune_default_tune_days_summary">Default number of days of data to be processed by Autotune (up to 30)</string>
<string name="autotune_tunedprofile_name">Tuned</string>
<string name="autotune_profile">Profile :</string>
<string name="autotune_tune_days">Tune days :</string>
@ -568,7 +571,7 @@
<string name="autotune_select_profile">Select profile to tune</string>
<string name="autotune_ic_warning">Autotune works with only one IC value, your profile has %1$d values. Average value is %2$.2fg/U</string>
<string name="autotune_isf_warning">Autotune works with only one ISF value, your profile has %1$d values. Average value is %2$.1f%3$s/U</string>
<string name="autotune_error">Error in input data, try to reduce the number of days</string>
<string name="autotune_error">Error in input data, try to run again autotune or reduce the number of days</string>
<string name="autotune_warning_during_run">Autotune calculation started, please be patient</string>
<string name="autotune_warning_after_run">Check the results carefully before using it!</string>
<string name="autotune_partial_result">Partial result day %1$d / %2$d tuned</string>
@ -589,8 +592,9 @@
<string name="autotune_profile_invalid">Profile invalid</string>
<string name="autotune_log_title" translatable="false">|Param|Profile|Tuned|%/Miss.\n</string>
<string name="autotune_log_separator" translatable="false">+------------------------------------------\n</string>
<string name="autotune_log_isf" translatable="false">| %1$4.4s |\t%2$3.3f |\t%3$3.3f |\n</string>
<string name="autotune_log_ic" translatable="false">| %1$4.4s |\t%2$3.3f |\t%3$3.3f |\n</string>
<string name="autotune_log_peak" translatable="false">| %1$4.4s |\t%2$d |\t%3$d |\n</string>
<string name="autotune_log_dia" translatable="false">| %1$4.4s |\t%2$3.1f |\t%3$3.1f |\n</string>
<string name="autotune_log_ic_isf" translatable="false">| %1$4.4s | %2$3.3f |\t%3$3.3f |\n</string>
<string name="autotune_log_basal" translatable="false">|\t%1$02.0f\t| %2$3.3f |%3$3.3f\t| %5$.0f%% / %4$d\n</string>
<string name="autotune_log_sum_basal" translatable="false">|\t∑\t|\t%1$3.1f |\t%2$3.1f |\n</string>
<string name="autotune_run_without_autoswitch">Autotune runned without profile switch</string>

View file

@ -138,6 +138,7 @@ data class UserEntry(
Announcement,
Actions, //From Actions plugin
Automation, //From Automation plugin
Autotune, //From Autotune plugin
BG, //From BG plugin => Add One Source per BG Source for Calibration or Sensor Change
Aidex,
Dexcom,

View file

@ -5,6 +5,7 @@ enum class LTag(val tag: String, val defaultValue : Boolean = true, val requires
APS("APS"),
AUTOSENS("AUTOSENS", defaultValue = false),
AUTOMATION("AUTOMATION"),
AUTOTUNE("AUTOTUNE", defaultValue = false),
BGSOURCE("BGSOURCE"),
CONFIGBUILDER("CONFIGBUILDER"),
CONSTRAINTS("CONSTRAINTS"),