|
|
@@ -6,141 +6,196 @@ extension ForecastGenerator {
|
|
|
static func forecastIOB(
|
|
|
startingGlucose: Decimal,
|
|
|
glucoseImpactSeries: [Decimal],
|
|
|
- deviation: Decimal,
|
|
|
+ iobData: [IobResult],
|
|
|
+ carbImpact: Decimal,
|
|
|
+ dynamicIsfState: DynamicIsfState,
|
|
|
+ insulinFactor: Decimal?,
|
|
|
+ tdd: Decimal,
|
|
|
+ adjustmentFactorLogrithmic: Decimal
|
|
|
) -> [Decimal] {
|
|
|
var result = [startingGlucose]
|
|
|
- for (count, glucoseImpact) in glucoseImpactSeries.enumerated() {
|
|
|
- let forecastedDeviation = deviation * (1 - min(1, Decimal(count) / (60 / 5)))
|
|
|
- let next = result.last! + glucoseImpact + forecastedDeviation
|
|
|
- result.append(next.clamp(lowerBound: 39, upperBound: 401))
|
|
|
+ for (glucoseImpact, iob) in zip(glucoseImpactSeries, iobData) {
|
|
|
+ let forecastedDeviation = carbImpact * (1 - min(1, Decimal(result.count) / (60 / 5)))
|
|
|
+ let lastForecast = result.last!
|
|
|
+ let next: Decimal
|
|
|
+ if let insulinFactor = insulinFactor, dynamicIsfState == .logrithmic {
|
|
|
+ let adjustedGlucoseImpact = adjustedGlucoseImpactForLogrithmicDynamicIsf(
|
|
|
+ lastForecast: lastForecast,
|
|
|
+ insulinFactor: insulinFactor,
|
|
|
+ tdd: tdd,
|
|
|
+ adjustmentFactorLogrithmic: adjustmentFactorLogrithmic,
|
|
|
+ iobActivity: iob.activity
|
|
|
+ )
|
|
|
+ next = lastForecast + adjustedGlucoseImpact + forecastedDeviation
|
|
|
+ } else {
|
|
|
+ next = lastForecast + glucoseImpact.jsRounded(scale: 2) + forecastedDeviation
|
|
|
+ }
|
|
|
+ if result.count < 48 { result.append(next) }
|
|
|
}
|
|
|
- return ForecastGenerator.trimFlatTails(result, lookback: 90 / 5)
|
|
|
+ let clampedResult = result.map { $0.clamp(lowerBound: 39, upperBound: 401) }
|
|
|
+ return ForecastGenerator.trimFlatTails(clampedResult, lookback: 13)
|
|
|
}
|
|
|
|
|
|
static func forecastCOB(
|
|
|
startingGlucose: Decimal,
|
|
|
glucoseImpactSeries: [Decimal],
|
|
|
- mealData: ComputedCarbs,
|
|
|
- profile: Profile,
|
|
|
carbImpact: Decimal,
|
|
|
- deviation: Decimal,
|
|
|
- adjustedSensitivity: Decimal,
|
|
|
- sensitivityRatio: Decimal,
|
|
|
- currentTime: Date
|
|
|
+ carbImpactParams: CarbImpactParams
|
|
|
) -> [Decimal] {
|
|
|
// Start with the current BG
|
|
|
var result = [startingGlucose]
|
|
|
|
|
|
- let carbImpactParams = CarbImpactParams.calculate(
|
|
|
- adjustedSensitivity: adjustedSensitivity,
|
|
|
- profile: profile,
|
|
|
- mealData: mealData,
|
|
|
- carbImpact: carbImpact,
|
|
|
- sensitivityRatio: sensitivityRatio,
|
|
|
- currentTime: currentTime
|
|
|
- )
|
|
|
-
|
|
|
- // How many intervals we spread the initial CI decay over?
|
|
|
- // We use twice the absorption window (so that by 2x the window, CI has decayed to zero).
|
|
|
- let decayIntervals = max(carbImpactParams.maxAbsorptionIntervals * 2, 1)
|
|
|
-
|
|
|
- // Helper: negative deviation only (never positive)
|
|
|
- let forecastedDeviation = min(0, deviation)
|
|
|
-
|
|
|
// Build forecast out to glucoseImpactSeries.count (usually 48)
|
|
|
- for seriesCount in 1 ..< glucoseImpactSeries.count {
|
|
|
- let insulinEffect = glucoseImpactSeries[seriesCount]
|
|
|
+ for glucoseImpact in glucoseImpactSeries {
|
|
|
+ let forecastedDeviation = carbImpact * (1 - min(1, Decimal(result.count) / (60 / 5)))
|
|
|
|
|
|
// Linearly decay the *observed* carb impact from initialCI → 0
|
|
|
- let decayFactor = max(0, 1 - seriesCount / decayIntervals)
|
|
|
- let forecastedCarbImpact = carbImpactParams.cappedCarbImpact * Decimal(decayFactor)
|
|
|
+ // var predCI = Math.max(0, Math.max(0,ci) * ( 1 - COBpredBGs.length/Math.max(cid*2,1) ) );
|
|
|
+ let decayFactor = max(0, 1 - Decimal(result.count) / max(carbImpactParams.carbImpactDuration * 2, Decimal(1)))
|
|
|
+ let forecastedCarbImpact = max(0, max(0, carbImpact) * decayFactor)
|
|
|
|
|
|
// Add a simple triangle bump for remaining carbs:
|
|
|
// – ramp up linearly to peak over the first half of the window,
|
|
|
// – ramp down linearly over the second half,
|
|
|
// – zero afterwards.
|
|
|
- let triangle: Decimal
|
|
|
- if carbImpactParams.triangleIntervals > 0, seriesCount <= carbImpactParams.triangleIntervals {
|
|
|
- // FIXME: integer division here might be slightly off for odd number intervals.
|
|
|
- // FIXME: For perfect symmetry we could use let halfTriangle = (triangleIntervals + 1) / 2 — Change this?!
|
|
|
- let halfTriangle = carbImpactParams.triangleIntervals / 2
|
|
|
- if seriesCount <= halfTriangle {
|
|
|
- // Ramp up
|
|
|
- triangle = carbImpactParams.remainingCarbImpactPeak * Decimal(seriesCount) / Decimal(halfTriangle)
|
|
|
- } else {
|
|
|
- // Ramp down
|
|
|
- triangle = carbImpactParams
|
|
|
- .remainingCarbImpactPeak * Decimal(carbImpactParams.triangleIntervals - seriesCount) /
|
|
|
- Decimal(halfTriangle)
|
|
|
- }
|
|
|
- } else {
|
|
|
- triangle = 0
|
|
|
- }
|
|
|
+
|
|
|
+ // var intervals = Math.min( COBpredBGs.length, (remainingCATime*12)-COBpredBGs.length );
|
|
|
+ // var remainingCI = Math.max(0, intervals / (remainingCATime/2*12) * remainingCIpeak );
|
|
|
+ let intervals = min(Decimal(result.count), carbImpactParams.remainingCarbAbsorptionTime * 12 - Decimal(result.count))
|
|
|
+ let triangle = max(
|
|
|
+ 0,
|
|
|
+ intervals / (carbImpactParams.remainingCarbAbsorptionTime / 2 * 12) * carbImpactParams.remainingCarbImpactPeak
|
|
|
+ )
|
|
|
|
|
|
let next = result.last!
|
|
|
- + insulinEffect
|
|
|
- + forecastedDeviation
|
|
|
+ + glucoseImpact.jsRounded(scale: 2)
|
|
|
+ + min(0, forecastedDeviation)
|
|
|
+ forecastedCarbImpact
|
|
|
+ triangle
|
|
|
|
|
|
- result.append(next.clamp(lowerBound: 39, upperBound: 1500))
|
|
|
+ if result.count < 48 { result.append(next) }
|
|
|
}
|
|
|
|
|
|
- return ForecastGenerator.trimFlatTails(result, lookback: 12)
|
|
|
+ let clampedResult = result.map { $0.clamp(lowerBound: 39, upperBound: 1500) }
|
|
|
+ return ForecastGenerator.trimFlatTails(clampedResult, lookback: 13)
|
|
|
}
|
|
|
|
|
|
static func forecastUAM(
|
|
|
startingGlucose: Decimal,
|
|
|
glucoseImpactSeries: [Decimal],
|
|
|
mealData: ComputedCarbs,
|
|
|
+ uamCarbImpact: Decimal,
|
|
|
carbImpact: Decimal,
|
|
|
- deviation: Decimal
|
|
|
+ iobData: [IobResult],
|
|
|
+ dynamicIsfState: DynamicIsfState,
|
|
|
+ insulinFactor: Decimal?,
|
|
|
+ tdd: Decimal,
|
|
|
+ adjustmentFactorLogrithmic: Decimal
|
|
|
) -> [Decimal] {
|
|
|
var result = [startingGlucose]
|
|
|
|
|
|
- let slopeFromDeviations = mealData.slopeFromMinDeviation
|
|
|
+ let slopeFromDeviations = min(
|
|
|
+ mealData.slopeFromMaxDeviation.jsRounded(scale: 2),
|
|
|
+ -mealData.slopeFromMinDeviation.jsRounded(scale: 2) / 3
|
|
|
+ )
|
|
|
let ticksInThreeHours: Decimal = 36 // 3 * 60 / 5
|
|
|
|
|
|
- let unannouncedCarbImpact = carbImpact
|
|
|
+ let unannouncedCarbImpact = uamCarbImpact
|
|
|
|
|
|
- for glucoseImpact in 1 ..< glucoseImpactSeries.count {
|
|
|
- let insulinEffect = glucoseImpactSeries[glucoseImpact]
|
|
|
- let forecastedDeviaton = min(0, deviation)
|
|
|
+ for (glucoseImpact, iob) in zip(glucoseImpactSeries, iobData) {
|
|
|
+ let forecastedDeviation = carbImpact * (1 - min(1, Decimal(result.count) / (60 / 5)))
|
|
|
|
|
|
// In JS: predUCIslope = max(0, uci + (tick * slopeFromDeviations))
|
|
|
let forecastedUnannouncedCarbImpactSlope = max(
|
|
|
0,
|
|
|
- unannouncedCarbImpact + Decimal(glucoseImpact) * slopeFromDeviations
|
|
|
+ unannouncedCarbImpact + Decimal(result.count) * slopeFromDeviations
|
|
|
)
|
|
|
|
|
|
// In JS: predUCImax = max(0, uci * (1 - tick / ticksInThreeHours))
|
|
|
let maxForecastedUnannouncedCarbImpact = max(
|
|
|
0,
|
|
|
- unannouncedCarbImpact * (1 - Decimal(glucoseImpact) / ticksInThreeHours)
|
|
|
+ unannouncedCarbImpact * (1 - Decimal(result.count) / ticksInThreeHours)
|
|
|
)
|
|
|
let forecastedUnannouncedCarbImpact = min(
|
|
|
forecastedUnannouncedCarbImpactSlope,
|
|
|
maxForecastedUnannouncedCarbImpact
|
|
|
)
|
|
|
|
|
|
- let next = result.last! + insulinEffect + forecastedDeviaton + forecastedUnannouncedCarbImpact
|
|
|
+ let lastForecast = result.last!
|
|
|
+ let next: Decimal
|
|
|
+ if let insulinFactor = insulinFactor, dynamicIsfState == .logrithmic {
|
|
|
+ let adjustedGlucoseImpact = adjustedGlucoseImpactForLogrithmicDynamicIsf(
|
|
|
+ lastForecast: lastForecast,
|
|
|
+ insulinFactor: insulinFactor,
|
|
|
+ tdd: tdd,
|
|
|
+ adjustmentFactorLogrithmic: adjustmentFactorLogrithmic,
|
|
|
+ iobActivity: iob.activity
|
|
|
+ )
|
|
|
+ next = lastForecast + adjustedGlucoseImpact + min(0, forecastedDeviation) + forecastedUnannouncedCarbImpact
|
|
|
+ } else {
|
|
|
+ next = lastForecast + glucoseImpact
|
|
|
+ .jsRounded(scale: 2) + min(0, forecastedDeviation) + forecastedUnannouncedCarbImpact
|
|
|
+ }
|
|
|
|
|
|
- result.append(next.clamp(lowerBound: 39, upperBound: 401))
|
|
|
+ if result.count < 48 { result.append(next) }
|
|
|
}
|
|
|
|
|
|
- return ForecastGenerator.trimFlatTails(result, lookback: 12)
|
|
|
+ let clampedResult = result.map { $0.clamp(lowerBound: 39, upperBound: 401) }
|
|
|
+ return ForecastGenerator.trimFlatTails(clampedResult, lookback: 13)
|
|
|
}
|
|
|
|
|
|
static func forecastZT(
|
|
|
startingGlucose: Decimal,
|
|
|
glucoseImpactSeriesWithZeroTemp: [Decimal],
|
|
|
- deviation: Decimal
|
|
|
+ targetBG: Decimal,
|
|
|
+ iobData: [IobResult],
|
|
|
+ dynamicIsfState: DynamicIsfState,
|
|
|
+ insulinFactor: Decimal?,
|
|
|
+ tdd: Decimal,
|
|
|
+ adjustmentFactorLogrithmic: Decimal
|
|
|
) -> [Decimal] {
|
|
|
- // essentially insulin effect only, but with zero-temp ISF if needed
|
|
|
- Self.forecastIOB(
|
|
|
- startingGlucose: startingGlucose,
|
|
|
- glucoseImpactSeries: glucoseImpactSeriesWithZeroTemp,
|
|
|
- deviation: deviation
|
|
|
- )
|
|
|
+ var result = [startingGlucose]
|
|
|
+ // Potential bug: ZT doesn't use forecastedDeviation like IoB does
|
|
|
+ for (glucoseImpact, iob) in zip(glucoseImpactSeriesWithZeroTemp, iobData) {
|
|
|
+ let lastForecast = result.last!
|
|
|
+ let next: Decimal
|
|
|
+ if let insulinFactor = insulinFactor, dynamicIsfState == .logrithmic {
|
|
|
+ let adjustedGlucoseImpact = adjustedGlucoseImpactForLogrithmicDynamicIsf(
|
|
|
+ lastForecast: lastForecast,
|
|
|
+ insulinFactor: insulinFactor,
|
|
|
+ tdd: tdd,
|
|
|
+ adjustmentFactorLogrithmic: adjustmentFactorLogrithmic,
|
|
|
+ iobActivity: iob.iobWithZeroTemp.activity
|
|
|
+ )
|
|
|
+ next = lastForecast + adjustedGlucoseImpact
|
|
|
+ } else {
|
|
|
+ next = lastForecast + glucoseImpact.jsRounded(scale: 2)
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.count < 48 { result.append(next) }
|
|
|
+ }
|
|
|
+ let clampedResult = result.map { $0.clamp(lowerBound: 39, upperBound: 401) }
|
|
|
+ return ForecastGenerator.trimZTTails(series: clampedResult, targetBG: targetBG)
|
|
|
+ }
|
|
|
+
|
|
|
+ static func adjustedGlucoseImpactForLogrithmicDynamicIsf(
|
|
|
+ lastForecast: Decimal,
|
|
|
+ insulinFactor: Decimal,
|
|
|
+ tdd: Decimal,
|
|
|
+ adjustmentFactorLogrithmic: Decimal,
|
|
|
+ iobActivity: Decimal
|
|
|
+ ) -> Decimal {
|
|
|
+ // The JS code is extremely difficult to understand, so I tried
|
|
|
+ // to break down the components with the JS snippet listed above
|
|
|
+
|
|
|
+ // (Math.max( ZTpredBGs[ZTpredBGs.length-1],39) / insulinFactor )
|
|
|
+ let adjustedLastForecast = max(lastForecast, 39) / insulinFactor
|
|
|
+ // ( tdd * adjustmentFactor * (Math.log(adjustedLastForecast + 1 ) ) )
|
|
|
+ let adjustedTdd = tdd * adjustmentFactorLogrithmic * Decimal.log(adjustedLastForecast + 1)
|
|
|
+ // For ZT:
|
|
|
+ // round(( -iobTick.iobWithZeroTemp.activity * (1800 / adjustedTdd) * 5 ),2)
|
|
|
+ // For UAM and IOB:
|
|
|
+ // round(( -iobTick.activity * (1800 / adjustedTdd) * 5 ),2)
|
|
|
+ return (-iobActivity * (1800 / adjustedTdd) * 5).jsRounded(scale: 2)
|
|
|
}
|
|
|
}
|