Explorar el Código

Merge pull request #592 from nightscout/oref-swift-determine-basal-big-bug-fix

Bug fixes for the swift implementation of Determine Basal
Sam King hace 5 meses
padre
commit
4045f654d5

+ 0 - 3
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DeterminationError.swift

@@ -5,7 +5,6 @@ enum DeterminationError: LocalizedError, Equatable {
     case missingProfile
     case missingCurrentBasal
     case invalidProfileTarget
-    case staleGlucoseData(ageMinutes: Double)
     case glucoseOutOfRange(glucose: Decimal)
     case cgmNoiseTooHigh(noise: Int)
     case noDelta
@@ -26,8 +25,6 @@ enum DeterminationError: LocalizedError, Equatable {
         case .invalidProfileTarget:
             // string copied from JS including trailing space
             return String(localized: "Error: could not determine target_bg. ")
-        case let .staleGlucoseData(ageMinutes):
-            return String(localized: "Glucose data is too old (\(ageMinutes) min ago).")
         case let .glucoseOutOfRange(glucose):
             return String(localized: "Glucose out of range: \(glucose.description).")
         case let .cgmNoiseTooHigh(noise):

+ 10 - 5
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasal+Helpers.swift

@@ -164,9 +164,14 @@ extension DeterminationGenerator {
         return (ratio, updateAutosensRatio)
     }
 
-    static func computeAdjustedBasal(currentBasalRate: Decimal, sensitivityRatio: Decimal) -> Decimal {
-        // FIXME: Ideally, we round this here to allowed pump basal increments
-        currentBasalRate * sensitivityRatio
+    static func computeAdjustedBasal(
+        profile: Profile,
+        currentBasalRate: Decimal,
+        sensitivityRatio: Decimal,
+        overrideFactor: Decimal
+    ) -> Decimal {
+        let adjustedBasal = currentBasalRate * sensitivityRatio * overrideFactor
+        return TempBasalFunctions.roundBasal(profile: profile, basalRate: adjustedBasal)
     }
 
     static func computeAdjustedSensitivity(
@@ -175,8 +180,8 @@ extension DeterminationGenerator {
         trioCustomOrefVariables: TrioCustomOrefVariables
     ) -> Decimal {
         let sensitivity = trioCustomOrefVariables.override(sensitivity: sensitivity)
-        guard sensitivityRatio != 1.0 else { return sensitivity }
-        return (sensitivity / sensitivityRatio).rounded(toPlaces: 1)
+        guard sensitivityRatio != 1.0 else { return sensitivity.jsRounded(scale: 1) }
+        return (sensitivity / sensitivityRatio).jsRounded(scale: 1)
     }
 
     static func checkCurrentTempBasalRateSafety(

+ 34 - 33
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -65,8 +65,7 @@ enum DeterminationGenerator {
             currentTemp: currentTemp,
             iobData: iobData,
             profile: profile,
-            trioCustomOrefVariables: trioCustomOrefVariables,
-            currentTime: currentTime,
+            trioCustomOrefVariables: trioCustomOrefVariables
         )
 
         let currentGlucose: Decimal = glucoseStatus.glucose
@@ -75,7 +74,8 @@ enum DeterminationGenerator {
             glucoseStatus: glucoseStatus,
             profile: profile,
             currentTemp: currentTemp,
-            currentTime: currentTime
+            currentTime: currentTime,
+            trioCustomOrefVariables: trioCustomOrefVariables
         ) {
             return errorDetermination
         }
@@ -100,24 +100,24 @@ enum DeterminationGenerator {
                 sensitivityRatio: nil,
                 rate: 0,
                 duration: 0,
-                iob: iobData.first?.iob,
+                iob: nil,
                 cob: nil,
                 predictions: nil,
                 deliverAt: currentTime,
                 carbsReq: nil,
                 temp: .absolute,
-                bg: glucoseStatus.glucose,
+                bg: nil,
                 reservoir: nil,
-                isf: profile.sens,
-                timestamp: currentTime,
+                isf: nil,
+                timestamp: nil,
                 tdd: nil,
-                current_target: profile.targetBg,
+                current_target: nil,
                 minDelta: nil,
                 expectedDelta: nil,
                 minGuardBG: nil,
                 minPredBG: nil,
                 threshold: nil,
-                carbRatio: profile.carbRatio,
+                carbRatio: nil,
                 received: false
             )
         }
@@ -154,16 +154,20 @@ enum DeterminationGenerator {
             )
         }
 
-        let basal: Decimal
-        if let dynamicIsfResult = dynamicIsfResult, profile.tddAdjBasal {
+        var basal = profile.currentBasal ?? profile.basalFor(time: currentTime)
+        if dynamicIsfResult == nil {
             basal = computeAdjustedBasal(
+                profile: profile,
                 currentBasalRate: profile.currentBasal ?? profile.basalFor(time: currentTime),
-                sensitivityRatio: dynamicIsfResult.tddRatio
+                sensitivityRatio: sensitivityRatio,
+                overrideFactor: trioCustomOrefVariables.overrideFactor()
             )
-        } else {
+        } else if let dynamicIsfResult = dynamicIsfResult, profile.tddAdjBasal {
             basal = computeAdjustedBasal(
+                profile: profile,
                 currentBasalRate: profile.currentBasal ?? profile.basalFor(time: currentTime),
-                sensitivityRatio: sensitivityRatio
+                sensitivityRatio: dynamicIsfResult.tddRatio,
+                overrideFactor: trioCustomOrefVariables.overrideFactor()
             )
         }
 
@@ -306,7 +310,7 @@ enum DeterminationGenerator {
             id: UUID(),
             reason: reason,
             units: nil,
-            insulinReq: nil,
+            insulinReq: 0,
             eventualBG: Int(forecastResult.eventualGlucose.jsRounded()),
             sensitivityRatio: sensitivityRatio, // this would only the AS-adjusted one for now
             rate: nil,
@@ -377,7 +381,7 @@ enum DeterminationGenerator {
                 targetGlucose: adjustedGlucoseTargets.targetGlucose,
                 minDelta: minDelta,
                 expectedDelta: expectedDelta,
-                carbsRequired: dosingInputs.carbsRequired?.carbs ?? 0,
+                carbsRequired: dosingInputs.rawCarbsRequired,
                 naiveEventualGlucose: naiveEventualGlucose,
                 glucoseStatus: glucoseStatus,
                 currentTemp: currentTemp,
@@ -476,7 +480,7 @@ enum DeterminationGenerator {
             currentTime: currentTime,
             targetGlucose: adjustedGlucoseTargets.targetGlucose,
             naiveEventualGlucose: naiveEventualGlucose,
-            minIOBForecastedGlucose: forecastResult.iob.min() ?? 0,
+            minIOBForecastedGlucose: forecastResult.minIOBForecastedGlucose,
             adjustedSensitivity: adjustedSensitivity,
             overrideFactor: trioCustomOrefVariables.overrideFactor(),
             adjustedCarbRatio: forecastResult.adjustedCarbRatio,
@@ -503,8 +507,7 @@ enum DeterminationGenerator {
         currentTemp _: TempBasal?,
         iobData: [IobResult]?,
         profile: Profile?,
-        trioCustomOrefVariables: TrioCustomOrefVariables,
-        currentTime: Date = Date()
+        trioCustomOrefVariables: TrioCustomOrefVariables
     ) throws {
         guard let glucoseStatus = glucoseStatus else {
             throw DeterminationError.missingGlucoseStatus
@@ -515,10 +518,6 @@ enum DeterminationGenerator {
         guard profile.profileTarget(trioCustomOrefVariables: trioCustomOrefVariables) != nil else {
             throw DeterminationError.invalidProfileTarget
         }
-        let glucoseAge = currentTime.timeIntervalSince(glucoseStatus.date)
-        if glucoseAge > 15 * 60 {
-            throw DeterminationError.staleGlucoseData(ageMinutes: glucoseAge / 60)
-        }
         // we have to allow 38 values so that we can cancel high temps
         if glucoseStatus.glucose < 38 || glucoseStatus.glucose > 600 {
             throw DeterminationError.glucoseOutOfRange(glucose: glucoseStatus.glucose)
@@ -532,7 +531,8 @@ enum DeterminationGenerator {
         glucoseStatus: GlucoseStatus,
         profile: Profile,
         currentTemp: TempBasal?,
-        currentTime: Date
+        currentTime: Date,
+        trioCustomOrefVariables: TrioCustomOrefVariables
     ) throws -> Determination? {
         let glucose = glucoseStatus.glucose
         let noise = glucoseStatus.noise
@@ -544,9 +544,10 @@ enum DeterminationGenerator {
         let device = glucoseStatus.device
 
         // Always use profile-supplied basal
-        guard let basal = profile.currentBasal else {
+        guard let profileBasal = profile.currentBasal else {
             throw DeterminationError.missingCurrentBasal
         }
+        let basal = profileBasal * trioCustomOrefVariables.overrideFactor()
 
         // Compose tick for log
         let tick: String = (delta > -0.5) ? "+\(delta.rounded(toPlaces: 0))" : "\(delta.rounded(toPlaces: 0))"
@@ -605,7 +606,7 @@ enum DeterminationGenerator {
                 deliverAt: currentTime,
                 carbsReq: nil,
                 temp: .absolute,
-                bg: glucose,
+                bg: nil,
                 reservoir: nil,
                 isf: profile.sens,
                 timestamp: currentTime,
@@ -616,7 +617,7 @@ enum DeterminationGenerator {
                 minGuardBG: nil,
                 minPredBG: nil,
                 threshold: nil,
-                carbRatio: profile.carbRatio,
+                carbRatio: nil,
                 received: false
             )
         } else if currentTemp.rate == 0, currentTemp.duration > 30 {
@@ -637,7 +638,7 @@ enum DeterminationGenerator {
                 deliverAt: currentTime,
                 carbsReq: nil,
                 temp: .absolute,
-                bg: glucose,
+                bg: nil,
                 reservoir: nil,
                 isf: profile.sens,
                 timestamp: currentTime,
@@ -648,7 +649,7 @@ enum DeterminationGenerator {
                 minGuardBG: nil,
                 minPredBG: nil,
                 threshold: nil,
-                carbRatio: profile.carbRatio,
+                carbRatio: nil,
                 received: false
             )
         } else {
@@ -661,15 +662,15 @@ enum DeterminationGenerator {
                 insulinReq: nil,
                 eventualBG: nil,
                 sensitivityRatio: nil,
-                rate: currentTemp.rate,
-                duration: Decimal(currentTemp.duration),
+                rate: nil,
+                duration: nil,
                 iob: nil,
                 cob: nil,
                 predictions: nil,
                 deliverAt: currentTime,
                 carbsReq: nil,
                 temp: currentTemp.temp,
-                bg: glucose,
+                bg: nil,
                 reservoir: nil,
                 isf: profile.sens,
                 timestamp: currentTime,
@@ -680,7 +681,7 @@ enum DeterminationGenerator {
                 minGuardBG: nil,
                 minPredBG: nil,
                 threshold: nil,
-                carbRatio: profile.carbRatio,
+                carbRatio: nil,
                 received: false
             )
         }

+ 23 - 21
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DosingEngine.swift

@@ -4,6 +4,7 @@ enum DosingEngine {
     struct DosingInputs {
         let reason: String
         let carbsRequired: (carbs: Decimal, minutes: Decimal)?
+        let rawCarbsRequired: Decimal
     }
 
     /// struct to keep the relevant state needed for the output of the SMB decision logic
@@ -177,7 +178,6 @@ enum DosingEngine {
         reason += "; " // Start of conclusion
 
         let carbsRequiredResult = calculateCarbsRequired(
-            profile: profile,
             mealData: mealData,
             naiveEventualGlucose: naiveEventualGlucose,
             minGuardGlucose: forecast.minGuardGlucose,
@@ -192,18 +192,19 @@ enum DosingEngine {
             adjustedCarbRatio: forecast.adjustedCarbRatio
         )
 
-        if let result = carbsRequiredResult {
-            reason += "\(result.carbs) add'l carbs req w/in \(result.minutes)m; "
+        var carbsRequired: (carbs: Decimal, minutes: Decimal)?
+        if carbsRequiredResult.carbs >= profile.carbsReqThreshold, carbsRequiredResult.minutes <= 45 {
+            reason += "\(carbsRequiredResult.carbs) add'l carbs req w/in \(carbsRequiredResult.minutes)m; "
+            carbsRequired = carbsRequiredResult
         }
 
-        return DosingInputs(reason: reason, carbsRequired: carbsRequiredResult)
+        return DosingInputs(reason: reason, carbsRequired: carbsRequired, rawCarbsRequired: carbsRequiredResult.carbs)
     }
 
     /// Calculates the carbohydrates required to avoid a potential hypoglycemic event.
     ///
-    /// - Returns: A tuple containing the required carbs and minutes until BG is below threshold, or `nil` if no carbs are required.
+    /// - Returns: A tuple containing the required carbs and minutes until glucose is below threshold.
     static func calculateCarbsRequired(
-        profile: Profile,
         mealData: ComputedCarbs,
         naiveEventualGlucose: Decimal,
         minGuardGlucose: Decimal,
@@ -216,7 +217,7 @@ enum DosingEngine {
         overrideFactor: Decimal,
         adjustedSensitivity: Decimal,
         adjustedCarbRatio: Decimal
-    ) -> (carbs: Decimal, minutes: Decimal)? {
+    ) -> (carbs: Decimal, minutes: Decimal) {
         var carbsRequiredGlucose = naiveEventualGlucose
         if naiveEventualGlucose < 40 {
             carbsRequiredGlucose = min(minGuardGlucose, naiveEventualGlucose)
@@ -243,19 +244,14 @@ enum DosingEngine {
         let mealCarbs = mealData.carbs
         let cobForCarbsRequired = max(0, mealData.mealCOB - (Decimal(0.25) * mealCarbs))
 
-        guard adjustedCarbRatio > 0 else { return nil }
+        guard adjustedCarbRatio > 0 else { return (carbs: 0, minutes: minutesAboveThreshold) }
         let carbSensitivityFactor = adjustedSensitivity / adjustedCarbRatio
-        guard carbSensitivityFactor > 0 else { return nil }
+        guard carbSensitivityFactor > 0 else { return (carbs: 0, minutes: minutesAboveThreshold) }
 
         var carbsRequired = (glucoseUndershoot - zeroTempEffect) / carbSensitivityFactor - cobForCarbsRequired
         carbsRequired = carbsRequired.rounded(toPlaces: 0)
 
-        let carbsRequiredThreshold = profile.carbsReqThreshold
-        if carbsRequired >= carbsRequiredThreshold, minutesAboveThreshold <= 45 {
-            return (carbs: carbsRequired, minutes: minutesAboveThreshold)
-        }
-
-        return nil
+        return (carbs: carbsRequired, minutes: minutesAboveThreshold)
     }
 
     /// Determines if a low glucose suspend is warranted.
@@ -309,7 +305,7 @@ enum DosingEngine {
             }
 
             let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
-            var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
+            var durationRequired = (60 * worstCaseInsulinRequired / currentBasal * overrideFactor).jsRounded()
             durationRequired = (durationRequired / 30).jsRounded() * 30
             durationRequired = max(30, min(120, durationRequired))
 
@@ -402,7 +398,11 @@ enum DosingEngine {
             .reason +=
             "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) < \(convertGlucose(profile: profile, glucose: minGlucose))"
 
-        // if 5m or 30m avg BG is rising faster than expected delta
+        // if 5m or 30m avg glucose is rising faster than expected delta
+        // BUG: in JS it's doing a "truthiness" check for carbs required
+        //      but if you get a negative carbsRequired it will evaluate
+        //      to true when it should be false (negative carbs required
+        //      means no carbs required)
         if minDelta > expectedDelta, minDelta > 0, carbsRequired == 0 {
             if naiveEventualGlucose < 40 {
                 newDetermination.reason += ", naive_eventualBG < 40. "
@@ -445,7 +445,7 @@ enum DosingEngine {
             }
         }
 
-        // calculate 30m low-temp required to get projected BG up to target
+        // calculate 30m low-temp required to get projected glucose up to target
         var insulinRequired = 2 * min(0, (eventualGlucose - targetGlucose) / adjustedSensitivity)
         insulinRequired = insulinRequired.jsRounded(scale: 2)
 
@@ -485,7 +485,7 @@ enum DosingEngine {
                 }
                 let glucoseUndershoot = targetGlucose - naiveEventualGlucose
                 let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
-                var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
+                var durationRequired = (60 * worstCaseInsulinRequired / currentBasal * overrideFactor).jsRounded()
 
                 if durationRequired < 0 {
                     durationRequired = 0
@@ -685,7 +685,9 @@ enum DosingEngine {
 
         if insulinRequired > maxIob - currentIob {
             newDetermination.reason += "max_iob \(maxIob), "
-            insulinRequired = (maxIob - currentIob).jsRounded(scale: 2)
+            // Important: on this path insulinRequired gets rounded
+            // to three decimal places, not 2 like on the default path
+            insulinRequired = (maxIob - currentIob).jsRounded(scale: 3)
         }
         newDetermination.insulinReq = insulinRequired
         return (insulinRequired, newDetermination)
@@ -744,7 +746,7 @@ enum DosingEngine {
 
         let worstCaseInsulinRequired = (targetGlucose - (naiveEventualGlucose + minIOBForecastedGlucose) / 2) /
             adjustedSensitivity
-        var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
+        var durationRequired = (60 * worstCaseInsulinRequired / currentBasal * overrideFactor).jsRounded()
 
         // if insulinRequired > 0 but not enough for a microBolus, don't set an SMB zero temp
         if insulinRequired > 0, microBolus < profile.bolusIncrement {

+ 2 - 1
Trio/Sources/APS/OpenAPSSwift/Extensions/Decimal+rounding.swift

@@ -31,7 +31,8 @@ extension Decimal {
     }
 
     func jsRounded() -> Decimal {
-        jsRounded(scale: 0)
+        // double rounding to help with imprecision in calculations
+        jsRounded(scale: 6).jsRounded(scale: 0)
     }
 
     func clamp(lowerBound: Decimal, upperBound: Decimal) -> Decimal {

+ 42 - 31
Trio/Sources/APS/OpenAPSSwift/Forecasts/ForecastGenerator.swift

@@ -119,7 +119,7 @@ enum ForecastGenerator {
             remainingCarbImpactPeak: carbImpactParams.remainingCarbImpactPeak,
             fractionCarbsLeft: mealData.carbs > 0 ? mealData.mealCOB / mealData.carbs : Decimal(0),
             threshold: threshold,
-            targetGlucose: profile.targetBg ?? 100,
+            targetGlucose: targetGlucose,
             currentGlucose: glucose
         )
 
@@ -128,7 +128,7 @@ enum ForecastGenerator {
         if mealData.mealCOB > 0, carbImpact > 0 || carbImpactParams.remainingCarbImpactPeak > 0 {
             finalCobForecast = cobResult.forecasts
             if let lastCobGlucose = cobResult.forecasts.last {
-                eventualGlucose = max(eventualGlucose, lastCobGlucose)
+                eventualGlucose = max(eventualGlucose, lastCobGlucose.jsRounded())
             }
         }
 
@@ -136,7 +136,7 @@ enum ForecastGenerator {
         if profile.enableUAM, carbImpact > 0 || carbImpactParams.remainingCarbImpactPeak > 0 {
             finalUamForecast = uamResult.forecasts
             if let lastUamGlucose = uamResult.forecasts.last {
-                eventualGlucose = max(eventualGlucose, lastUamGlucose)
+                eventualGlucose = max(eventualGlucose, lastUamGlucose.jsRounded())
             }
         }
 
@@ -149,6 +149,7 @@ enum ForecastGenerator {
             internalUam: uamResult.forecasts,
             eventualGlucose: eventualGlucose,
             minForecastedGlucose: blendedForecasts.minForecastedGlucose,
+            minIOBForecastedGlucose: initialForecasts.iob.minForecastGlucose,
             minGuardGlucose: blendedForecasts.minGuardGlucose,
             carbImpact: carbImpact,
             remainingCarbImpactPeak: carbImpactParams.remainingCarbImpactPeak,
@@ -189,38 +190,48 @@ enum ForecastGenerator {
         // start at 1 because the first entry is currentGlucose
         for index in 1 ..< minCount {
             let length = index + 1
-            let iob = iobForecast.rawForecasts[index]
-            let cob = cobForecast.rawForecasts[index]
-            let uam = uamForecast.rawForecasts[index]
+            let currentIobForecastGlucose = iobForecast.rawForecasts[index]
+            let currentCobForecastGlucose = cobForecast.rawForecasts[index]
+            let currentUamForecastGlucose = uamForecast.rawForecasts[index]
 
             // the max calculations don't get rounded in JS
-            if length > insulinPeak5m, iob < minIobForecastGlucose {
-                minIobForecastGlucose = iob.jsRounded()
+            if length > insulinPeak5m, currentIobForecastGlucose < minIobForecastGlucose {
+                minIobForecastGlucose = currentIobForecastGlucose.jsRounded()
             }
-            if iob > maxIobForecastGlucose {
-                maxIobForecastGlucose = iob
+            if currentIobForecastGlucose > maxIobForecastGlucose {
+                maxIobForecastGlucose = currentIobForecastGlucose
             }
-            if carbImpactDuration != 0 || remainingCarbImpactPeak > 0, length > insulinPeak5m, cob < minCobForecastGlucose {
-                minCobForecastGlucose = cob.jsRounded()
+            if carbImpactDuration != 0 || remainingCarbImpactPeak > 0, length > insulinPeak5m,
+               currentCobForecastGlucose < minCobForecastGlucose
+            {
+                minCobForecastGlucose = currentCobForecastGlucose.jsRounded()
             }
-            if carbImpactDuration != 0 || remainingCarbImpactPeak > 0, cob > maxCobForecastGlucose {
-                maxCobForecastGlucose = cob
+            // BUG: I can't tell if the comparison against maxIobForecastGlucose is
+            // intentional or not, but this is what is in JS
+            if carbImpactDuration != 0 || remainingCarbImpactPeak > 0, currentCobForecastGlucose > maxIobForecastGlucose {
+                maxCobForecastGlucose = currentCobForecastGlucose
             }
-            if uamEnabled, length > 12, uam < minUamForecastGlucose {
-                minUamForecastGlucose = uam.jsRounded()
+            if uamEnabled, length > 12, currentUamForecastGlucose < minUamForecastGlucose {
+                minUamForecastGlucose = currentUamForecastGlucose.jsRounded()
             }
             // BUG: I can't tell if the comparison against maxIobForecastGlucose is
             // intentional or not, but this is what is in JS
-            if uamEnabled, uam > maxIobForecastGlucose {
-                maxUamForecastGlucose = uam
+            if uamEnabled, currentUamForecastGlucose > maxIobForecastGlucose {
+                maxUamForecastGlucose = currentUamForecastGlucose
             }
         }
+
+        minIobForecastGlucose = max(39, minIobForecastGlucose)
+        minCobForecastGlucose = max(39, minCobForecastGlucose)
+        minUamForecastGlucose = max(39, minUamForecastGlucose)
+
         return AllForecasts(
             iob: IOBForecast(
                 forecasts: iobForecast.forecasts,
                 minGuardGlucose: iobForecast.minGuardGlucose,
                 minForecastGlucose: minIobForecastGlucose,
-                maxForecastGlucose: maxIobForecastGlucose
+                maxForecastGlucose: maxIobForecastGlucose,
+                lastForecastGlucose: iobForecast.rawForecasts.last ?? currentGlucose
             ),
             zt: ZTForecast(
                 forecasts: ztForecast.forecasts,
@@ -230,14 +241,16 @@ enum ForecastGenerator {
                 forecasts: cobForecast.forecasts,
                 minGuardGlucose: cobForecast.minGuardGlucose,
                 minForecastGlucose: minCobForecastGlucose,
-                maxForecastGlucose: maxCobForecastGlucose
+                maxForecastGlucose: maxCobForecastGlucose,
+                lastForecastGlucose: cobForecast.rawForecasts.last ?? currentGlucose
             ),
             uam: UAMForecast(
                 forecasts: uamForecast.forecasts,
                 minGuardGlucose: uamForecast.minGuardGlucose,
                 minForecastGlucose: minUamForecastGlucose,
                 maxForecastGlucose: maxUamForecastGlucose,
-                duration: uamForecast.duration!
+                duration: uamForecast.duration!,
+                lastForecastGlucose: uamForecast.rawForecasts.last ?? currentGlucose
             ) // I don't love the force unwrap here but it should always be set
         )
     }
@@ -294,34 +307,32 @@ enum ForecastGenerator {
         // 1. Calculate minZTUAMForecastGlucose ("minZTUAMPredBG" in JS)
         var minZTUAMForecastGlucose = uamResult.minForecastGlucose
         if ztResult.minGuardGlucose < threshold {
-            minZTUAMForecastGlucose = ((uamResult.minForecastGlucose + ztResult.minGuardGlucose) / 2)
-                .rounded()
+            minZTUAMForecastGlucose = (uamResult.minForecastGlucose + ztResult.minGuardGlucose) / 2
         } else if ztResult.minGuardGlucose < targetGlucose {
             let blendPct = (ztResult.minGuardGlucose - threshold) / (targetGlucose - threshold)
             let blendedMinZTGuardGlucose = uamResult.minForecastGlucose * blendPct + ztResult.minGuardGlucose * (1 - blendPct)
-            minZTUAMForecastGlucose = ((uamResult.minForecastGlucose + blendedMinZTGuardGlucose) / 2).rounded()
+            minZTUAMForecastGlucose = (uamResult.minForecastGlucose + blendedMinZTGuardGlucose) / 2
         } else if ztResult.minGuardGlucose > uamResult.minForecastGlucose {
-            minZTUAMForecastGlucose = ((uamResult.minForecastGlucose + ztResult.minGuardGlucose) / 2)
-                .rounded()
+            minZTUAMForecastGlucose = (uamResult.minForecastGlucose + ztResult.minGuardGlucose) / 2
         }
+        minZTUAMForecastGlucose = minZTUAMForecastGlucose.jsRounded()
 
         // 2. avgForecastGlucose blending (like avgPredBG)
         let avgerageForecastGlucose: Decimal
         if uamResult.minForecastGlucose < 999, cobResult.minForecastGlucose < 999 {
             avgerageForecastGlucose = (
-                (1 - fractionCarbsLeft) * (uamResult.forecasts.last ?? currentGlucose) + fractionCarbsLeft *
-                    (cobResult.forecasts.last ?? currentGlucose)
+                (1 - fractionCarbsLeft) * uamResult.lastForecastGlucose + fractionCarbsLeft * cobResult.lastForecastGlucose
             ).rounded()
         } else if cobResult.minForecastGlucose < 999 {
             avgerageForecastGlucose =
-                (((iobResult.forecasts.last ?? currentGlucose) + (cobResult.forecasts.last ?? currentGlucose)) / 2)
+                ((iobResult.lastForecastGlucose + cobResult.lastForecastGlucose) / 2)
                     .rounded()
         } else if uamResult.minForecastGlucose < 999 {
             avgerageForecastGlucose =
-                (((iobResult.forecasts.last ?? currentGlucose) + (uamResult.forecasts.last ?? currentGlucose)) / 2)
+                ((iobResult.lastForecastGlucose + uamResult.lastForecastGlucose) / 2)
                     .rounded()
         } else {
-            avgerageForecastGlucose = (iobResult.forecasts.last ?? currentGlucose).rounded()
+            avgerageForecastGlucose = iobResult.lastForecastGlucose.rounded()
         }
         let adjustedAverageForecastGlucose = max(avgerageForecastGlucose, ztResult.minGuardGlucose)
 

+ 3 - 0
Trio/Sources/APS/OpenAPSSwift/Forecasts/ForecastResults.swift

@@ -5,6 +5,7 @@ struct IOBForecast {
     let minGuardGlucose: Decimal // The absolute min of the untrimmed array
     let minForecastGlucose: Decimal // The min after the initial 90-min peak
     let maxForecastGlucose: Decimal // The absolute max of the untrimmed array
+    let lastForecastGlucose: Decimal // The last forecast (IOBPredBG in JS)
 }
 
 struct COBForecast {
@@ -12,6 +13,7 @@ struct COBForecast {
     let minGuardGlucose: Decimal // The absolute min of the untrimmed array
     let minForecastGlucose: Decimal // The min after the initial 90-min peak
     let maxForecastGlucose: Decimal // The absolute max of the untrimmed array
+    let lastForecastGlucose: Decimal // The last forecast (COBPredBG in JS)
 }
 
 struct UAMForecast {
@@ -20,6 +22,7 @@ struct UAMForecast {
     let minForecastGlucose: Decimal // The min after the initial 60-min peak
     let maxForecastGlucose: Decimal // The absolute max of the untrimmed array
     let duration: Decimal // The calculated UAM duration in hours
+    let lastForecastGlucose: Decimal // The last forecast (UAMPredBG in JS)
 }
 
 struct ZTForecast {

+ 5 - 4
Trio/Sources/APS/OpenAPSSwift/Logging/OrefFunction.swift

@@ -45,12 +45,13 @@ enum OrefFunction: String, Codable {
         case .determineBasal:
             // FIXME: Adjust as we go
             return Set([
+                // Final dosing calculation
+                // "units",
+                // "insulinReq",
+                // "rate",
+                // "duration",
                 // Not calculating yet
                 "id",
-                "units",
-                "insulinReq",
-                "rate",
-                "duration",
                 "deliverAt",
                 "temp",
                 "reservoir",

+ 1 - 0
Trio/Sources/APS/OpenAPSSwift/Models/ForecastResult.swift

@@ -9,6 +9,7 @@ struct ForecastResult {
     public let internalUam: [Decimal] // non optional, used downstream
     public let eventualGlucose: Decimal
     public let minForecastedGlucose: Decimal
+    public let minIOBForecastedGlucose: Decimal
     public let minGuardGlucose: Decimal
     public let carbImpact: Decimal
     public let remainingCarbImpactPeak: Decimal

+ 0 - 3
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -118036,9 +118036,6 @@
         }
       }
     },
-    "Glucose data is too old (%lf min ago)." : {
-
-    },
     "Glucose Data used for statistics" : {
       "comment" : "Debug option view Glucose Data used for statistics",
       "extractionState" : "manual",

+ 2 - 2
TrioTests/OpenAPSSwiftTests/DetermineBasalEarlyExitTests.swift

@@ -305,8 +305,8 @@ import Testing
             currentTime: currentTime
         )
 
-        #expect(result?.rate == 0.5)
-        #expect(result?.duration == 30)
+        #expect(result?.rate == nil)
+        #expect(result?.duration == nil)
         #expect(result?.reason.contains("doing nothing") == true)
     }
 

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/basal-set-temp.js