Skip to content

Commit

Permalink
LOOP-4781 Loop to use LoopAlgorithm swift package (#617)
Browse files Browse the repository at this point in the history
* Adding testRunWithOngoingTempBasal

* Building with LoopAlgorithm swift package

* fix merge

* Tests moved to LoopAlgorithm package

* Fix warning

* Adding cancel helper for TempBasalRecommendation

* Add TempBasalRecommendationTests for extensions

* Add SimpleInsulinDose so we can specify which fast acting model to LoopAlgorithm

* Remove unused imports

* Update for LoopAlgorithm using doses with insulin models

* Updates from PR review

* Fix signature of method call, and cleanup unused method

* Unused var

* Remove unused parts of method for clarity
  • Loading branch information
ps2 authored Mar 5, 2024
1 parent 41c8031 commit 0bd52ba
Show file tree
Hide file tree
Showing 76 changed files with 1,368 additions and 1,195 deletions.
2 changes: 1 addition & 1 deletion Common/Extensions/SampleValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import HealthKit
import LoopKit

import LoopAlgorithm

extension Collection where Element == SampleValue {
/// O(n)
Expand Down
1 change: 1 addition & 0 deletions Common/Models/StatusExtensionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Foundation
import HealthKit
import LoopKit
import LoopKitUI
import LoopAlgorithm


struct NetBasalContext {
Expand Down
1 change: 1 addition & 0 deletions Common/Models/WatchContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import HealthKit
import LoopKit
import LoopAlgorithm


final class WatchContext: RawRepresentable {
Expand Down
1 change: 1 addition & 0 deletions Common/Models/WatchHistoricalGlucose.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import HealthKit
import LoopKit
import LoopAlgorithm

struct WatchHistoricalGlucose {
let samples: [StoredGlucoseSample]
Expand Down
1 change: 1 addition & 0 deletions Common/Models/WatchPredictedGlucose.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import LoopKit
import HealthKit
import LoopAlgorithm


struct WatchPredictedGlucose: Equatable {
Expand Down
1 change: 0 additions & 1 deletion Learn/Managers/DataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ final class DataManager {
healthStore: healthStore,
cacheStore: cacheStore,
observationEnabled: false,
insulinModelProvider: PresetInsulinModelProvider(defaultRapidActingModel: defaultRapidActingModel),
longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration,
basalProfile: basalRateSchedule,
insulinSensitivitySchedule: insulinSensitivitySchedule,
Expand Down
13 changes: 1 addition & 12 deletions Loop Status Extension/StatusViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import LoopUI
import NotificationCenter
import UIKit
import SwiftCharts
import LoopAlgorithm

class StatusViewController: UIViewController, NCWidgetProviding {

Expand Down Expand Up @@ -91,7 +92,6 @@ class StatusViewController: UIViewController, NCWidgetProviding {

lazy var doseStore = DoseStore(
cacheStore: cacheStore,
insulinModelProvider: PresetInsulinModelProvider(defaultRapidActingModel: settingsStore.latestSettings?.defaultRapidActingModel?.presetForRapidActingInsulin),
longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration,
basalProfile: settingsStore.latestSettings?.basalRateSchedule,
insulinSensitivitySchedule: settingsStore.latestSettings?.insulinSensitivitySchedule,
Expand Down Expand Up @@ -187,17 +187,6 @@ class StatusViewController: UIViewController, NCWidgetProviding {
var activeInsulin: Double?
let carbUnit = HKUnit.gram()
var glucose: [StoredGlucoseSample] = []

group.enter()
doseStore.insulinOnBoard(at: Date()) { (result) in
switch result {
case .success(let iobValue):
activeInsulin = iobValue.value
case .failure:
activeInsulin = nil
}
group.leave()
}

charts.startDate = Calendar.current.nextDate(after: Date(timeIntervalSinceNow: .minutes(-5)), matching: DateComponents(minute: 0), matchingPolicy: .strict, direction: .backward) ?? Date()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import HealthKit
import LoopCore
import LoopKit
import WidgetKit
import LoopAlgorithm


struct StatusWidgetTimelimeEntry: TimelineEntry {
var date: Date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LoopCore
import LoopKit
import OSLog
import WidgetKit
import LoopAlgorithm

class StatusWidgetTimelineProvider: TimelineProvider {
lazy var defaults = UserDefaults.appGroup
Expand Down
38 changes: 23 additions & 15 deletions Loop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Loop/Extensions/BasalDeliveryState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import LoopKit
import LoopCore
import LoopAlgorithm

extension PumpManagerStatus.BasalDeliveryState {
func getNetBasal(basalSchedule: BasalRateSchedule, maximumBasalRatePerHour: Double?) -> NetBasal? {
Expand Down
51 changes: 51 additions & 0 deletions Loop/Extensions/BasalRelativeDose.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// BasalRelativeDose.swift
// Loop
//
// Created by Pete Schwamb on 2/12/24.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation
import LoopAlgorithm

public extension Array where Element == BasalRelativeDose {
func trimmed(from start: Date? = nil, to end: Date? = nil) -> [BasalRelativeDose] {
return self.compactMap { (dose) -> BasalRelativeDose? in
if let start, dose.endDate < start {
return nil
}
if let end, dose.startDate > end {
return nil
}
if dose.type == .bolus {
// Do not split boluses
return dose
}
return dose.trimmed(from: start, to: end)
}
}
}

extension BasalRelativeDose {
public func trimmed(from start: Date? = nil, to end: Date? = nil, syncIdentifier: String? = nil) -> BasalRelativeDose {

let originalDuration = endDate.timeIntervalSince(startDate)

let startDate = max(start ?? .distantPast, self.startDate)
let endDate = max(startDate, min(end ?? .distantFuture, self.endDate))

var trimmedVolume: Double = volume

if originalDuration > .ulpOfOne && (startDate > self.startDate || endDate < self.endDate) {
trimmedVolume = volume * (endDate.timeIntervalSince(startDate) / originalDuration)
}

return BasalRelativeDose(
type: self.type,
startDate: startDate,
endDate: endDate,
volume: trimmedVolume
)
}
}
1 change: 1 addition & 0 deletions Loop/Extensions/CollectionType+Loop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import LoopKit
import LoopAlgorithm


public extension Sequence where Element: TimelineValue {
Expand Down
3 changes: 2 additions & 1 deletion Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import HealthKit
import LoopKit
import LoopAlgorithm

// MARK: - Simulated Core Data

Expand Down Expand Up @@ -168,7 +169,7 @@ fileprivate extension StoredDosingDecision {
duration: .minutes(30)),
bolusUnits: 1.25)
let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 0.2,
notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: date.addingTimeInterval(.minutes(30)),
notice: .predictedGlucoseBelowTarget(minGlucose: SimpleGlucoseValue(startDate: date.addingTimeInterval(.minutes(30)),
quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 95.0)))),
date: date.addingTimeInterval(-.minutes(1)))
let manualBolusRequested = 0.5
Expand Down
1 change: 1 addition & 0 deletions Loop/Extensions/SettingsStore+SimulatedCoreData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import HealthKit
import LoopKit
import LoopAlgorithm

// MARK: - Simulated Core Data

Expand Down
67 changes: 67 additions & 0 deletions Loop/Extensions/TempBasalRecommendation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// TempBasalRecommendation.swift
// Loop
//
// Created by Pete Schwamb on 2/9/24.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation
import LoopKit
import LoopAlgorithm

extension TempBasalRecommendation {
/// Equates the recommended rate with another rate
///
/// - Parameter unitsPerHour: The rate to compare
/// - Returns: Whether the rates are equal within Double precision
private func matchesRate(_ unitsPerHour: Double) -> Bool {
return abs(self.unitsPerHour - unitsPerHour) < .ulpOfOne
}

/// Adjusts a recommendation based on the current state of pump delivery. If the current temp basal matches
/// the recommendation, and enough time is remaining, then recommend no action. If we are running a temp basal
/// and the new rate matches the scheduled rate, then cancel the currently running temp basal. If the current scheduled
/// rate matches the recommended rate, then recommend no action. Otherwise, set a new temp basal of the
/// recommended rate.
///
/// - Parameters:
/// - date: The date the recommendation would be delivered
/// - neutralBasalRate: The scheduled basal rate at `date`
/// - lastTempBasal: The previously set temp basal
/// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command
/// - neutralBasalRateMatchesPump: A flag describing whether `neutralBasalRate` matches the scheduled basal rate of the pump.
/// If `false` and the recommendation matches `neutralBasalRate`, the temp will be recommended
/// at the scheduled basal rate rather than recommending no temp.
/// - Returns: A temp basal recommendation
func adjustForCurrentDelivery(
at date: Date,
neutralBasalRate: Double,
currentTempBasal: DoseEntry?,
continuationInterval: TimeInterval,
neutralBasalRateMatchesPump: Bool
) -> TempBasalRecommendation? {
// Adjust behavior for the currently active temp basal
if let currentTempBasal, currentTempBasal.type == .tempBasal, currentTempBasal.endDate > date
{
/// If the last temp basal has the same rate, and has more than `continuationInterval` of time remaining, don't set a new temp
if matchesRate(currentTempBasal.unitsPerHour),
currentTempBasal.endDate.timeIntervalSince(date) > continuationInterval {
return nil
} else if matchesRate(neutralBasalRate), neutralBasalRateMatchesPump {
// If our new temp matches the scheduled rate of the pump, cancel the current temp
return .cancel
}
} else if matchesRate(neutralBasalRate), neutralBasalRateMatchesPump {
// If we recommend the in-progress scheduled basal rate of the pump, do nothing
return nil
}

return self
}

public static var cancel: TempBasalRecommendation {
return self.init(unitsPerHour: 0, duration: 0)
}
}

1 change: 1 addition & 0 deletions Loop/Extensions/UserDefaults+Loop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import LoopKit
import LoopAlgorithm


extension UserDefaults {
Expand Down
7 changes: 4 additions & 3 deletions Loop/Managers/AppExpirationAlerter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ class AppExpirationAlerter {
static func isTestFlightBuild() -> Bool {
// If the target environment is a simulator, then
// this is not a TestFlight distribution. Return false.
#if targetEnvironment(simulator)
return false
#endif
#if targetEnvironment(simulator)
return false
#else

// If an "embedded.mobileprovision" is present in the main bundle, then
// this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false.
Expand All @@ -143,6 +143,7 @@ class AppExpirationAlerter {
// A TestFlight distribution presents a "sandboxReceipt", while an App Store
// distribution presents a "receipt". Return true if we have a TestFlight receipt.
return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame
#endif
}

static func calculateExpirationDate(profileExpiration: Date) -> Date {
Expand Down
1 change: 1 addition & 0 deletions Loop/Managers/CGMStalenessMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import LoopKit
import LoopCore
import LoopAlgorithm

protocol CGMStalenessMonitorDelegate: AnyObject {
func getLatestCGMGlucose(since: Date, completion: @escaping (_ result: Swift.Result<StoredGlucoseSample?, Error>) -> Void)
Expand Down
5 changes: 3 additions & 2 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import LoopCore
import LoopTestingKit
import UserNotifications
import Combine
import LoopAlgorithm

protocol LoopControl {
var lastLoopCompleted: Date? { get }
Expand Down Expand Up @@ -1397,15 +1398,15 @@ extension DeviceDataManager: DeliveryDelegate {
return pumpManager.roundToSupportedBolusVolume(units: units)
}

var pumpInsulinType: LoopKit.InsulinType? {
var pumpInsulinType: InsulinType? {
return pumpManager?.status.insulinType
}

var isSuspended: Bool {
return pumpManager?.status.basalDeliveryState?.isSuspended ?? false
}

func enact(_ recommendation: LoopKit.AutomaticDoseRecommendation) async throws {
func enact(_ recommendation: AutomaticDoseRecommendation) async throws {
guard let pumpManager = pumpManager else {
throw LoopError.configurationError(.pumpManager)
}
Expand Down
1 change: 1 addition & 0 deletions Loop/Managers/DoseEnactor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import LoopKit
import LoopAlgorithm

class DoseEnactor {

Expand Down
17 changes: 2 additions & 15 deletions Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import MockKit
import HealthKit
import WidgetKit
import LoopCore

import LoopAlgorithm

#if targetEnvironment(simulator)
enum SimulatorError: Error {
Expand Down Expand Up @@ -228,8 +228,6 @@ class LoopAppManager: NSObject {
observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval)
)

let absorptionTimes = LoopCoreConstants.defaultCarbAbsorptionTimes

temporaryPresetsManager = TemporaryPresetsManager(settingsProvider: settingsManager)
temporaryPresetsManager.overrideHistory.delegate = self

Expand All @@ -239,9 +237,7 @@ class LoopAppManager: NSObject {
self.carbStore = CarbStore(
healthKitSampleStore: carbHealthStore,
cacheStore: cacheStore,
cacheLength: localCacheDuration,
defaultAbsorptionTimes: absorptionTimes,
carbAbsorptionModel: FeatureFlags.nonlinearCarbModelEnabled ? .piecewiseLinear : .linear
cacheLength: localCacheDuration
)

let insulinHealthStore = HealthKitSampleStore(
Expand All @@ -251,19 +247,10 @@ class LoopAppManager: NSObject {
observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval)
)

let insulinModelProvider: InsulinModelProvider

if FeatureFlags.adultChildInsulinModelSelectionEnabled {
insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: settingsManager.settings.defaultRapidActingModel?.presetForRapidActingInsulin)
} else {
insulinModelProvider = PresetInsulinModelProvider(defaultRapidActingModel: nil)
}

self.doseStore = DoseStore(
healthKitSampleStore: insulinHealthStore,
cacheStore: cacheStore,
cacheLength: localCacheDuration,
insulinModelProvider: insulinModelProvider,
longestEffectDuration: ExponentialInsulinModelPreset.rapidActingAdult.effectDuration,
basalProfile: settingsManager.settings.basalRateSchedule,
lastPumpEventsReconciliation: nil // PumpManager is nil at this point. Will update this via addPumpEvents below
Expand Down
Loading

0 comments on commit 0bd52ba

Please sign in to comment.