Просмотр исходного кода

Fix inconsistencies with all forecasts: IOB, ZT, UAM, and COB

Sam King 10 месяцев назад
Родитель
Сommit
95b21579e3

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

@@ -363,12 +363,9 @@ extension DeterminationGenerator {
         // FIXME: this is assuming 5min steps...
         // Activity is U/hr
         if withZeroTemp {
-            return iobDataSeries.map { iobWithZeroTemp in
-                -iobWithZeroTemp.activity * sensitivity * 5 }
+            return iobDataSeries.map { -$0.iobWithZeroTemp.activity * sensitivity * 5 }
         } else {
-            return iobDataSeries.map { iob in
-                -iob.activity * sensitivity * 5
-            }
+            return iobDataSeries.map { -$0.activity * sensitivity * 5 }
         }
     }
 }

+ 6 - 2
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -201,11 +201,15 @@ enum DeterminationGenerator {
 
         let forecastResult = ForecastGenerator.generate(
             glucose: currentGlucose,
+            glucoseStatus: glucoseStatus,
+            currentGlucoseImpact: currentGlucoseImpact,
             glucoseImpactSeries: glucoseImpactSeries,
             glucoseImpactSeriesWithZeroTemp: glucoseImpactSeriesWithZeroTemp,
             iobData: iobData,
             mealData: mealData,
             profile: profile,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            targetGlucose: adjustedGlucoseTargets.targetGlucose,
             adjustedSensitivity: sensitivity,
             sensitivityRatio: sensitivityRatio,
             naiveEventualGlucose: naiveEventualGlucose,
@@ -236,8 +240,8 @@ enum DeterminationGenerator {
             iob: iobData.first?.iob,
             cob: mealData.mealCOB,
             predictions: Predictions(
-                iob: forecastResult.iob.map { Int($0) },
-                zt: forecastResult.zt.map { Int($0) },
+                iob: forecastResult.iob.map { Int($0.jsRounded()) },
+                zt: forecastResult.zt.map { Int($0.jsRounded()) },
                 cob: forecastResult.cob.map { Int($0) },
                 uam: forecastResult.uam.map { Int($0) }
             ),

+ 19 - 14
Trio/Sources/APS/OpenAPSSwift/Forecasts/CarbImpactParams.swift

@@ -1,39 +1,45 @@
 import Foundation
 
 struct CarbImpactParams {
-    let carbSensivityFactor: Decimal
     let cappedCarbImpact: Decimal
-    let remainingCarbAbsorptionTime: Decimal
+    let carbImpactDuration: Decimal
     let maxAbsorptionIntervals: Int
     let triangleIntervals: Int
     let remainingCarbImpactPeak: Decimal
 
     static func calculate(
-        adjustedSensitivity: Decimal,
+        carbSensitivityFactor: Decimal,
         profile: Profile,
         mealData: ComputedCarbs,
         carbImpact: Decimal,
         sensitivityRatio: Decimal,
         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
-        let maxCarbImpact = (maxCarbAbsorptionRate * carbSensivityFactor * 5 / 60).rounded(toPlaces: 1)
-        let cappedCarbImpact = min(initialCarbImpact, maxCarbImpact)
+        let maxCarbImpact = (maxCarbAbsorptionRate * carbSensitivityFactor * 5 / 60).rounded(toPlaces: 1)
+        let cappedCarbImpact = min(carbImpact, maxCarbImpact)
 
         let computedRemainingCarbAbsorptionTime = ForecastGenerator.calculateRemainingCarbAbsorptionTime(
             sensitivityRatio: sensitivityRatio,
             maxMealAbsorptionTime: profile.maxMealAbsorptionTime,
             mealCOB: mealData.mealCOB,
-            lastCarbTime: Date(timeIntervalSince1970: mealData.lastCarbTime),
+            lastCarbTime: Date(timeIntervalSince1970: mealData.lastCarbTime / 1000),
             currentTime: currentTime
         )
         // Clamp remainingTime for more robustness
         let remainingCarbAbsorptionTime = min(computedRemainingCarbAbsorptionTime, profile.maxMealAbsorptionTime)
 
+        let carbImpactDuration: Decimal
+        if carbImpact == 0 {
+            carbImpactDuration = 0
+        } else {
+            // cid = Math.min(remainingCATime*60/5/2,Math.max(0, meal_data.mealCOB * csf / ci ));
+            carbImpactDuration = min(
+                remainingCarbAbsorptionTime * 60 / 5 / 2,
+                max(0, mealData.mealCOB * carbSensitivityFactor / carbImpact)
+            )
+        }
+
         // Convert remainingCarbAbsorptionTime (hours) to intervals (each 5m):
         let dynamicAbsorptionIntervals = Int((remainingCarbAbsorptionTime * 60) / 5)
         // Number of 5-minute intervals over which we expect *all* carbs to absorb
@@ -44,7 +50,7 @@ struct CarbImpactParams {
         // Total CI (mg/dL)
         let totalCarbImpact = max(0, cappedCarbImpact / 5 * 60 * remainingCarbAbsorptionTime / 2)
         // Total carbs absorbed from CI (g)
-        let totalCarbsAbsorbed: Decimal = totalCarbImpact / carbSensivityFactor
+        let totalCarbsAbsorbed: Decimal = totalCarbImpact / carbSensitivityFactor
 
         // Remaining carbs cap/fraction logic
         let remainingCarbsCap = min(90, profile.remainingCarbsCap)
@@ -58,15 +64,14 @@ struct CarbImpactParams {
         // Peak impact (mg/dL per 5m) of the *remaining* carbs
         let remainingCarbImpactPeak: Decimal
         if remainingCarbAbsorptionTime > 0 {
-            remainingCarbImpactPeak = (remainingCarbs * carbSensivityFactor * 5 / 60) / (remainingCarbAbsorptionTime / 2)
+            remainingCarbImpactPeak = (remainingCarbs * carbSensitivityFactor * 5 / 60) / (remainingCarbAbsorptionTime / 2)
         } else {
             remainingCarbImpactPeak = 0
         }
 
         return CarbImpactParams(
-            carbSensivityFactor: carbSensivityFactor,
             cappedCarbImpact: cappedCarbImpact,
-            remainingCarbAbsorptionTime: remainingCarbAbsorptionTime,
+            carbImpactDuration: carbImpactDuration,
             maxAbsorptionIntervals: maxAbsorptionIntervals,
             triangleIntervals: triangleIntervals,
             remainingCarbImpactPeak: remainingCarbImpactPeak

+ 44 - 59
Trio/Sources/APS/OpenAPSSwift/Forecasts/ForecastGenerator+Forecasts.swift

@@ -6,71 +6,52 @@ extension ForecastGenerator {
     static func forecastIOB(
         startingGlucose: Decimal,
         glucoseImpactSeries: [Decimal],
-        deviation: Decimal,
+        carbImpact: 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 in glucoseImpactSeries {
+            let forecastedDeviation = carbImpact * (1 - min(1, Decimal(result.count) / (60 / 5)))
+            let next = result.last! + 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 {
+            if carbImpactParams.triangleIntervals > 0, result.count <= 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 {
+                if result.count <= halfTriangle {
                     // Ramp up
-                    triangle = carbImpactParams.remainingCarbImpactPeak * Decimal(seriesCount) / Decimal(halfTriangle)
+                    triangle = carbImpactParams.remainingCarbImpactPeak * Decimal(result.count) / Decimal(halfTriangle)
                 } else {
                     // Ramp down
                     triangle = carbImpactParams
-                        .remainingCarbImpactPeak * Decimal(carbImpactParams.triangleIntervals - seriesCount) /
+                        .remainingCarbImpactPeak * Decimal(carbImpactParams.triangleIntervals - result.count) /
                         Decimal(halfTriangle)
                 }
             } else {
@@ -78,69 +59,73 @@ extension ForecastGenerator {
             }
 
             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: 12)
     }
 
     static func forecastUAM(
         startingGlucose: Decimal,
         glucoseImpactSeries: [Decimal],
         mealData: ComputedCarbs,
-        carbImpact: Decimal,
-        deviation: Decimal
+        uamCarbImpact: Decimal,
+        carbImpact: Decimal
     ) -> [Decimal] {
         var result = [startingGlucose]
 
-        let slopeFromDeviations = mealData.slopeFromMinDeviation
+        let slopeFromDeviations = min(mealData.slopeFromMaxDeviation, -mealData.slopeFromMinDeviation / 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 in glucoseImpactSeries {
+            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 next = result.last! + 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: 12)
     }
 
     static func forecastZT(
         startingGlucose: Decimal,
         glucoseImpactSeriesWithZeroTemp: [Decimal],
-        deviation: Decimal
+        targetBG: Decimal
     ) -> [Decimal] {
-        // essentially insulin effect only, but with zero-temp ISF if needed
-        Self.forecastIOB(
-            startingGlucose: startingGlucose,
-            glucoseImpactSeries: glucoseImpactSeriesWithZeroTemp,
-            deviation: deviation
-        )
+        var result = [startingGlucose]
+        for glucoseImpact in glucoseImpactSeriesWithZeroTemp {
+            // Potential bug: ZT doesn't use forecastedDeviation like IoB does
+            let next = result.last! + 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)
     }
 }

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

@@ -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))
+    }
 }

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

@@ -72,10 +72,7 @@ enum OrefFunction: String, Codable {
                 // in Swift but not in JS
                 "timestamp",
                 "minGuardBG",
-                "minPredBG",
-                // We haven't implemented DynamicISF support for forecasting yet
-                "predBGs",
-                "eventualBG"
+                "minPredBG"
             ])
         }
     }
@@ -117,7 +114,11 @@ enum OrefFunction: String, Codable {
         case .determineBasal:
             return [
                 "sensitivityRatio": 0.011,
-                "expectedDelta": 0.11
+                "expectedDelta": 0.11,
+                "IOB": 1.1,
+                "ZT": 1.1,
+                "UAM": 1.1,
+                "COB": 1.1
             ]
         }
     }