|
|
@@ -4,11 +4,15 @@ import Foundation
|
|
|
enum ForecastGenerator {
|
|
|
public static func generate(
|
|
|
glucose: Decimal,
|
|
|
+ glucoseStatus: GlucoseStatus,
|
|
|
+ currentGlucoseImpact: Decimal,
|
|
|
glucoseImpactSeries: [Decimal],
|
|
|
glucoseImpactSeriesWithZeroTemp: [Decimal],
|
|
|
iobData _: [IobResult],
|
|
|
mealData: ComputedCarbs,
|
|
|
profile: Profile,
|
|
|
+ trioCustomOrefVariables: TrioCustomOrefVariables,
|
|
|
+ targetGlucose: Decimal,
|
|
|
adjustedSensitivity: Decimal,
|
|
|
sensitivityRatio: Decimal,
|
|
|
naiveEventualGlucose _: Decimal,
|
|
|
@@ -16,42 +20,63 @@ enum ForecastGenerator {
|
|
|
threshold: Decimal,
|
|
|
currentTime: Date
|
|
|
) -> ForecastResult {
|
|
|
- let currentDeviation = mealData.currentDeviation ?? 0
|
|
|
- let carbImpact = currentDeviation * (profile.carbRatio ?? profile.carbRatioFor(time: currentTime)) /
|
|
|
- (profile.sens ?? profile.sensitivityFor(time: currentTime))
|
|
|
- let deviation = currentDeviation
|
|
|
+ let profileCarbRatio = profile.carbRatio ?? profile.carbRatioFor(time: currentTime)
|
|
|
+ let adjustedCarbRatio: Decimal
|
|
|
+ if trioCustomOrefVariables.useOverride, trioCustomOrefVariables.cr {
|
|
|
+ let overrideFactor = trioCustomOrefVariables.overridePercentage / 100
|
|
|
+ adjustedCarbRatio = profileCarbRatio / overrideFactor
|
|
|
+ } else {
|
|
|
+ adjustedCarbRatio = profileCarbRatio
|
|
|
+ }
|
|
|
+
|
|
|
+ let carbSensitivityFactor = adjustedSensitivity / adjustedCarbRatio
|
|
|
+ let minDelta = min(glucoseStatus.delta, glucoseStatus.shortAvgDelta)
|
|
|
+ // this carbImpact is `ci` in JS
|
|
|
+ var carbImpact = (minDelta - currentGlucoseImpact).jsRounded(scale: 1)
|
|
|
+ let maxCarbAbsorptionRate = Decimal(30)
|
|
|
+ let maxCI = (maxCarbAbsorptionRate * carbSensitivityFactor * Decimal(5) / Decimal(60)).jsRounded(scale: 1)
|
|
|
+ if carbImpact > maxCI {
|
|
|
+ carbImpact = maxCI
|
|
|
+ }
|
|
|
+
|
|
|
+ let carbImpactParams = CarbImpactParams.calculate(
|
|
|
+ carbSensitivityFactor: carbSensitivityFactor,
|
|
|
+ profile: profile,
|
|
|
+ mealData: mealData,
|
|
|
+ carbImpact: carbImpact,
|
|
|
+ sensitivityRatio: sensitivityRatio,
|
|
|
+ currentTime: currentTime
|
|
|
+ )
|
|
|
+
|
|
|
+ // this is `uci` in JS, it isn't limited by maxCI
|
|
|
+ let uamCarbImpact = (minDelta - currentGlucoseImpact).jsRounded(scale: 1)
|
|
|
|
|
|
// JS oref initializes all xxxPredBGs array with current glucose, we do the same, then generate
|
|
|
- let iobForecast = [glucose] + forecastIOB(
|
|
|
+ let iobForecast = forecastIOB(
|
|
|
startingGlucose: glucose,
|
|
|
glucoseImpactSeries: glucoseImpactSeries,
|
|
|
- deviation: deviation,
|
|
|
+ carbImpact: carbImpact,
|
|
|
)
|
|
|
|
|
|
- let cobForecast = [glucose] + forecastCOB(
|
|
|
+ let cobForecast = forecastCOB(
|
|
|
startingGlucose: glucose,
|
|
|
glucoseImpactSeries: glucoseImpactSeries,
|
|
|
- mealData: mealData,
|
|
|
- profile: profile,
|
|
|
carbImpact: carbImpact,
|
|
|
- deviation: deviation,
|
|
|
- adjustedSensitivity: adjustedSensitivity,
|
|
|
- sensitivityRatio: sensitivityRatio,
|
|
|
- currentTime: currentTime
|
|
|
+ carbImpactParams: carbImpactParams
|
|
|
)
|
|
|
|
|
|
- let uamForecast = [glucose] + forecastUAM(
|
|
|
+ let uamForecast = forecastUAM(
|
|
|
startingGlucose: glucose,
|
|
|
glucoseImpactSeries: glucoseImpactSeries,
|
|
|
mealData: mealData,
|
|
|
- carbImpact: carbImpact,
|
|
|
- deviation: deviation
|
|
|
+ uamCarbImpact: uamCarbImpact,
|
|
|
+ carbImpact: carbImpact
|
|
|
)
|
|
|
|
|
|
- let ztForecast = [glucose] + forecastZT(
|
|
|
+ let ztForecast = forecastZT(
|
|
|
startingGlucose: glucose,
|
|
|
glucoseImpactSeriesWithZeroTemp: glucoseImpactSeriesWithZeroTemp,
|
|
|
- deviation: deviation
|
|
|
+ targetBG: targetGlucose
|
|
|
)
|
|
|
|
|
|
let computedForecastSelection = Self.computeForecastSelection(
|
|
|
@@ -62,26 +87,12 @@ enum ForecastGenerator {
|
|
|
currentGlucose: glucose
|
|
|
)
|
|
|
|
|
|
- let carbImpactParams = CarbImpactParams.calculate(
|
|
|
- adjustedSensitivity: adjustedSensitivity,
|
|
|
- profile: profile,
|
|
|
- mealData: mealData,
|
|
|
- carbImpact: carbImpact,
|
|
|
- sensitivityRatio: sensitivityRatio,
|
|
|
- currentTime: currentTime
|
|
|
- )
|
|
|
-
|
|
|
- let carbImpactDuration = carbImpact > 0 ? min(
|
|
|
- carbImpactParams.remainingCarbAbsorptionTime * 60 / 5 / 2,
|
|
|
- max(0, mealData.mealCOB * carbImpactParams.carbSensivityFactor / carbImpact)
|
|
|
- ) : 0
|
|
|
-
|
|
|
let blendedForecasts = Self.blendForecasts(
|
|
|
selectionResult: computedForecastSelection,
|
|
|
carbs: mealData.carbs,
|
|
|
mealCOB: mealData.mealCOB,
|
|
|
enableUAM: profile.enableUAM,
|
|
|
- carbImpactDuration: carbImpactDuration,
|
|
|
+ carbImpactDuration: carbImpactParams.carbImpactDuration,
|
|
|
remainingCarbImpactPeak: carbImpactParams.remainingCarbImpactPeak,
|
|
|
fractionCarbsLeft: mealData.carbs > 0 ? mealData.mealCOB / mealData.carbs : 0,
|
|
|
threshold: threshold,
|
|
|
@@ -89,6 +100,17 @@ enum ForecastGenerator {
|
|
|
currentGlucose: glucose
|
|
|
)
|
|
|
|
|
|
+ // FIXME: Revisit this after I get predBG working
|
|
|
+ /*
|
|
|
+ var eventualGlucose = eventualGlucose
|
|
|
+ if let finalCOBGlucose = cobForecast.last {
|
|
|
+ eventualGlucose = max(eventualGlucose, finalCOBGlucose)
|
|
|
+ }
|
|
|
+ if let finalUAMGlucose = uamForecast.last {
|
|
|
+ eventualGlucose = max(eventualGlucose, finalUAMGlucose)
|
|
|
+ }
|
|
|
+ */
|
|
|
+
|
|
|
return ForecastResult(
|
|
|
iob: iobForecast,
|
|
|
cob: cobForecast,
|
|
|
@@ -109,25 +131,28 @@ enum ForecastGenerator {
|
|
|
/// - Returns: Remaining CA time in hours (Decimal)
|
|
|
static func calculateRemainingCarbAbsorptionTime(
|
|
|
sensitivityRatio: Decimal,
|
|
|
- maxMealAbsorptionTime: Decimal,
|
|
|
+ maxMealAbsorptionTime _: Decimal,
|
|
|
mealCOB: Decimal,
|
|
|
lastCarbTime: Date?,
|
|
|
currentTime: Date
|
|
|
) -> Decimal {
|
|
|
- var minRemainingCarbAbsorptionTime: Decimal = min(3, maxMealAbsorptionTime) // hours
|
|
|
+ var minRemainingCarbAbsorptionTime: Decimal = 3 // hours
|
|
|
if sensitivityRatio > 0 {
|
|
|
minRemainingCarbAbsorptionTime = minRemainingCarbAbsorptionTime / sensitivityRatio
|
|
|
}
|
|
|
+
|
|
|
+ var remainingCarbAbsorptionTime = minRemainingCarbAbsorptionTime
|
|
|
if mealCOB > 0 {
|
|
|
let assumedCarbAbsorptionRate: Decimal = 20 // g/h
|
|
|
minRemainingCarbAbsorptionTime = max(minRemainingCarbAbsorptionTime, mealCOB / assumedCarbAbsorptionRate)
|
|
|
+ if let lastCarbTime = lastCarbTime {
|
|
|
+ let lastCarbAgeMin = Decimal(currentTime.timeIntervalSince(lastCarbTime) / 60).jsRounded()
|
|
|
+ remainingCarbAbsorptionTime = minRemainingCarbAbsorptionTime + 1.5 * (lastCarbAgeMin / 60)
|
|
|
+ remainingCarbAbsorptionTime = remainingCarbAbsorptionTime.jsRounded(scale: 1)
|
|
|
+ }
|
|
|
}
|
|
|
- var remainingCarbAbsorptionTime = minRemainingCarbAbsorptionTime
|
|
|
- if let lastCarbTime = lastCarbTime {
|
|
|
- let lastCarbAgeMin = Decimal(currentTime.timeIntervalSince(lastCarbTime) / 60)
|
|
|
- remainingCarbAbsorptionTime += 1.5 * (lastCarbAgeMin / 60)
|
|
|
- }
|
|
|
- return remainingCarbAbsorptionTime.rounded(toPlaces: 1)
|
|
|
+
|
|
|
+ return remainingCarbAbsorptionTime
|
|
|
}
|
|
|
|
|
|
static func computeForecastSelection(
|
|
|
@@ -284,7 +309,7 @@ enum ForecastGenerator {
|
|
|
return series
|
|
|
}
|
|
|
let maxToRemove = series.count - lookback
|
|
|
- let reversedSeries = series.reversed()
|
|
|
+ let reversedSeries = series.map({ $0.jsRounded() }).reversed()
|
|
|
var removeCount = 0
|
|
|
for (curr, next) in zip(reversedSeries, reversedSeries.dropFirst()) {
|
|
|
guard curr == next else {
|
|
|
@@ -297,4 +322,26 @@ enum ForecastGenerator {
|
|
|
|
|
|
return Array(series.dropLast(removeCount))
|
|
|
}
|
|
|
+
|
|
|
+ /// Trims trailing ZT points once they are rising and above target
|
|
|
+ public static func trimZTTails(series: [Decimal], targetBG: Decimal) -> [Decimal] {
|
|
|
+ let lookback = 7 // i > 6 in JS
|
|
|
+
|
|
|
+ guard series.count > lookback else {
|
|
|
+ return series
|
|
|
+ }
|
|
|
+ let maxToRemove = series.count - lookback
|
|
|
+ let reversedSeries = series.map({ $0.jsRounded() }).reversed()
|
|
|
+ var removeCount = 0
|
|
|
+ for (curr, next) in zip(reversedSeries, reversedSeries.dropFirst()) {
|
|
|
+ if next >= curr || curr <= targetBG {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ removeCount += 1
|
|
|
+ }
|
|
|
+
|
|
|
+ removeCount = min(maxToRemove, removeCount)
|
|
|
+
|
|
|
+ return Array(series.dropLast(removeCount))
|
|
|
+ }
|
|
|
}
|