Procházet zdrojové kódy

Refactor ForecastGenerator to object-less enum; fix bug for ZT forecast WIP

Deniz Cengiz před 11 měsíci
rodič
revize
b1c8613439

+ 4 - 4
Trio.xcodeproj/project.pbxproj

@@ -633,7 +633,7 @@
 		DD30B9C72E06257900DA677C /* DetermineBasalGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9C62E06257300DA677C /* DetermineBasalGenerator.swift */; };
 		DD30B9CA2E062A3400DA677C /* ForecastGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9C92E062A3300DA677C /* ForecastGenerator.swift */; };
 		DD30B9CC2E062A7000DA677C /* ForecastResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9CB2E062A7000DA677C /* ForecastResult.swift */; };
-		DD30B9CE2E062AA300DA677C /* SingleForecasting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9CD2E062AA300DA677C /* SingleForecasting.swift */; };
+		DD30B9CE2E062AA300DA677C /* ForecastGenerator+Forecasts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9CD2E062AA300DA677C /* ForecastGenerator+Forecasts.swift */; };
 		DD30B9FE2E0742E200DA677C /* DetermineBasalSMBEnablementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9FD2E0742E200DA677C /* DetermineBasalSMBEnablementTests.swift */; };
 		DD30BA002E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30B9FF2E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift */; };
 		DD30BA022E074F0F00DA677C /* GlucoseStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD30BA012E074F0F00DA677C /* GlucoseStatus.swift */; };
@@ -1549,7 +1549,7 @@
 		DD30B9C62E06257300DA677C /* DetermineBasalGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalGenerator.swift; sourceTree = "<group>"; };
 		DD30B9C92E062A3300DA677C /* ForecastGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastGenerator.swift; sourceTree = "<group>"; };
 		DD30B9CB2E062A7000DA677C /* ForecastResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastResult.swift; sourceTree = "<group>"; };
-		DD30B9CD2E062AA300DA677C /* SingleForecasting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleForecasting.swift; sourceTree = "<group>"; };
+		DD30B9CD2E062AA300DA677C /* ForecastGenerator+Forecasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ForecastGenerator+Forecasts.swift"; sourceTree = "<group>"; };
 		DD30B9FD2E0742E200DA677C /* DetermineBasalSMBEnablementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalSMBEnablementTests.swift; sourceTree = "<group>"; };
 		DD30B9FF2E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalDeltaCalculationTests.swift; sourceTree = "<group>"; };
 		DD30BA012E074F0F00DA677C /* GlucoseStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatus.swift; sourceTree = "<group>"; };
@@ -3718,7 +3718,7 @@
 			isa = PBXGroup;
 			children = (
 				DD30BA192E08AB9F00DA677C /* CarbImpactParams.swift */,
-				DD30B9CD2E062AA300DA677C /* SingleForecasting.swift */,
+				DD30B9CD2E062AA300DA677C /* ForecastGenerator+Forecasts.swift */,
 				DD30B9C92E062A3300DA677C /* ForecastGenerator.swift */,
 			);
 			path = Forecasts;
@@ -4799,7 +4799,7 @@
 				DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
-				DD30B9CE2E062AA300DA677C /* SingleForecasting.swift in Sources */,
+				DD30B9CE2E062AA300DA677C /* ForecastGenerator+Forecasts.swift in Sources */,
 				BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */,
 				BD7DB88E2D2C4A17003D3155 /* BolusCalculationManager.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,

+ 10 - 11
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasal+Dosing.swift

@@ -16,17 +16,16 @@ extension DeterminationGenerator {
     }
 
     static func determineDosing(
-        profile: Profile,
-        currentTemp: TempBasal,
-        iobData: IobResult,
-        mealData: ComputedCarbs,
-        autosensData: Autosens,
-        forecastResult: ForecastResult,
-        glucoseStatus: GlucoseStatus,
-        enableSMB: Bool,
-        currentTime: Date
+        profile _: Profile,
+        currentTemp _: TempBasal,
+        iobData _: IobResult,
+        mealData _: ComputedCarbs,
+        autosensData _: Autosens,
+        forecastResult _: ForecastResult,
+        glucoseStatus _: GlucoseStatus,
+        enableSMB _: Bool,
+        currentTime _: Date
     ) -> DosingMetrics? {
-        return nil
+        nil
     }
-
 }

+ 15 - 7
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasal+Helpers.swift

@@ -127,7 +127,7 @@ extension DeterminationGenerator {
         // no enable condition met → disable SMB
         return false
     }
-    
+
     static func calculateSensitivityRatio(
         profile: Profile,
         autosens: Autosens?,
@@ -238,12 +238,20 @@ extension DeterminationGenerator {
         return (AdjustedGlucoseTargets(minGlucose: minGlucose, maxGlucose: maxGlucose, targetGlucose: targetGlucose), threshold)
     }
 
-    static func buildGlucoseImpactSeries(iobDataSeries: [IobResult], sensitivity: Decimal) -> [Decimal] {
-        iobDataSeries.map { iob in
-            // FIXME: this is assuming 5min steps...
-            // Activity is U/hr
-            // oref0 uses: -activity * ISF * 5
-            -iob.activity * sensitivity * 5
+    static func buildGlucoseImpactSeries(
+        iobDataSeries: [IobResult],
+        sensitivity: Decimal,
+        withZeroTemp: Bool = false
+    ) -> [Decimal] {
+        // FIXME: this is assuming 5min steps...
+        // Activity is U/hr
+        if withZeroTemp {
+            return iobDataSeries.map { iobWithZeroTemp in
+                -iobWithZeroTemp.activity * sensitivity * 5 }
+        } else {
+            return iobDataSeries.map { iob in
+                -iob.activity * sensitivity * 5
+            }
         }
     }
 }

+ 14 - 3
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -110,6 +110,12 @@ enum DeterminationGenerator {
         )
 
         let glucoseImpactSeries = buildGlucoseImpactSeries(iobDataSeries: iobData, sensitivity: sensitivity)
+        let glucoseImpactSeriesWithZeroTemp = buildGlucoseImpactSeries(
+            iobDataSeries: iobData,
+            sensitivity: sensitivity,
+            withZeroTemp: true
+        )
+
         let currentGlucoseImpact = glucoseImpactSeries[0]
 
         let minDelta = min(glucoseStatus.delta, glucoseStatus.shortAvgDelta)
@@ -145,10 +151,10 @@ enum DeterminationGenerator {
             throw DeterminationError.eventualGlucoseCalculationError(sensitivity: sensitivity, deviation: deviation)
         }
 
-        let forecastGenerator = ForecastGenerator()
-        let forecastResult = forecastGenerator.generate(
+        let forecastResult = ForecastGenerator.generate(
             glucose: currentGlucose,
             glucoseImpactSeries: glucoseImpactSeries,
+            glucoseImpactSeriesWithZeroTemp: glucoseImpactSeriesWithZeroTemp,
             iobData: iobData,
             mealData: mealData,
             profile: profile,
@@ -181,7 +187,12 @@ enum DeterminationGenerator {
             duration: nil,
             iob: iobData.first?.iob,
             cob: mealData.mealCOB,
-            predictions: Predictions(iob: forecastResult.iob.map { Int($0) }, zt: forecastResult.zt.map { Int($0) }, cob: forecastResult.cob.map { Int($0) }, uam: forecastResult.uam.map { Int($0) } ),
+            predictions: Predictions(
+                iob: forecastResult.iob.map { Int($0) },
+                zt: forecastResult.zt.map { Int($0) },
+                cob: forecastResult.cob.map { Int($0) },
+                uam: forecastResult.uam.map { Int($0) }
+            ),
             deliverAt: currentTime,
             carbsReq: nil,
             temp: nil,

+ 1 - 1
Trio/Sources/APS/OpenAPSSwift/Forecasts/CarbImpactParams.swift

@@ -17,7 +17,7 @@ struct CarbImpactParams {
         currentTime: Date
     ) -> CarbImpactParams {
         let carbSensivityFactor = adjustedSensitivity / (profile.carbRatio ?? profile.carbRatioFor(time: currentTime))
-        
+
         // Initial carb impact in mg/dL per 5m
         let initialCarbImpact = carbImpact * carbSensivityFactor
         let maxCarbAbsorptionRate: Decimal = 30 // g/h

+ 32 - 82
Trio/Sources/APS/OpenAPSSwift/Forecasts/SingleForecasting.swift

@@ -1,42 +1,12 @@
 import Foundation
 
-/// Common interface for a single forecast pipeline
-protocol SingleForecasting {
-    /// - Parameters:
-    ///   - startingGlucose: the current glucose
-    ///   - glucoseImpactSeries:  the series of BGI (insulin effect) ticks
-    ///   - mealData:   absorption & COB info
-    ///   - profile:    user profile (for carbRatio, DIA, etc)
-    ///   - carbImpact:         current carb impact (mg/dL per 5m)
-    ///   - deviation:  current deviation (mg/dL per 5m)
-    /// - Returns: a capped/clamped array of future BGs, one per 5-minute interval
-    func forecast(
-        startingGlucose: Decimal,
-        glucoseImpactSeries: [Decimal],
-        mealData: ComputedCarbs,
-        profile: Profile,
-        carbImpact: Decimal,
-        deviation: Decimal,
-        adjustedSensitivity: Decimal,
-        sensitivityRatio: Decimal,
-        currentTime: Date
-    ) -> [Decimal]
-}
-
-/// Forecast sub-generator for insulin-only effect (IOB)
-struct IOBForecastGenerator: SingleForecasting {
+public extension ForecastGenerator {
     // TODO: Dynamic ISF not yet supported
 
-    public func forecast(
+    static func forecastIOB(
         startingGlucose: Decimal,
         glucoseImpactSeries: [Decimal],
-        mealData _: ComputedCarbs,
-        profile _: Profile,
-        carbImpact _: Decimal,
         deviation: Decimal,
-        adjustedSensitivity _: Decimal,
-        sensitivityRatio _: Decimal,
-        currentTime _: Date
     ) -> [Decimal] {
         var result = [startingGlucose]
         for (count, glucoseImpact) in glucoseImpactSeries.enumerated() {
@@ -46,13 +16,8 @@ struct IOBForecastGenerator: SingleForecasting {
         }
         return ForecastGenerator.trimFlatTails(result, lookback: 90 / 5)
     }
-}
 
-/// Forecast sub-generator for carb-only effect (COB + UAM piece)
-struct COBForecastGenerator: SingleForecasting {
-    // TODO: Dynamic ISF not yet supported
-
-    public func forecast(
+    static func forecastCOB(
         startingGlucose: Decimal,
         glucoseImpactSeries: [Decimal],
         mealData: ComputedCarbs,
@@ -104,7 +69,9 @@ struct COBForecastGenerator: SingleForecasting {
                     triangle = carbImpactParams.remainingCarbImpactPeak * Decimal(seriesCount) / Decimal(halfTriangle)
                 } else {
                     // Ramp down
-                    triangle = carbImpactParams.remainingCarbImpactPeak * Decimal(carbImpactParams.triangleIntervals - seriesCount) / Decimal(halfTriangle)
+                    triangle = carbImpactParams
+                        .remainingCarbImpactPeak * Decimal(carbImpactParams.triangleIntervals - seriesCount) /
+                        Decimal(halfTriangle)
                 }
             } else {
                 triangle = 0
@@ -121,76 +88,59 @@ struct COBForecastGenerator: SingleForecasting {
 
         return ForecastGenerator.trimFlatTails(result, lookback: 12)
     }
-}
 
-/// Forecast sub-generator for “unannounced meal” impact (UAM)
-struct UAMForecastGenerator: SingleForecasting {
-    // TODO: Dynamic ISF not yet supported
-
-    public func forecast(
+    static func forecastUAM(
         startingGlucose: Decimal,
         glucoseImpactSeries: [Decimal],
         mealData: ComputedCarbs,
-        profile _: Profile,
         carbImpact: Decimal,
-        deviation: Decimal,
-        adjustedSensitivity _: Decimal,
-        sensitivityRatio _: Decimal,
-        currentTime _: Date
+        deviation: Decimal
     ) -> [Decimal] {
         var result = [startingGlucose]
 
         let slopeFromDeviations = mealData.slopeFromMinDeviation
         let ticksInThreeHours: Decimal = 36 // 3 * 60 / 5
-        
+
         let unannouncedCarbImpact = carbImpact
-        
-        for glucoseImpact in 1..<glucoseImpactSeries.count {
+
+        for glucoseImpact in 1 ..< glucoseImpactSeries.count {
             let insulinEffect = glucoseImpactSeries[glucoseImpact]
             let forecastedDeviaton = min(0, deviation)
-            
+
             // In JS: predUCIslope = max(0, uci + (tick * slopeFromDeviations))
-            let forecastedUnannouncedCarbImpactSlope = max(0, unannouncedCarbImpact + Decimal(glucoseImpact) * slopeFromDeviations)
-            
+            let forecastedUnannouncedCarbImpactSlope = max(
+                0,
+                unannouncedCarbImpact + Decimal(glucoseImpact) * slopeFromDeviations
+            )
+
             // In JS: predUCImax = max(0, uci * (1 - tick / ticksInThreeHours))
-            let maxForecastedUnannouncedCarbImpact = max(0, unannouncedCarbImpact * (1 - Decimal(glucoseImpact) / ticksInThreeHours))
-            let forecastedUnannouncedCarbImpact = min(forecastedUnannouncedCarbImpactSlope, maxForecastedUnannouncedCarbImpact)
+            let maxForecastedUnannouncedCarbImpact = max(
+                0,
+                unannouncedCarbImpact * (1 - Decimal(glucoseImpact) / ticksInThreeHours)
+            )
+            let forecastedUnannouncedCarbImpact = min(
+                forecastedUnannouncedCarbImpactSlope,
+                maxForecastedUnannouncedCarbImpact
+            )
 
             let next = result.last! + insulinEffect + forecastedDeviaton + forecastedUnannouncedCarbImpact
-            
+
             result.append(next.clamp(lowerBound: 39, upperBound: 401))
         }
 
         return ForecastGenerator.trimFlatTails(result, lookback: 12)
     }
-}
-
-/// Forecast sub-generator for “zero-temp” baseline (ZT)
-struct ZTForecastGenerator: SingleForecasting {
-    // TODO: Dynamic ISF not yet supported
 
-    public func forecast(
+    static func forecastZT(
         startingGlucose: Decimal,
-        glucoseImpactSeries: [Decimal],
-        mealData: ComputedCarbs,
-        profile: Profile,
-        carbImpact: Decimal,
-        deviation: Decimal,
-        adjustedSensitivity: Decimal,
-        sensitivityRatio: Decimal,
-        currentTime: Date
+        glucoseImpactSeriesWithZeroTemp: [Decimal],
+        deviation: Decimal
     ) -> [Decimal] {
         // essentially insulin effect only, but with zero-temp ISF if needed
-        IOBForecastGenerator().forecast(
+        Self.forecastIOB(
             startingGlucose: startingGlucose,
-            glucoseImpactSeries: glucoseImpactSeries.map { /* TODO: use iobWithZeroTemp.activity */ $0 },
-            mealData: mealData,
-            profile: profile,
-            carbImpact: carbImpact,
-            deviation: deviation,
-            adjustedSensitivity: adjustedSensitivity,
-            sensitivityRatio: sensitivityRatio,
-            currentTime: currentTime
+            glucoseImpactSeries: glucoseImpactSeriesWithZeroTemp,
+            deviation: deviation
         )
     }
 }

+ 11 - 43
Trio/Sources/APS/OpenAPSSwift/Forecasts/ForecastGenerator.swift

@@ -1,27 +1,11 @@
 import Foundation
 
 /// The top-level orchestrator
-struct ForecastGenerator {
-    let iob: SingleForecasting
-    let cob: SingleForecasting
-    let uam: SingleForecasting
-    let zt: SingleForecasting
-
-    init(
-        iob: SingleForecasting = IOBForecastGenerator(),
-        cob: SingleForecasting = COBForecastGenerator(),
-        uam: SingleForecasting = UAMForecastGenerator(),
-        zt: SingleForecasting = ZTForecastGenerator()
-    ) {
-        self.iob = iob
-        self.cob = cob
-        self.uam = uam
-        self.zt = zt
-    }
-
-    public func generate(
+enum ForecastGenerator {
+    public static func generate(
         glucose: Decimal,
         glucoseImpactSeries: [Decimal],
+        glucoseImpactSeriesWithZeroTemp: [Decimal],
         iobData _: [IobResult],
         mealData: ComputedCarbs,
         profile: Profile,
@@ -38,19 +22,13 @@ struct ForecastGenerator {
         let deviation = mealData.currentDeviation
 
         // JS oref initializes all xxxPredBGs array with current glucose, we do the same, then generate
-        let iobForecast = [glucose] + iob.forecast(
+        let iobForecast = [glucose] + forecastIOB(
             startingGlucose: glucose,
             glucoseImpactSeries: glucoseImpactSeries,
-            mealData: mealData,
-            profile: profile,
-            carbImpact: carbImpact,
             deviation: deviation,
-            adjustedSensitivity: adjustedSensitivity,
-            sensitivityRatio: sensitivityRatio,
-            currentTime: currentTime
         )
 
-        let cobForecast = [glucose] + cob.forecast(
+        let cobForecast = [glucose] + forecastCOB(
             startingGlucose: glucose,
             glucoseImpactSeries: glucoseImpactSeries,
             mealData: mealData,
@@ -62,28 +40,18 @@ struct ForecastGenerator {
             currentTime: currentTime
         )
 
-        let uamForecast = [glucose] + uam.forecast(
+        let uamForecast = [glucose] + forecastUAM(
             startingGlucose: glucose,
             glucoseImpactSeries: glucoseImpactSeries,
             mealData: mealData,
-            profile: profile,
             carbImpact: carbImpact,
-            deviation: deviation,
-            adjustedSensitivity: adjustedSensitivity,
-            sensitivityRatio: sensitivityRatio,
-            currentTime: currentTime
+            deviation: deviation
         )
 
-        let ztForecast = [glucose] + zt.forecast(
+        let ztForecast = [glucose] + forecastZT(
             startingGlucose: glucose,
-            glucoseImpactSeries: glucoseImpactSeries,
-            mealData: mealData,
-            profile: profile,
-            carbImpact: carbImpact,
-            deviation: deviation,
-            adjustedSensitivity: adjustedSensitivity,
-            sensitivityRatio: sensitivityRatio,
-            currentTime: currentTime
+            glucoseImpactSeriesWithZeroTemp: glucoseImpactSeriesWithZeroTemp,
+            deviation: deviation
         )
 
         let computedForecastSelection = Self.computeForecastSelection(
@@ -102,7 +70,7 @@ struct ForecastGenerator {
             sensitivityRatio: sensitivityRatio,
             currentTime: currentTime
         )
-        
+
         let carbImpactDuration = carbImpact > 0 ? min(
             carbImpactParams.remainingCarbAbsorptionTime * 60 / 5 / 2,
             max(0, mealData.mealCOB * carbImpactParams.carbSensivityFactor / carbImpact)