| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671 |
- import Foundation
- enum DosingEngine {
- struct DosingInputs {
- let reason: String
- let carbsRequired: (carbs: Decimal, minutes: Decimal)?
- }
- /// struct to keep the relevant state needed for the output of the SMB decision logic
- struct SMBDecision {
- let isEnabled: Bool
- let minGuardGlucose: Decimal?
- let reason: String?
- }
- /// checks to see if SMB are enabled via the profile
- private static func isProfileSmbEnabled(
- currentGlucose: Decimal,
- adjustedTargetGlucose: Decimal,
- profile: Profile,
- meal: ComputedCarbs,
- trioCustomOrefVariables: TrioCustomOrefVariables,
- clock: Date
- ) throws -> Bool {
- if trioCustomOrefVariables.smbIsOff {
- return false
- }
- if try isSmbScheduledOff(trioCustomOrefVariables: trioCustomOrefVariables, clock: clock) {
- return false
- }
- if trioCustomOrefVariables.shouldProtectDueToHIGH {
- return false
- }
- if !profile.allowSMBWithHighTemptarget, profile.temptargetSet == true, adjustedTargetGlucose > 100 {
- return false
- }
- if profile.enableSMBAlways {
- return true
- }
- if profile.enableSMBWithCOB, meal.mealCOB > 0 {
- return true
- }
- if profile.enableSMBAfterCarbs, meal.carbs > 0 {
- return true
- }
- if profile.enableSMBWithTemptarget, profile.temptargetSet == true, adjustedTargetGlucose < 100 {
- return true
- }
- if profile.enableSMBHighBg, currentGlucose >= profile.enableSMBHighBgTarget {
- return true
- }
- return false
- }
- /// helper function to check if SMB is scheduled off given the current timezone
- private static func isSmbScheduledOff(trioCustomOrefVariables: TrioCustomOrefVariables, clock: Date) throws -> Bool {
- guard trioCustomOrefVariables.smbIsScheduledOff else {
- return false
- }
- guard let currentHour = clock.hourInLocalTime.map({ Decimal($0) }) else {
- throw CalendarError.invalidCalendarHourOnly
- }
- let startHour = trioCustomOrefVariables.start
- let endHour = trioCustomOrefVariables.end
- // SMBs will be disabled from [start, end) local time
- if startHour < endHour, currentHour >= startHour && currentHour < endHour {
- // disable when the schedule does not wrap around midnight
- return true
- } else if startHour > endHour, currentHour >= startHour || currentHour < endHour {
- // disable when the schedule does wrap around midnight
- return true
- } else if startHour == 0, endHour == 0 {
- // schedule specifies the entire day
- return true
- } else if startHour == endHour, currentHour == startHour {
- // one hour of scheduled off SMB
- return true
- }
- return false
- }
- /// helper function for reason string glucose output
- static func convertGlucose(profile: Profile, glucose: Decimal) -> Decimal {
- let units = profile.outUnits ?? .mgdL
- switch units {
- case .mgdL: return glucose.jsRounded()
- case .mmolL: return glucose.asMmolL
- }
- }
- /// Top level smb enabling logic
- ///
- /// This function includes both the profile / customOrefVariable checks from JS `enable_smb` as
- /// well as some of the later checks from `determineBasal` that can disable SMB
- static func makeSMBDosingDecision(
- profile: Profile,
- meal: ComputedCarbs,
- currentGlucose: Decimal,
- adjustedTargetGlucose: Decimal,
- minGuardGlucose: Decimal,
- threshold: Decimal,
- glucoseStatus: GlucoseStatus,
- trioCustomOrefVariables: TrioCustomOrefVariables,
- clock: Date
- ) throws -> SMBDecision {
- var smbIsEnabled = try isProfileSmbEnabled(
- currentGlucose: currentGlucose,
- adjustedTargetGlucose: adjustedTargetGlucose,
- profile: profile,
- meal: meal,
- trioCustomOrefVariables: trioCustomOrefVariables,
- clock: clock
- )
- // these last two checks are implemented outside of the core enable_smb
- // function in JS but we should keep all of the smb enabling logic
- // in one place. Note: We can't shortcut the return value because
- // the determineBasal logic always evaluates this logic
- var minGuardGlucoseDecision: Decimal?
- var reason: String?
- if smbIsEnabled, minGuardGlucose < threshold {
- minGuardGlucoseDecision = minGuardGlucose
- smbIsEnabled = false
- }
- let maxDeltaGlucoseThreshold = min(profile.maxDeltaBgThreshold, 0.4)
- if glucoseStatus.maxDelta > maxDeltaGlucoseThreshold * currentGlucose {
- reason =
- "maxDelta \(convertGlucose(profile: profile, glucose: glucoseStatus.maxDelta)) > \(100 * maxDeltaGlucoseThreshold)% of BG \(convertGlucose(profile: profile, glucose: currentGlucose)) - SMB disabled!, "
- smbIsEnabled = false
- }
- return SMBDecision(
- isEnabled: smbIsEnabled,
- minGuardGlucose: minGuardGlucoseDecision,
- reason: reason
- )
- }
- static func prepareDosingInputs(
- profile: Profile,
- mealData: ComputedCarbs,
- forecast: ForecastResult,
- naiveEventualGlucose: Decimal,
- threshold: Decimal,
- glucoseImpact: Decimal,
- deviation: Decimal,
- currentBasal: Decimal,
- overrideFactor: Decimal,
- adjustedSensitivity: Decimal,
- isfReason: String,
- tddReason: String,
- targetLog: String // This is a pre-formatted string from the JS
- ) -> DosingInputs {
- let lastIOBpredBG = forecast.iob.last ?? 0
- let lastCOBpredBG = forecast.cob?.last
- let lastUAMpredBG = forecast.uam?.last
- var reason =
- "\(isfReason), COB: \(mealData.mealCOB), Dev: \(deviation), BGI: \(glucoseImpact), CR: \(forecast.adjustedCarbRatio), Target: \(targetLog), minPredBG \(forecast.minForecastedGlucose), minGuardBG \(forecast.minGuardGlucose), IOBpredBG \(lastIOBpredBG)"
- if let lastCOB = lastCOBpredBG {
- reason += ", COBpredBG \(lastCOB)"
- }
- if let lastUAM = lastUAMpredBG {
- reason += ", UAMpredBG \(lastUAM)"
- }
- reason += tddReason
- reason += "; " // Start of conclusion
- let carbsRequiredResult = calculateCarbsRequired(
- profile: profile,
- mealData: mealData,
- naiveEventualGlucose: naiveEventualGlucose,
- minGuardGlucose: forecast.minGuardGlucose,
- threshold: threshold,
- iobForecast: forecast.iob,
- cobForecast: forecast.internalCob,
- carbImpact: forecast.carbImpact,
- remainingCarbImpactPeak: forecast.remainingCarbImpactPeak,
- currentBasal: currentBasal,
- overrideFactor: overrideFactor,
- adjustedSensitivity: adjustedSensitivity,
- adjustedCarbRatio: forecast.adjustedCarbRatio
- )
- if let result = carbsRequiredResult {
- reason += "\(result.carbs) add'l carbs req w/in \(result.minutes)m; "
- }
- return DosingInputs(reason: reason, carbsRequired: carbsRequiredResult)
- }
- /// 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.
- static func calculateCarbsRequired(
- profile: Profile,
- mealData: ComputedCarbs,
- naiveEventualGlucose: Decimal,
- minGuardGlucose: Decimal,
- threshold: Decimal,
- iobForecast: [Decimal],
- cobForecast: [Decimal],
- carbImpact: Decimal,
- remainingCarbImpactPeak: Decimal,
- currentBasal: Decimal,
- overrideFactor: Decimal,
- adjustedSensitivity: Decimal,
- adjustedCarbRatio: Decimal
- ) -> (carbs: Decimal, minutes: Decimal)? {
- var carbsRequiredGlucose = naiveEventualGlucose
- if naiveEventualGlucose < 40 {
- carbsRequiredGlucose = min(minGuardGlucose, naiveEventualGlucose)
- }
- let glucoseUndershoot = threshold - carbsRequiredGlucose
- var minutesAboveThreshold = Decimal(240)
- let useCOBForecast = mealData.mealCOB > 0 && (carbImpact > 0 || remainingCarbImpactPeak > 0)
- let forecast = useCOBForecast ? cobForecast : iobForecast
- // At this point in the JS the forecasts have already been rounded
- for (index, glucose) in forecast.map({ $0.jsRounded() }).enumerated() {
- if glucose < threshold {
- minutesAboveThreshold = Decimal(5) * Decimal(index)
- break
- }
- }
- let zeroTempDuration = minutesAboveThreshold
- let zeroTempEffect = currentBasal * adjustedSensitivity * overrideFactor * zeroTempDuration / 60
- let mealCarbs = mealData.carbs
- let cobForCarbsRequired = max(0, mealData.mealCOB - (Decimal(0.25) * mealCarbs))
- guard adjustedCarbRatio > 0 else { return nil }
- let carbSensitivityFactor = adjustedSensitivity / adjustedCarbRatio
- guard carbSensitivityFactor > 0 else { return nil }
- 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
- }
- /// Determines if a low glucose suspend is warranted.
- ///
- /// This function checks for low glucose conditions and may modify the determination object
- /// with a suspend recommendation and an updated reason string.
- ///
- /// - Returns: A tuple containing:
- /// - `setTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
- /// - `determination`: The (potentially modified) determination object.
- static func lowGlucoseSuspend(
- currentGlucose: Decimal,
- minGuardGlucose: Decimal,
- iob: Decimal,
- minDelta: Decimal,
- expectedDelta: Decimal,
- threshold: Decimal,
- overrideFactor: Decimal,
- profile: Profile,
- adjustedSensitivity: Decimal,
- targetGlucose: Decimal,
- currentTemp: TempBasal,
- determination: Determination
- ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
- var newDetermination = determination
- guard let currentBasal = profile.currentBasal else {
- // Should have been checked earlier
- throw TempBasalFunctionError.invalidBasalRateOnProfile
- }
- let suspendThreshold = -currentBasal * overrideFactor * 20 / 60
- if currentGlucose < threshold, iob < suspendThreshold, minDelta > 0, minDelta > expectedDelta {
- let iobString = String(describing: iob)
- let suspendString = String(describing: suspendThreshold.jsRounded(scale: 2))
- let minDeltaString = String(describing: convertGlucose(profile: profile, glucose: minDelta))
- let expectedDeltaString = String(describing: convertGlucose(profile: profile, glucose: expectedDelta))
- newDetermination
- .reason +=
- "IOB \(iobString) < \(suspendString) and minDelta \(minDeltaString) > expectedDelta \(expectedDeltaString); "
- return (shouldSetTempBasal: false, determination: newDetermination)
- } else if currentGlucose < threshold || minGuardGlucose < threshold {
- let minGuardGlucoseString = String(describing: convertGlucose(profile: profile, glucose: minGuardGlucose))
- let thresholdString = String(describing: convertGlucose(profile: profile, glucose: threshold))
- newDetermination.reason += "minGuardBG \(minGuardGlucoseString) < \(thresholdString)"
- let glucoseUndershoot = targetGlucose - minGuardGlucose
- if minGuardGlucose < threshold {
- newDetermination.minGuardBG = minGuardGlucose
- }
- let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
- var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
- durationRequired = (durationRequired / 30).jsRounded() * 30
- durationRequired = max(30, min(120, durationRequired))
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: 0,
- duration: durationRequired,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- return (shouldSetTempBasal: false, determination: determination)
- }
- /// Determines if a neutral temp basal should be skipped to avoid pump alerts.
- ///
- /// - Returns: A tuple containing:
- /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
- /// - `determination`: The (potentially modified) determination object.
- static func skipNeutralTempBasal(
- smbIsEnabled: Bool,
- profile: Profile,
- clock: Date,
- currentTemp: TempBasal,
- determination: Determination
- ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
- guard profile.skipNeutralTemps else {
- return (shouldSetTempBasal: false, determination: determination)
- }
- guard let totalMinutes = clock.minutesSinceMidnight else {
- throw CalendarError.invalidCalendar
- }
- let minute = totalMinutes % 60
- guard minute >= 55 else {
- return (shouldSetTempBasal: false, determination: determination)
- }
- if !smbIsEnabled {
- var newDetermination = determination
- let minutesLeft = 60 - minute
- newDetermination
- .reason +=
- "; Canceling temp at \(minutesLeft)min before turn of the hour to avoid beeping of MDT. SMB are disabled anyways."
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: 0,
- duration: 0,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- } else {
- // In the JS, this path logs to the console but does not modify determination.
- // We will do nothing here to match that behavior.
- return (shouldSetTempBasal: false, determination: determination)
- }
- }
- /// Handles the case where eventual glucose is predicted to be low.
- ///
- /// - Returns: A tuple containing:
- /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
- /// - `determination`: The (potentially modified) determination object.
- static func handleLowEventualGlucose(
- eventualGlucose: Decimal,
- minGlucose: Decimal,
- targetGlucose: Decimal,
- minDelta: Decimal,
- expectedDelta: Decimal,
- carbsRequired: Decimal,
- naiveEventualGlucose: Decimal,
- glucoseStatus: GlucoseStatus,
- currentTemp: TempBasal,
- basal: Decimal,
- profile: Profile,
- determination: Determination,
- adjustedSensitivity: Decimal,
- overrideFactor: Decimal
- ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
- guard eventualGlucose < minGlucose else {
- return (shouldSetTempBasal: false, determination: determination)
- }
- var newDetermination = determination
- newDetermination
- .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 minDelta > expectedDelta, minDelta > 0, carbsRequired == 0 {
- if naiveEventualGlucose < 40 {
- newDetermination.reason += ", naive_eventualBG < 40. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: 0,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- if glucoseStatus.delta > minDelta {
- newDetermination
- .reason +=
- ", but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) > expectedDelta \(convertGlucose(profile: profile, glucose: expectedDelta))"
- } else {
- newDetermination
- .reason +=
- ", but Min. Delta \(minDelta.jsRounded(scale: 2)) > Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
- }
- let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
- let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
- if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
- newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
- return (shouldSetTempBasal: true, determination: newDetermination)
- } else {
- newDetermination.reason += "; setting current basal of \(basal) as temp. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: basal,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- }
- // calculate 30m low-temp required to get projected BG up to target
- var insulinRequired = 2 * min(0, (eventualGlucose - targetGlucose) / adjustedSensitivity)
- insulinRequired = insulinRequired.jsRounded(scale: 2)
- let naiveInsulinRequired = min(0, (naiveEventualGlucose - targetGlucose) / adjustedSensitivity).jsRounded(scale: 2)
- if minDelta < 0, minDelta > expectedDelta {
- let newInsulinRequired = (insulinRequired * (minDelta / expectedDelta)).jsRounded(scale: 2)
- insulinRequired = newInsulinRequired
- }
- var rate = basal + (2 * insulinRequired)
- rate = TempBasalFunctions.roundBasal(profile: profile, basalRate: rate)
- let insulinScheduled = Decimal(currentTemp.duration) * (currentTemp.rate - basal) / 60
- let minInsulinRequired = min(insulinRequired, naiveInsulinRequired)
- if insulinScheduled < minInsulinRequired - basal * 0.3 {
- newDetermination
- .reason += ", \(currentTemp.duration)m@\(currentTemp.rate.jsRounded(scale: 2)) is a lot less than needed. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: rate,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- if currentTemp.duration > 5, rate >= currentTemp.rate * 0.8 {
- newDetermination.reason += ", temp \(currentTemp.rate) ~< req \(rate)U/hr. "
- return (shouldSetTempBasal: true, determination: newDetermination)
- } else {
- if rate <= 0 {
- guard let currentBasal = profile.currentBasal else {
- throw TempBasalFunctionError.invalidBasalRateOnProfile
- }
- let glucoseUndershoot = targetGlucose - naiveEventualGlucose
- let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
- var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
- if durationRequired < 0 {
- durationRequired = 0
- } else {
- durationRequired = (durationRequired / 30).jsRounded() * 30
- durationRequired = min(120, max(0, durationRequired))
- }
- if durationRequired > 0 {
- newDetermination.reason += ", setting \(durationRequired)m zero temp. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: rate,
- duration: durationRequired,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- } else {
- newDetermination.reason += ", setting \(rate)U/hr. "
- }
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: rate,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- }
- /// Handles the case where glucose is falling faster than expected.
- ///
- /// - Returns: A tuple containing:
- /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
- /// - `determination`: The (potentially modified) determination object.
- static func glucoseFallingFasterThanExpected(
- eventualGlucose: Decimal,
- minGlucose: Decimal,
- minDelta: Decimal,
- expectedDelta: Decimal,
- glucoseStatus: GlucoseStatus,
- currentTemp: TempBasal,
- basal: Decimal,
- smbIsEnabled: Bool,
- profile: Profile,
- determination: Determination
- ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
- guard minDelta < expectedDelta else {
- return (shouldSetTempBasal: false, determination: determination)
- }
- var newDetermination = determination
- if !smbIsEnabled {
- if glucoseStatus.delta < minDelta {
- newDetermination
- .reason +=
- "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
- } else {
- newDetermination
- .reason +=
- "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Min. Delta \(minDelta.jsRounded(scale: 2)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
- }
- let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
- let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
- if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
- newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
- return (shouldSetTempBasal: true, determination: newDetermination)
- } else {
- newDetermination.reason += "; setting current basal of \(basal) as temp. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: basal,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- }
- return (shouldSetTempBasal: false, determination: determination)
- }
- /// Handles the case where the eventual or forecasted glucose is less than the max glucose.
- ///
- /// - Returns: A tuple containing:
- /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
- /// - `determination`: The (potentially modified) determination object.
- static func eventualOrForecastGlucoseLessThanMax(
- eventualGlucose: Decimal,
- maxGlucose: Decimal,
- minForecastGlucose: Decimal,
- currentTemp: TempBasal,
- basal: Decimal,
- smbIsEnabled: Bool,
- profile: Profile,
- determination: Determination
- ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
- guard min(eventualGlucose, minForecastGlucose) < maxGlucose else {
- return (shouldSetTempBasal: false, determination: determination)
- }
- var newDetermination = determination
- newDetermination.minPredBG = minForecastGlucose
- if !smbIsEnabled {
- newDetermination
- .reason +=
- "\(convertGlucose(profile: profile, glucose: eventualGlucose))-\(convertGlucose(profile: profile, glucose: minForecastGlucose)) in range: no temp required"
- let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
- let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
- if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
- newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
- return (shouldSetTempBasal: true, determination: newDetermination)
- } else {
- newDetermination.reason += "; setting current basal of \(basal) as temp. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: basal,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- }
- return (shouldSetTempBasal: false, determination: determination)
- }
- /// Handles the case where IOB is greater than the max IOB.
- ///
- /// - Returns: A tuple containing:
- /// - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
- /// - `determination`: The (potentially modified) determination object.
- static func iobGreaterThanMax(
- iob: Decimal,
- maxIob: Decimal,
- currentTemp: TempBasal,
- basal: Decimal,
- profile: Profile,
- determination: Determination
- ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
- guard iob > maxIob else {
- return (shouldSetTempBasal: false, determination: determination)
- }
- var newDetermination = determination
- newDetermination.reason += "IOB \(iob.jsRounded(scale: 2)) > max_iob \(maxIob)"
- let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
- let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
- if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
- newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
- return (shouldSetTempBasal: true, determination: newDetermination)
- } else {
- newDetermination.reason += "; setting current basal of \(basal) as temp. "
- let finalDetermination = try TempBasalFunctions.setTempBasal(
- rate: basal,
- duration: 30,
- profile: profile,
- determination: newDetermination,
- currentTemp: currentTemp
- )
- return (shouldSetTempBasal: true, determination: finalDetermination)
- }
- }
- }
|