diff --git a/Common/Extensions/GlucoseRangeSchedule.swift b/Common/Extensions/GlucoseRangeSchedule.swift index a7bfef414f..4a1d04c3f3 100644 --- a/Common/Extensions/GlucoseRangeSchedule.swift +++ b/Common/Extensions/GlucoseRangeSchedule.swift @@ -6,7 +6,6 @@ // import LoopKit -import LoopAlgorithm extension GlucoseRangeSchedule { diff --git a/Common/Extensions/HKUnit.swift b/Common/Extensions/HKUnit.swift index 69cf8fb40c..004c1164a4 100644 --- a/Common/Extensions/HKUnit.swift +++ b/Common/Extensions/HKUnit.swift @@ -6,7 +6,7 @@ // Copyright © 2016 LoopKit Authors. All rights reserved. // -import LoopAlgorithm +import LoopKit import LoopCore // Code in this extension is duplicated from: diff --git a/Common/Extensions/NumberFormatter.swift b/Common/Extensions/NumberFormatter.swift index c0f8c3addb..0b7cd834b8 100644 --- a/Common/Extensions/NumberFormatter.swift +++ b/Common/Extensions/NumberFormatter.swift @@ -7,7 +7,7 @@ // import Foundation -import LoopAlgorithm +import LoopKit extension NumberFormatter { diff --git a/Common/Extensions/SampleValue.swift b/Common/Extensions/SampleValue.swift index 40d9faab4d..d8c0e87699 100644 --- a/Common/Extensions/SampleValue.swift +++ b/Common/Extensions/SampleValue.swift @@ -6,7 +6,6 @@ // import LoopKit -import LoopAlgorithm extension SampleValue { /// A glucose value entered by hand, such as a fingerstick or calibration. diff --git a/Common/LoopKitTypeAliases.swift b/Common/LoopKitTypeAliases.swift new file mode 100644 index 0000000000..3bc59914a6 --- /dev/null +++ b/Common/LoopKitTypeAliases.swift @@ -0,0 +1,34 @@ +// +// LoopKitTypeAliases.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import LoopKit + +// LoopKit and LoopAlgorithm define types with the same names. These make the unqualified names +// mean LoopKit's; algorithm types are written LoopAlgorithm.Name. +typealias AbsoluteScheduleValue = LoopKit.AbsoluteScheduleValue +typealias AutomaticDoseRecommendation = LoopKit.AutomaticDoseRecommendation +typealias BolusRecommendationNotice = LoopKit.BolusRecommendationNotice +typealias CarbEntry = LoopKit.CarbEntry +typealias CarbValue = LoopKit.CarbValue +typealias DoseUnit = LoopKit.DoseUnit +typealias ExponentialInsulinModel = LoopKit.ExponentialInsulinModel +typealias ExponentialInsulinModelPreset = LoopKit.ExponentialInsulinModelPreset +typealias GlucoseChange = LoopKit.GlucoseChange +typealias GlucoseCondition = LoopKit.GlucoseCondition +typealias GlucoseEffect = LoopKit.GlucoseEffect +typealias GlucoseEffectVelocity = LoopKit.GlucoseEffectVelocity +typealias GlucoseRangeTimeline = LoopKit.GlucoseRangeTimeline +typealias GlucoseSampleValue = LoopKit.GlucoseSampleValue +typealias GlucoseValue = LoopKit.GlucoseValue +typealias InsulinModel = LoopKit.InsulinModel +typealias InsulinValue = LoopKit.InsulinValue +typealias ManualBolusRecommendation = LoopKit.ManualBolusRecommendation +typealias PredictedGlucoseValue = LoopKit.PredictedGlucoseValue +typealias SampleValue = LoopKit.SampleValue +typealias SimpleGlucoseValue = LoopKit.SimpleGlucoseValue +typealias TempBasalRecommendation = LoopKit.TempBasalRecommendation +typealias TimelineValue = LoopKit.TimelineValue diff --git a/Common/Models/StatusExtensionContext.swift b/Common/Models/StatusExtensionContext.swift index 114f2a416f..dc216bd731 100644 --- a/Common/Models/StatusExtensionContext.swift +++ b/Common/Models/StatusExtensionContext.swift @@ -8,10 +8,8 @@ // This class allows Loop to pass context data to the Loop Status Extension. import Foundation -import LoopAlgorithm import LoopKit import LoopKitUI -import LoopAlgorithm struct NetBasalContext { diff --git a/Common/Models/WatchHistoricalGlucose.swift b/Common/Models/WatchHistoricalGlucose.swift index 71209d428d..d360ec6f26 100644 --- a/Common/Models/WatchHistoricalGlucose.swift +++ b/Common/Models/WatchHistoricalGlucose.swift @@ -9,7 +9,6 @@ import Foundation import HealthKit import LoopKit -import LoopAlgorithm struct WatchHistoricalGlucose { let samples: [StoredGlucoseSample] diff --git a/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift b/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift index 314d0de950..071c16d6ad 100644 --- a/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift +++ b/Loop Widget Extension/Live Activity/GlucoseLiveActivityConfiguration.swift @@ -9,7 +9,6 @@ import ActivityKit import Charts import HealthKit -import LoopAlgorithm import LoopCore import LoopKit import SwiftUI diff --git a/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift b/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift index 4fe09ba12f..657eecb28d 100644 --- a/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift +++ b/Loop Widget Extension/Timeline/StatusWidgetTimelimeEntry.swift @@ -9,7 +9,6 @@ import LoopCore import LoopKit import WidgetKit -import LoopAlgorithm struct StatusWidgetTimelimeEntry: TimelineEntry { @@ -56,6 +55,6 @@ struct StatusWidgetTimelimeEntry: TimelineEntry { } let glucoseAge = date - glucoseDate - return glucoseAge >= LoopAlgorithm.inputDataRecencyInterval + return glucoseAge >= LoopCoreConstants.inputDataRecencyInterval } } diff --git a/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift b/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift index bfc46aeaa0..cb0c7c5ce0 100644 --- a/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift +++ b/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift @@ -11,7 +11,6 @@ import LoopCore import LoopKit import OSLog import WidgetKit -import LoopAlgorithm class StatusWidgetTimelineProvider: TimelineProvider { lazy var defaults = UserDefaults.appGroup @@ -74,7 +73,7 @@ class StatusWidgetTimelineProvider: TimelineProvider { // Date glucose staleness changes if let lastBGTime = newEntry.currentGlucose?.startDate { - let staleBgRefreshTime = lastBGTime.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval+1) + let staleBgRefreshTime = lastBGTime.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval+1) datesToRefreshWidget.append(staleBgRefreshTime) } @@ -97,7 +96,7 @@ class StatusWidgetTimelineProvider: TimelineProvider { func update(completion: @escaping (StatusWidgetTimelimeEntry) -> Void) { - let startDate = Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval) + let startDate = Date(timeIntervalSinceNow: -LoopCoreConstants.inputDataRecencyInterval) Task { diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 03ce9afa1d..b147fc93e2 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -717,6 +717,15 @@ FEEDB0000000000000000004 /* Statistics/TimeInRangeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000004 /* Statistics/TimeInRangeBar.swift */; }; 30E4A68B8C914C5E4B245555 /* InsulinDeliveryHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F69CE2EE3714858772FE7A26 /* InsulinDeliveryHistoryTests.swift */; }; FEEDB0000000000000000005 /* GlucoseStatisticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEEDF0000000000000000005 /* GlucoseStatisticsTests.swift */; }; + 17EA66859884C0208DED47CF /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; + 1C99EE4A3BB181BB23DEF066 /* LoopAlgorithm in Frameworks */ = {isa = PBXBuildFile; productRef = C42F6B8A2B1B9E0142078BF9 /* LoopAlgorithm */; }; + BD37CA2F10445DB600A35199 /* TemporaryScheduleOverrideAlgorithmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C372374D3D3D9C5F8E3AD93 /* TemporaryScheduleOverrideAlgorithmTests.swift */; }; + 5E77F6516D69B22A57EB850A /* LoopAlgorithm in Frameworks */ = {isa = PBXBuildFile; productRef = 761F6A285BE2833DF06F7E55 /* LoopAlgorithm */; }; + 613BF2B78104AB9694590C65 /* AlgorithmBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */; }; + 767B36174D190F3191F2EA73 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; + 8DC1CE32F39301122B550325 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; + ABD9218EB98C9216FBB04382 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; + AF79DFAB0F55143D66022668 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1584,6 +1593,9 @@ FEEDF0000000000000000004 /* Statistics/TimeInRangeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics/TimeInRangeBar.swift; sourceTree = ""; }; F69CE2EE3714858772FE7A26 /* InsulinDeliveryHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryHistoryTests.swift; sourceTree = ""; }; FEEDF0000000000000000005 /* GlucoseStatisticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatisticsTests.swift; sourceTree = ""; }; + 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoopKitTypeAliases.swift; sourceTree = ""; }; + 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AlgorithmBridge.swift; sourceTree = ""; }; + 6C372374D3D3D9C5F8E3AD93 /* TemporaryScheduleOverrideAlgorithmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryScheduleOverrideAlgorithmTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1637,6 +1649,7 @@ files = ( 43D9002D21EB225D00AF44BF /* HealthKit.framework in Frameworks */, C1BE2ADD2FCDE9F700D5A369 /* LoopKit.framework in Frameworks */, + 1C99EE4A3BB181BB23DEF066 /* LoopAlgorithm in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1645,6 +1658,7 @@ buildActionMask = 2147483647; files = ( C1275DDE2E81FD470013B99D /* LoopKit.framework in Frameworks */, + 5E77F6516D69B22A57EB850A /* LoopAlgorithm in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1856,6 +1870,7 @@ C16F511A2B89363A00EFD7A1 /* SimpleInsulinDose.swift */, C152B9F42C9C7D4A00ACBC06 /* AutomationHistoryEntry.swift */, 84213C882D94941000642E78 /* InsulinDeliveryLogEvent.swift */, + 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */, ); path = Models; sourceTree = ""; @@ -2363,6 +2378,7 @@ 43785E9B2120E7060057DED1 /* Intents.intentdefinition */, 89E267FB2292456700A3F2AF /* FeatureFlags.swift */, 7D9BEEF52335CF8D005DCFD6 /* Localizable.strings */, + 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */, ); path = Common; sourceTree = ""; @@ -2807,6 +2823,7 @@ A9BD28E6272226B40071DF15 /* TestLocalizedError.swift */, A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */, A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */, + 6C372374D3D3D9C5F8E3AD93 /* TemporaryScheduleOverrideAlgorithmTests.swift */, ); path = Models; sourceTree = ""; @@ -3092,6 +3109,9 @@ dependencies = ( ); name = "LoopCore-watchOS"; + packageProductDependencies = ( + C42F6B8A2B1B9E0142078BF9 /* LoopAlgorithm */, + ); productName = LoopCore; productReference = 43D9002A21EB209400AF44BF /* LoopCore.framework */; productType = "com.apple.product-type.framework"; @@ -3110,6 +3130,9 @@ dependencies = ( ); name = LoopCore; + packageProductDependencies = ( + 761F6A285BE2833DF06F7E55 /* LoopAlgorithm */, + ); productName = LoopCore; productReference = 43D9FFCF21EAE05D00AF44BF /* LoopCore.framework */; productType = "com.apple.product-type.framework"; @@ -3619,6 +3642,7 @@ 84AA81DD2A4A2999000B658B /* StatusWidgetTimelineProvider.swift in Sources */, 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */, 8496F7312B5711C4003E672C /* ContentMargin.swift in Sources */, + ABD9218EB98C9216FBB04382 /* LoopKitTypeAliases.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3893,6 +3917,8 @@ 43C2FAE11EB656A500364AFF /* GlucoseEffectVelocity.swift in Sources */, 14C970862C5C358C00E8A01B /* FavoriteFoodInsightsChartsView.swift in Sources */, 5021CA8C264146C9DFF33E9D /* PluginPickerView.swift in Sources */, + 17EA66859884C0208DED47CF /* LoopKitTypeAliases.swift in Sources */, + 613BF2B78104AB9694590C65 /* AlgorithmBridge.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3993,6 +4019,7 @@ 894F6DD3243BCBDB00CCE676 /* Environment+SizeClass.swift in Sources */, C17D52042E7F0578001D2AD2 /* LabelValueRow.swift in Sources */, 895788B2242E69A2002CB114 /* CircularAccessoryButtonStyle.swift in Sources */, + 8DC1CE32F39301122B550325 /* LoopKitTypeAliases.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4129,6 +4156,8 @@ E93E86B224DDE21D00FF40C8 /* MockCarbStore.swift in Sources */, C1DA43592B1A784900CBD33F /* MockDeliveryDelegate.swift in Sources */, C18859A02AF1612B0010F21F /* PersistenceController.swift in Sources */, + BD37CA2F10445DB600A35199 /* TemporaryScheduleOverrideAlgorithmTests.swift in Sources */, + AF79DFAB0F55143D66022668 /* LoopKitTypeAliases.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4173,6 +4202,7 @@ B4E96D57248A7B0F002DABAD /* StatusHighlightHUDView.swift in Sources */, B42C951424A3C76000857C73 /* CGMStatusHUDViewModel.swift in Sources */, 4F75289E1DFE1F6000C322D6 /* LoopCompletionHUDView.swift in Sources */, + 767B36174D190F3191F2EA73 /* LoopKitTypeAliases.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5510,6 +5540,14 @@ package = C1CCF10B2858F4F70035389C /* XCRemoteSwiftPackageReference "SwiftCharts" */; productName = SwiftCharts; }; + 761F6A285BE2833DF06F7E55 /* LoopAlgorithm */ = { + isa = XCSwiftPackageProductDependency; + productName = LoopAlgorithm; + }; + C42F6B8A2B1B9E0142078BF9 /* LoopAlgorithm */ = { + isa = XCSwiftPackageProductDependency; + productName = LoopAlgorithm; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Loop/Extensions/BasalDeliveryState.swift b/Loop/Extensions/BasalDeliveryState.swift index 32cf77c930..6b53f06e2b 100644 --- a/Loop/Extensions/BasalDeliveryState.swift +++ b/Loop/Extensions/BasalDeliveryState.swift @@ -8,7 +8,6 @@ import LoopKit import LoopCore -import LoopAlgorithm extension PumpManagerStatus.BasalDeliveryState { func getNetBasal(basalSchedule: BasalRateSchedule, maximumBasalRatePerHour: Double?) -> NetBasal? { diff --git a/Loop/Extensions/BasalRelativeDose.swift b/Loop/Extensions/BasalRelativeDose.swift index d78d5ad967..41497fdd38 100644 --- a/Loop/Extensions/BasalRelativeDose.swift +++ b/Loop/Extensions/BasalRelativeDose.swift @@ -7,6 +7,7 @@ // import Foundation +import LoopKit import LoopAlgorithm public extension Array where Element == BasalRelativeDose { diff --git a/Loop/Extensions/CarbStore+SimulatedCoreData.swift b/Loop/Extensions/CarbStore+SimulatedCoreData.swift index 71cca3c2fc..6640dc07f3 100644 --- a/Loop/Extensions/CarbStore+SimulatedCoreData.swift +++ b/Loop/Extensions/CarbStore+SimulatedCoreData.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit // MARK: - Simulated Core Data diff --git a/Loop/Extensions/CollectionType+Loop.swift b/Loop/Extensions/CollectionType+Loop.swift index 8740bdb453..7aedadc6b0 100644 --- a/Loop/Extensions/CollectionType+Loop.swift +++ b/Loop/Extensions/CollectionType+Loop.swift @@ -8,10 +8,9 @@ import Foundation import LoopKit -import LoopAlgorithm -public extension Sequence where Element: TimelineValue { +public extension Sequence where Element: LoopKit.TimelineValue { /// Returns the closest element index in the sorted sequence prior to the specified date /// /// - parameter date: The date to use in the search diff --git a/Loop/Extensions/DoseStore+SimulatedCoreData.swift b/Loop/Extensions/DoseStore+SimulatedCoreData.swift index 185bea71b9..0ca98d22f9 100644 --- a/Loop/Extensions/DoseStore+SimulatedCoreData.swift +++ b/Loop/Extensions/DoseStore+SimulatedCoreData.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit // MARK: - Simulated Core Data diff --git a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift index 83072b4701..f9ba288d06 100644 --- a/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift +++ b/Loop/Extensions/DosingDecisionStore+SimulatedCoreData.swift @@ -9,7 +9,6 @@ import Foundation import HealthKit import LoopKit -import LoopAlgorithm // MARK: - Simulated Core Data diff --git a/Loop/Extensions/Environment+SettingsManager.swift b/Loop/Extensions/Environment+SettingsManager.swift index 717726f025..a52626794e 100644 --- a/Loop/Extensions/Environment+SettingsManager.swift +++ b/Loop/Extensions/Environment+SettingsManager.swift @@ -8,7 +8,6 @@ import SwiftUI import LoopKit -import LoopAlgorithm @MainActor diff --git a/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift b/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift index 99cc96c19e..e579f4d928 100644 --- a/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift +++ b/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit // MARK: - Simulated Core Data diff --git a/Loop/Extensions/SettingsStore+SimulatedCoreData.swift b/Loop/Extensions/SettingsStore+SimulatedCoreData.swift index 4e430c0834..8fff07dbaf 100644 --- a/Loop/Extensions/SettingsStore+SimulatedCoreData.swift +++ b/Loop/Extensions/SettingsStore+SimulatedCoreData.swift @@ -9,7 +9,6 @@ import Foundation import HealthKit import LoopKit -import LoopAlgorithm // MARK: - Simulated Core Data diff --git a/Loop/Extensions/TempBasalRecommendation.swift b/Loop/Extensions/TempBasalRecommendation.swift index 0d7103b7f6..f5cae224c3 100644 --- a/Loop/Extensions/TempBasalRecommendation.swift +++ b/Loop/Extensions/TempBasalRecommendation.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm extension TempBasalRecommendation { /// Equates the recommended rate with another rate diff --git a/Loop/Extensions/UserDefaults+Loop.swift b/Loop/Extensions/UserDefaults+Loop.swift index 58e44a9b84..ec07d031ef 100644 --- a/Loop/Extensions/UserDefaults+Loop.swift +++ b/Loop/Extensions/UserDefaults+Loop.swift @@ -7,7 +7,6 @@ import Foundation import LoopKit -import LoopAlgorithm extension UserDefaults { diff --git a/Loop/Managers/Alerts/GlucoseAlertManager.swift b/Loop/Managers/Alerts/GlucoseAlertManager.swift index 01109738c2..be6dd09f95 100644 --- a/Loop/Managers/Alerts/GlucoseAlertManager.swift +++ b/Loop/Managers/Alerts/GlucoseAlertManager.swift @@ -11,7 +11,6 @@ import Combine import Foundation -import LoopAlgorithm import LoopKit import LoopKitUI import os.log diff --git a/Loop/Managers/AnalyticsServicesManager.swift b/Loop/Managers/AnalyticsServicesManager.swift index bfa9392e67..627aa167c7 100644 --- a/Loop/Managers/AnalyticsServicesManager.swift +++ b/Loop/Managers/AnalyticsServicesManager.swift @@ -9,7 +9,6 @@ import Foundation import LoopKit import LoopCore -import LoopAlgorithm final class AnalyticsServicesManager { diff --git a/Loop/Managers/CGMStalenessMonitor.swift b/Loop/Managers/CGMStalenessMonitor.swift index 25c0365e1e..522f8fb5cc 100644 --- a/Loop/Managers/CGMStalenessMonitor.swift +++ b/Loop/Managers/CGMStalenessMonitor.swift @@ -9,7 +9,6 @@ import Foundation import LoopKit import LoopCore -import LoopAlgorithm protocol CGMStalenessMonitorDelegate: AnyObject { func getLatestCGMGlucose(since: Date) async throws -> StoredGlucoseSample? @@ -38,9 +37,9 @@ class CGMStalenessMonitor { let mostRecentGlucose = samples.map { $0.date }.max()! let cgmDataAge = -mostRecentGlucose.timeIntervalSinceNow - if cgmDataAge < LoopAlgorithm.inputDataRecencyInterval { + if cgmDataAge < LoopCoreConstants.inputDataRecencyInterval { self.cgmDataIsStale = false - self.updateCGMStalenessTimer(expiration: mostRecentGlucose.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval)) + self.updateCGMStalenessTimer(expiration: mostRecentGlucose.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval)) } else { self.cgmDataIsStale = true } @@ -60,11 +59,11 @@ class CGMStalenessMonitor { func checkCGMStaleness() async { do { - let sample = try await delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)) + let sample = try await delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopCoreConstants.inputDataRecencyInterval)) self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: sample)) if let sample = sample { self.cgmDataIsStale = false - self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance)) + self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance)) } else { self.cgmDataIsStale = true } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 236c40a22d..a9df54061f 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -13,7 +13,7 @@ import LoopCore import LoopTestingKit import UserNotifications import Combine -import LoopAlgorithm +import LoopKit public enum DemoError: LocalizedError { case CommsError @@ -669,7 +669,7 @@ final class DeviceDataManager { var isGlucoseValueStale: Bool { guard let latestGlucoseDataDate = glucoseStore.latestGlucose?.startDate else { return true } - return Date().timeIntervalSince(latestGlucoseDataDate) > LoopAlgorithm.inputDataRecencyInterval + return Date().timeIntervalSince(latestGlucoseDataDate) > LoopCoreConstants.inputDataRecencyInterval } } diff --git a/Loop/Managers/DoseEnactor.swift b/Loop/Managers/DoseEnactor.swift index f9df3ebdff..f7ff5b89eb 100644 --- a/Loop/Managers/DoseEnactor.swift +++ b/Loop/Managers/DoseEnactor.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm class DoseEnactor { diff --git a/Loop/Managers/ExtensionDataManager.swift b/Loop/Managers/ExtensionDataManager.swift index 32bc0b90c4..915da7554b 100644 --- a/Loop/Managers/ExtensionDataManager.swift +++ b/Loop/Managers/ExtensionDataManager.swift @@ -6,7 +6,6 @@ // Copyright © 2016 Nathan Racklyeft. All rights reserved. // -import LoopAlgorithm import UIKit import LoopKit import LoopCore @@ -149,7 +148,7 @@ final class ExtensionDataManager { let first = predictedGlucose[predictedGlucose.startIndex] let second = predictedGlucose[predictedGlucose.startIndex.advanced(by: 1)] context.predictedGlucose = PredictedGlucoseContext( - values: predictedGlucose.map { $0.quantity.doubleValue(for: glucoseUnit) }, + values: predictedGlucose.map { $0.loopKit.quantity.doubleValue(for: glucoseUnit) }, unit: glucoseUnit, startDate: first.startDate, interval: second.startDate.timeIntervalSince(first.startDate)) diff --git a/Loop/Managers/Live Activity/ChartAxisGenerator.swift b/Loop/Managers/Live Activity/ChartAxisGenerator.swift index 536d10daa7..b2db2973e2 100644 --- a/Loop/Managers/Live Activity/ChartAxisGenerator.swift +++ b/Loop/Managers/Live Activity/ChartAxisGenerator.swift @@ -8,7 +8,7 @@ import Foundation import HealthKit -import LoopAlgorithm +import LoopKit import SwiftCharts import UIKit diff --git a/Loop/Managers/Live Activity/LiveActivityManager.swift b/Loop/Managers/Live Activity/LiveActivityManager.swift index d8653b1c40..3d1073a100 100644 --- a/Loop/Managers/Live Activity/LiveActivityManager.swift +++ b/Loop/Managers/Live Activity/LiveActivityManager.swift @@ -9,7 +9,6 @@ import LoopKitUI import LoopKit import LoopCore -import LoopAlgorithm import Foundation import HealthKit import ActivityKit diff --git a/Loop/Managers/Live Activity/LiveActivityManagerProxy.swift b/Loop/Managers/Live Activity/LiveActivityManagerProxy.swift index ffea337d21..92d0899b8a 100644 --- a/Loop/Managers/Live Activity/LiveActivityManagerProxy.swift +++ b/Loop/Managers/Live Activity/LiveActivityManagerProxy.swift @@ -7,7 +7,6 @@ // import LoopKit -import LoopAlgorithm protocol LiveActivityManagerProxy { /// Update the live activity with current override and glucose target information. diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 57d121a979..10d4fcfc12 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -17,7 +17,6 @@ import MockKit import HealthKit import WidgetKit import LoopCore -import LoopAlgorithm import SwiftUI #if targetEnvironment(simulator) @@ -244,7 +243,7 @@ class LoopAppManager: NSObject { healthStore: healthStore, observeHealthKitSamplesFromOtherApps: FeatureFlags.observeHealthKitCarbSamplesFromOtherApps, // At some point we should let the user decide which apps they would like to import from. type: HealthKitSampleStore.carbType, - observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval) + observationStart: Date().addingTimeInterval(-LoopCoreConstants.maximumCarbAbsorptionTimeInterval) ) temporaryPresetsManager = TemporaryPresetsManager(settingsProvider: settingsManager, alertIssuer: alertManager) @@ -264,7 +263,7 @@ class LoopAppManager: NSObject { healthStore: healthStore, observeHealthKitSamplesFromOtherApps: FeatureFlags.observeHealthKitDoseSamplesFromOtherApps, type: HealthKitSampleStore.insulinQuantityType, - observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval) + observationStart: Date().addingTimeInterval(-LoopCoreConstants.maximumCarbAbsorptionTimeInterval) ) self.doseStore = await DoseStore( diff --git a/Loop/Managers/LoopDataManager+CarbAbsorption.swift b/Loop/Managers/LoopDataManager+CarbAbsorption.swift index cb1e49a977..3aee4d581a 100644 --- a/Loop/Managers/LoopDataManager+CarbAbsorption.swift +++ b/Loop/Managers/LoopDataManager+CarbAbsorption.swift @@ -21,7 +21,7 @@ extension LoopDataManager { func dynamicCarbsOnBoard(from start: Date? = nil, to end: Date? = nil) async -> [CarbValue] { if let effects = displayState.output?.effects { - return effects.carbStatus.dynamicCarbsOnBoard(from: start, to: end, absorptionModel: carbAbsorptionModel.model) + return effects.carbStatus.dynamicCarbsOnBoard(from: start, to: end, absorptionModel: carbAbsorptionModel.model).map(\.loopKit) } else { return [] } @@ -65,10 +65,11 @@ extension LoopDataManager { guard !carbRatio.isEmpty else { throw LoopError.configurationError(.carbRatioSchedule) } - let carbRatioWithOverrides = overrides.applyCarbRatio(over: carbRatio) + let carbRatioWithOverrides = overrides.applyCarbRatio(over: carbRatio).algorithmSchedule() + let algorithmSensitivity = sensitivityWithOverrides.algorithmMgdlSchedule() // Overlay basal history on basal doses, splitting doses to get amount delivered relative to basal - let annotatedDoses = doses.annotated(with: basalWithOverrides) + let annotatedDoses = doses.annotated(with: basalWithOverrides.algorithmSchedule()) // Use the standard mid-absorption ISF model — matching LoopAlgorithm's // prediction path (useMidAbsorptionISF) — so this review's carb @@ -77,7 +78,7 @@ extension LoopDataManager { // disagreed, an ISF-changing override made the carb screen and the // home screen show different COB. let insulinEffects = annotatedDoses.glucoseEffectsMidAbsorptionISF( - insulinSensitivityHistory: sensitivityWithOverrides, + insulinSensitivityHistoryMgdlPerUnit: algorithmSensitivity, from: start.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval).dateFlooredToTimeInterval(GlucoseMath.defaultDelta), to: nil) @@ -88,22 +89,22 @@ extension LoopDataManager { let carbStatus = carbEntries.map( to: insulinCounteractionEffects, carbRatio: carbRatioWithOverrides, - insulinSensitivity: sensitivityWithOverrides + insulinSensitivityMgdlPerUnit: algorithmSensitivity ) let carbEffects = carbStatus.dynamicGlucoseEffects( from: start, to: end.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration), carbRatios: carbRatioWithOverrides, - insulinSensitivities: sensitivityWithOverrides, + insulinSensitivitiesMgdlPerUnit: algorithmSensitivity, absorptionModel: CarbAbsorptionModel.piecewiseLinear.model ) return CarbAbsorptionReview( carbEntries: carbEntries, carbStatuses: carbStatus, - effectsVelocities: insulinCounteractionEffects, - carbEffects: carbEffects + effectsVelocities: insulinCounteractionEffects.map(\.loopKit), + carbEffects: carbEffects.map(\.loopKit) ) } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index da3ff8c76b..115360aa36 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -9,10 +9,10 @@ import Foundation import Combine import LoopKit +import LoopAlgorithm import LoopKitUI import LoopCore import WidgetKit -import LoopAlgorithm struct AlgorithmDisplayState { @@ -99,11 +99,11 @@ final class LoopDataManager: ObservableObject { // Display state convenience accessors var predictedGlucose: [PredictedGlucoseValue]? { - displayState.output?.predictedGlucose + displayState.output?.predictedGlucose.map(\.loopKit) } var tempBasalRecommendation: TempBasalRecommendation? { - displayState.output?.recommendation?.automatic?.basalAdjustment + displayState.output?.recommendation?.automatic?.basalAdjustment.loopKit } var automaticBolusRecommendation: Double? { @@ -111,7 +111,7 @@ final class LoopDataManager: ObservableObject { } var automaticRecommendation: AutomaticDoseRecommendation? { - displayState.output?.recommendation?.automatic + displayState.output?.recommendation?.automatic?.loopKit } @Published private(set) var lastLoopCompleted: Date? @@ -449,7 +449,7 @@ final class LoopDataManager: ObservableObject { start: baseTime, duration: recommendationInsulinModel.effectDuration ) - let neededSensitivityTimeline = LoopAlgorithm.timelineIntervalForSensitivity( + let neededSensitivityTimeline = Algorithm.timelineIntervalForSensitivity( doses: dosesWithModel, glucoseHistoryStart: glucose.first?.startDate ?? baseTime, recommendationEffectInterval: recommendationEffectInterval @@ -550,17 +550,17 @@ final class LoopDataManager: ObservableObject { doses: dosesWithModel, carbEntries: carbEntries, predictionStart: baseTime, - basal: basalWithOverrides, - sensitivity: sensitivityWithOverrides, - carbRatio: carbRatioWithOverrides, - target: target, - suspendThreshold: suspendThreshold, + basal: basalWithOverrides.algorithmSchedule(), + sensitivityMgdlPerUnit: sensitivityWithOverrides.algorithmMgdlSchedule(), + carbRatio: carbRatioWithOverrides.algorithmSchedule(), + targetMgdl: target.algorithmMgdlRanges(), + suspendThresholdMgdl: suspendThreshold.doubleValue(for: .milligramsPerDeciliter), maxBolus: maxBolus, maxBasalRate: maxBasalRate, useIntegralRetrospectiveCorrection: false, includePositiveVelocityAndRC: true, carbAbsorptionModel: carbAbsorptionModel, - recommendationInsulinModel: recommendationInsulinModel, + recommendationInsulinModel: AlgorithmInsulinModel(recommendationInsulinModel), recommendationType: .manualBolus, automaticBolusApplicationFactor: nil, experiments: experiments, @@ -637,7 +637,7 @@ final class LoopDataManager: ObservableObject { } private nonisolated func runAlgorithm(input: StoredDataAlgorithmInput) async -> AlgorithmOutput { - LoopAlgorithm.run(input: input) + Algorithm.run(input: input) } /// Cancel the active temp basal if it was automatically issued @@ -706,19 +706,19 @@ final class LoopDataManager: ObservableObject { throw LoopError.missingDataError(.glucose) } - guard loopBaseTime.timeIntervalSince(latestGlucose.startDate) <= LoopAlgorithm.inputDataRecencyInterval else { + guard loopBaseTime.timeIntervalSince(latestGlucose.startDate) <= LoopCoreConstants.inputDataRecencyInterval else { throw LoopError.glucoseTooOld(date: latestGlucose.startDate) } - guard latestGlucose.startDate.timeIntervalSince(loopBaseTime) <= LoopAlgorithm.inputDataRecencyInterval else { + guard latestGlucose.startDate.timeIntervalSince(loopBaseTime) <= LoopCoreConstants.inputDataRecencyInterval else { throw LoopError.invalidFutureGlucose(date: latestGlucose.startDate) } - guard loopBaseTime.timeIntervalSince(doseStore.lastAddedPumpData) <= LoopAlgorithm.inputDataRecencyInterval else { + guard loopBaseTime.timeIntervalSince(doseStore.lastAddedPumpData) <= LoopCoreConstants.inputDataRecencyInterval else { throw LoopError.pumpDataTooOld(date: doseStore.lastAddedPumpData) } - var output = LoopAlgorithm.run(input: input) + var output = Algorithm.run(input: input) dosingDecision.algorithmExperiments = algorithmExperiments(for: dosingStrategy, guardAndRunway: output.guardAndRunway) switch output.recommendationResult { @@ -742,7 +742,7 @@ final class LoopDataManager: ObservableObject { let scheduledBasalRate = input.basal.closestPrior(to: loopBaseTime)!.value let activeOverride = temporaryPresetsManager.presetHistory.activeOverride(at: loopBaseTime) - let basalAdjustment = basal.adjustForCurrentDelivery( + let basalAdjustment = basal.loopKit.adjustForCurrentDelivery( at: loopBaseTime, neutralBasalRate: scheduledBasalRate, currentTempBasal: deliveryDelegate.basalDeliveryState?.currentTempBasal, @@ -751,7 +751,7 @@ final class LoopDataManager: ObservableObject { ) if let basalAdjustment { - recommendationToEnact.basalAdjustment = basalAdjustment + recommendationToEnact.basalAdjustment = basalAdjustment.algorithm } output.recommendationResult = .success(.init(automatic: recommendationToEnact)) @@ -842,7 +842,7 @@ final class LoopDataManager: ObservableObject { input.recommendationType = .manualBolus try input.validateCarbScheduleCoverage() - let output = LoopAlgorithm.run(input: input) + let output = Algorithm.run(input: input) switch output.recommendationResult { @@ -851,7 +851,7 @@ final class LoopDataManager: ObservableObject { if let roundedAmount = deliveryDelegate?.roundBolusVolume(units: manualBolusRecommendation.amount) { manualBolusRecommendation.amount = roundedAmount } - return manualBolusRecommendation + return manualBolusRecommendation.loopKit case .failure(let error): throw error } @@ -865,7 +865,7 @@ final class LoopDataManager: ObservableObject { let now = data.predictionStart let midnight = Calendar.current.startOfDay(for: now) - let history = InsulinDeliveryHistory(doses: data.doses, scheduledBasal: data.basal, coverageStart: midnight, end: now) + let history = InsulinDeliveryHistory(doses: data.doses, scheduledBasal: data.basal.loopKitSchedule(), coverageStart: midnight, end: now) return InsulinValue( startDate: midnight, @@ -898,7 +898,7 @@ final class LoopDataManager: ObservableObject { } var iobValues: [InsulinValue] { - dosesRelativeToBasal.insulinOnBoardTimeline() + dosesRelativeToBasal.insulinOnBoardTimeline().map(\.loopKit) } var dosesRelativeToBasal: [BasalRelativeDose] { @@ -907,10 +907,10 @@ final class LoopDataManager: ObservableObject { func updateRemoteRecommendation(force: Bool = false) async { if lastManualBolusRecommendation == nil { - lastManualBolusRecommendation = displayState.output?.recommendation?.manual + lastManualBolusRecommendation = displayState.output?.recommendation?.manual?.loopKit } - let recommendationChanged = lastManualBolusRecommendation != displayState.output?.recommendation?.manual + let recommendationChanged = lastManualBolusRecommendation != displayState.output?.recommendation?.manual?.loopKit // DIY: post-dose "updateRemoteRecommendation" decisions are also Nightscout's // Loop pill + forecast source (NightscoutService pairs them with the cached "loop" @@ -920,19 +920,19 @@ final class LoopDataManager: ObservableObject { return } - lastManualBolusRecommendation = displayState.output?.recommendation?.manual + lastManualBolusRecommendation = displayState.output?.recommendation?.manual?.loopKit if let output = displayState.output { var dosingDecision = StoredDosingDecision(date: now, reason: "updateRemoteRecommendation") dosingDecision.algorithmExperiments = algorithmExperiments(for: currentDosingStrategy, guardAndRunway: output.guardAndRunway) - dosingDecision.predictedGlucose = output.predictedGlucose + dosingDecision.predictedGlucose = output.predictedGlucose.map(\.loopKit) dosingDecision.insulinOnBoard = displayState.activeInsulin dosingDecision.carbsOnBoard = displayState.activeCarbs switch output.recommendationResult { case .success(let recommendation): - dosingDecision.automaticDoseRecommendation = recommendation.automatic + dosingDecision.automaticDoseRecommendation = recommendation.automatic?.loopKit if let recommendationDate = displayState.input?.predictionStart, let manualRec = recommendation.manual { - dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: manualRec, date: recommendationDate) + dosingDecision.manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: manualRec.loopKit, date: recommendationDate) } case .failure(let error): if let loopError = error as? LoopError { @@ -989,7 +989,7 @@ final class LoopDataManager: ObservableObject { private var glucoseValueStaleDate: Date? { guard let latestGlucoseDataDate = glucoseStore.latestGlucose?.startDate else { return nil } - return latestGlucoseDataDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval) + return latestGlucoseDataDate.addingTimeInterval(LoopCoreConstants.inputDataRecencyInterval) } } @@ -1183,8 +1183,8 @@ extension LoopDataManager { // Calculate predicted glucose effect of suspending insulin delivery return suspendDoses.glucoseEffects( - insulinSensitivityHistory: sensitivity - ).filterDateRange(startSuspend, endSuspend) + insulinSensitivityHistoryMgdlPerUnit: sensitivity.algorithmMgdlSchedule() + ).filterDateRange(startSuspend, endSuspend).map(\.loopKit) } func computeSimpleBolusRecommendation(at date: Date, mealCarbs: LoopQuantity?, manualGlucose: LoopQuantity?) -> BolusDosingDecision? { @@ -1341,21 +1341,21 @@ extension StoredDataAlgorithmInput { guard carbRatio.closestPrior(to: entry.startDate) != nil else { throw LoopError.configurationError(.carbRatioSchedule) } - guard sensitivity.closestPrior(to: entry.startDate) != nil else { + guard sensitivityMgdlPerUnit.closestPrior(to: entry.startDate) != nil else { throw LoopError.configurationError(.insulinSensitivitySchedule) } } } - func predictGlucose(effectsOptions: AlgorithmEffectsOptions = .all) throws -> [PredictedGlucoseValue] { + func predictGlucose(effectsOptions: AlgorithmEffectsOptions = .all) throws -> [LoopAlgorithm.PredictedGlucoseValue] { try validateCarbScheduleCoverage() - let prediction = LoopAlgorithm.generatePrediction( + let prediction = Algorithm.generatePrediction( start: predictionStart, glucoseHistory: glucoseHistory, doses: doses, carbEntries: carbEntries, basal: basal, - sensitivity: sensitivity, + sensitivityMgdlPerUnit: sensitivityMgdlPerUnit, carbRatio: carbRatio, algorithmEffectsOptions: effectsOptions, useIntegralRetrospectiveCorrection: self.useIntegralRetrospectiveCorrection, @@ -1591,7 +1591,7 @@ extension LoopDataManager: BolusEntryViewModelDelegate { let prediction = try input.predictGlucose() - return (historicGlucose: input.glucoseHistory, predictedGlucose: prediction) + return (historicGlucose: input.glucoseHistory, predictedGlucose: prediction.map(\.loopKit)) } } @@ -1665,14 +1665,14 @@ extension LoopDataManager: FavoriteFoodInsightsViewModelDelegate { // binary-search filterDateRange. let annotatedDoses = doses .map({ $0.simpleDose(with: insulinModel(for: $0.insulinType)) }) - .annotated(with: basalWithOverrides) + .annotated(with: basalWithOverrides.algorithmSchedule()) .sorted { $0.startDate < $1.startDate } // Standard mid-absorption ISF model, matching LoopAlgorithm's prediction // path — keeps this review's carb absorption consistent with the // algorithm (dose-time `glucoseEffects` is legacy backwards-compat). let insulinEffects = annotatedDoses.glucoseEffectsMidAbsorptionISF( - insulinSensitivityHistory: sensitivityWithOverrides, + insulinSensitivityHistoryMgdlPerUnit: sensitivityWithOverrides.algorithmMgdlSchedule(), from: start.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval).dateFlooredToTimeInterval(GlucoseMath.defaultDelta), to: nil) @@ -1682,27 +1682,27 @@ extension LoopDataManager: FavoriteFoodInsightsViewModelDelegate { // Carb Effects let carbStatus = carbEntries.map( to: insulinCounteractionEffects, - carbRatio: carbRatioWithOverrides, - insulinSensitivity: sensitivityWithOverrides + carbRatio: carbRatioWithOverrides.algorithmSchedule(), + insulinSensitivityMgdlPerUnit: sensitivityWithOverrides.algorithmMgdlSchedule() ) let carbEffects = carbStatus.dynamicGlucoseEffects( from: start, to: end.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration), - carbRatios: carbRatioWithOverrides, - insulinSensitivities: sensitivityWithOverrides, + carbRatios: carbRatioWithOverrides.algorithmSchedule(), + insulinSensitivitiesMgdlPerUnit: sensitivityWithOverrides.algorithmMgdlSchedule(), absorptionModel: CarbAbsorptionModel.piecewiseLinear.model ) let carbAbsorptionReview = CarbAbsorptionReview( carbEntries: carbEntries, carbStatuses: carbStatus, - effectsVelocities: insulinCounteractionEffects, - carbEffects: carbEffects + effectsVelocities: insulinCounteractionEffects.map(\.loopKit), + carbEffects: carbEffects.map(\.loopKit) ) let trimmedDoses = annotatedDoses.filterDateRange(start, end) - let trimmedIOBValues = annotatedDoses.insulinOnBoardTimeline().filterDateRange(start, end) + let trimmedIOBValues = annotatedDoses.insulinOnBoardTimeline().filterDateRange(start, end).map(\.loopKit) let historicalChartsData = HistoricalChartsData( glucoseValues: glucose, @@ -1755,7 +1755,7 @@ extension StoredDosingDecision { self.historicalGlucose = input.glucoseHistory.map { HistoricalGlucoseValue(startDate: $0.startDate, quantity: $0.quantity) } switch output.recommendationResult { case .success(let recommendation): - self.automaticDoseRecommendation = recommendation.automatic + self.automaticDoseRecommendation = recommendation.automatic?.loopKit case .failure(let error): self.appendError(error as? LoopError ?? .unknownError(error)) } @@ -1765,7 +1765,7 @@ extension StoredDosingDecision { if let activeCarbs = output.activeCarbs { self.carbsOnBoard = CarbValue(startDate: input.predictionStart, value: activeCarbs) } - self.predictedGlucose = output.predictedGlucose + self.predictedGlucose = output.predictedGlucose.map(\.loopKit) } } @@ -1806,28 +1806,28 @@ extension LoopDataManager: DiagnosticReportGenerator { "insulinCounteractionEffects: [", "* GlucoseEffectVelocity(start, end, mg/dL/min)", (algoOutput?.effects.insulinCounteraction ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.endDate), \(entry.quantity.doubleValue(for: GlucoseEffectVelocity.unit))\n") + entries.append("* \(entry.startDate), \(entry.endDate), \(entry.mgdlPerSecond * 60)\n") }), "]", "insulinEffect: [", "* GlucoseEffect(start, mg/dL)", (algoOutput?.effects.insulin ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + entries.append("* \(entry.startDate), \(entry.mgdl)\n") }), "]", "carbEffect: [", "* GlucoseEffect(start, mg/dL)", (algoOutput?.effects.carbs ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + entries.append("* \(entry.startDate), \(entry.mgdl)\n") }), "]", "predictedGlucose: [", "* PredictedGlucoseValue(start, mg/dL)", (algoOutput?.predictedGlucose ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + entries.append("* \(entry.startDate), \(entry.mgdl)\n") }), "]", @@ -1836,7 +1836,7 @@ extension LoopDataManager: DiagnosticReportGenerator { "retrospectiveCorrection: [", "* GlucoseEffect(start, mg/dL)", (algoOutput?.effects.retrospectiveCorrection ?? []).reduce(into: "", { (entries, entry) in - entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + entries.append("* \(entry.startDate), \(entry.mgdl)\n") }), "]", diff --git a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift index 32b2dd37ce..a33a7d6c44 100644 --- a/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift +++ b/Loop/Managers/Missed Meal Detection/MealDetectionManager.swift @@ -10,8 +10,8 @@ import Foundation import OSLog import LoopCore import LoopKit -import Combine import LoopAlgorithm +import Combine enum MissedMealStatus: Equatable { case hasMissedMeal(startTime: Date, carbAmount: Double) @@ -104,9 +104,9 @@ class MealDetectionManager { // MARK: Meal Detection func hasMissedMeal( at date: Date, - glucoseSamples: [some GlucoseSampleValue], - insulinCounteractionEffects: [GlucoseEffectVelocity], - carbEffects: [GlucoseEffect], + glucoseSamples: [some LoopAlgorithm.GlucoseSampleValue], + insulinCounteractionEffects: [LoopAlgorithm.GlucoseEffectVelocity], + carbEffects: [LoopAlgorithm.GlucoseEffect], sensitivitySchedule: InsulinSensitivitySchedule, carbRatioSchedule: CarbRatioSchedule ) -> MissedMealStatus @@ -132,11 +132,11 @@ class MealDetectionManager { var effectValueCache: [Date: Double] = [:] /// Carb effects are cumulative, so we have to subtract the previous effect value - var previousEffectValue: Double = filteredCarbEffects.first?.quantity.doubleValue(for: unit) ?? 0 + var previousEffectValue: Double = filteredCarbEffects.first?.mgdl ?? 0 /// Counteraction effects only take insulin into account, so we need to account for the carb effects when computing the unexpected deviations for effect in filteredCarbEffects { - let value = effect.quantity.doubleValue(for: unit) + let value = effect.mgdl /// We do `-1 * (value - previousEffectValue)` because this will compute the carb _counteraction_ effect effectValueCache[effect.startDate] = (effectValueCache[effect.startDate] ?? 0) + -1 * (value - previousEffectValue) previousEffectValue = value @@ -150,16 +150,16 @@ class MealDetectionManager { let end = min($0.endDate, now) guard let effect = $0.effect(from: start, to: end) else { - let item: GlucoseEffect? = nil // FIXME: we get a compiler error if we try to return `nil` directly + let item: LoopAlgorithm.GlucoseEffect? = nil // FIXME: we get a compiler error if we try to return `nil` directly return item } - return GlucoseEffect(startDate: effect.endDate.dateCeiledToTimeInterval(delta), - quantity: effect.quantity) + return LoopAlgorithm.GlucoseEffect(startDate: effect.endDate.dateCeiledToTimeInterval(delta), + mgdl: effect.mgdl) } for effect in processedICE { - let value = effect.quantity.doubleValue(for: unit) + let value = effect.mgdl effectValueCache[effect.startDate] = (effectValueCache[effect.startDate] ?? 0) + value } @@ -278,9 +278,9 @@ class MealDetectionManager { /// - carbEffects: the effects of any active carb entries. Must include effects from `currentDate() - MissedMealSettings.maxRecency` until `currentDate()`. func generateMissedMealNotificationIfNeeded( at date: Date, - glucoseSamples: [some GlucoseSampleValue], - insulinCounteractionEffects: [GlucoseEffectVelocity], - carbEffects: [GlucoseEffect], + glucoseSamples: [some LoopAlgorithm.GlucoseSampleValue], + insulinCounteractionEffects: [LoopAlgorithm.GlucoseEffectVelocity], + carbEffects: [LoopAlgorithm.GlucoseEffect], sensitivitySchedule: InsulinSensitivitySchedule, carbRatioSchedule: CarbRatioSchedule, maxBolus: Double @@ -370,7 +370,7 @@ class MealDetectionManager { } } -fileprivate extension BidirectionalCollection where Element: GlucoseSampleValue, Index == Int { +fileprivate extension BidirectionalCollection where Element: LoopAlgorithm.GlucoseSampleValue, Index == Int { /// Returns whether there are any user-entered or calibration points /// Runtime: O(n) func containsUserEntered() -> Bool { @@ -387,9 +387,9 @@ extension BolusStateProvider { } } -extension GlucoseEffectVelocity { +extension LoopAlgorithm.GlucoseEffectVelocity { /// The integration of the velocity span from `start` to `end` - public func effect(from start: Date, to end: Date) -> GlucoseEffect? { + public func effect(from start: Date, to end: Date) -> LoopAlgorithm.GlucoseEffect? { guard start <= end, startDate <= start, @@ -399,15 +399,7 @@ extension GlucoseEffectVelocity { } let duration = end.timeIntervalSince(start) - let velocityPerSecond = quantity.doubleValue(for: GlucoseEffectVelocity.perSecondUnit) - - return GlucoseEffect( - startDate: end, - quantity: LoopQuantity( - unit: .milligramsPerDeciliter, - doubleValue: velocityPerSecond * duration - ) - ) + return LoopAlgorithm.GlucoseEffect(startDate: end, mgdl: mgdlPerSecond * duration) } } diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index b28583fb66..1e3a8c6a4b 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -8,7 +8,6 @@ import os.log import Foundation -import LoopAlgorithm import LoopKit import UIKit diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index 466581b765..66c7aa2c9f 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -14,7 +14,6 @@ import Combine import LoopCore import LoopKitUI import os.log -import LoopAlgorithm @MainActor diff --git a/Loop/Managers/StatusChartsManager.swift b/Loop/Managers/StatusChartsManager.swift index a57b52d970..e7a9df2dfd 100644 --- a/Loop/Managers/StatusChartsManager.swift +++ b/Loop/Managers/StatusChartsManager.swift @@ -10,7 +10,6 @@ import LoopKit import LoopUI import LoopKitUI import SwiftCharts -import LoopAlgorithm class StatusChartsManager: ChartsManager { diff --git a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift index 0d0d11d6a9..6de9aa59b2 100644 --- a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift @@ -8,7 +8,6 @@ import LoopKit import HealthKit -import LoopAlgorithm protocol DoseStoreProtocol: AnyObject { func getNormalizedDoseEntries(start: Date, end: Date?) async throws -> [DoseEntry] diff --git a/Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift b/Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift index 401dfb7e31..7451b237a2 100644 --- a/Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/DosingDecisionStoreProtocol.swift @@ -6,7 +6,6 @@ // Copyright © 2020 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit struct LightDosingDecision: DosingDecision { diff --git a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift b/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift index 915f0016f7..c1600d165b 100644 --- a/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/GlucoseStoreProtocol.swift @@ -7,7 +7,6 @@ // import LoopKit -import LoopAlgorithm protocol GlucoseStoreProtocol: AnyObject { var latestGlucose: GlucoseSampleValue? { get } diff --git a/Loop/Managers/TemporaryPresetsManager.swift b/Loop/Managers/TemporaryPresetsManager.swift index 0b71d95dc3..19729534cc 100644 --- a/Loop/Managers/TemporaryPresetsManager.swift +++ b/Loop/Managers/TemporaryPresetsManager.swift @@ -10,7 +10,6 @@ import Foundation import LoopKit import os.log import LoopCore -import LoopAlgorithm @MainActor protocol PresetActivationObserver: AnyObject { diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 77d42ba3dd..fd9c270140 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -9,7 +9,6 @@ import HealthKit import UIKit import WatchConnectivity -import LoopAlgorithm import LoopKit import LoopCore @@ -386,11 +385,11 @@ final class WatchDataManager: NSObject { // Drop the first element in predictedGlucose because it is the current glucose let filteredPredictedGlucose = predictedGlucose.dropFirst() if filteredPredictedGlucose.count > 0 { - context.predictedGlucose = WatchPredictedGlucose(values: Array(filteredPredictedGlucose)) + context.predictedGlucose = WatchPredictedGlucose(values: filteredPredictedGlucose.map(\.loopKit)) } } - dosingDecision.predictedGlucose = algoOutput?.predictedGlucose + dosingDecision.predictedGlucose = algoOutput?.predictedGlucose.map(\.loopKit) var preMealOverride = self.temporaryPresetsManager.preMealOverride if preMealOverride?.hasFinished() == true { diff --git a/Loop/Models/AlgorithmBridge.swift b/Loop/Models/AlgorithmBridge.swift new file mode 100644 index 0000000000..53ded55f37 --- /dev/null +++ b/Loop/Models/AlgorithmBridge.swift @@ -0,0 +1,184 @@ +// +// AlgorithmBridge.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import LoopAlgorithm +import LoopKitUI + +// LoopKit's stored values, given to LoopAlgorithm in its fixed units. + +extension StoredCarbEntry: @retroactive LoopAlgorithm.CarbEntry { + public var grams: Double { quantity.doubleValue(for: .gram) } + public var endDate: Date { startDate } +} + +extension StoredGlucoseSample: @retroactive LoopAlgorithm.GlucoseSampleValue { + public var mgdl: Double { quantity.doubleValue(for: .milligramsPerDeciliter) } + public var endDate: Date { startDate } +} + +// These types conform to both modules' TimelineValue, so the protocols' shared helpers are +// ambiguous on them; these concrete versions take precedence. +extension Array where Element == StoredGlucoseSample { + func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [StoredGlucoseSample] { + filter { (startDate == nil || $0.endDate >= startDate!) && (endDate == nil || $0.startDate <= endDate!) } + } +} + +extension Array where Element == StoredCarbEntry { + func filterDateRange(_ startDate: Date?, _ endDate: Date?) -> [StoredCarbEntry] { + filter { (startDate == nil || $0.endDate >= startDate!) && (endDate == nil || $0.startDate <= endDate!) } + } +} + +struct AlgorithmInsulinModel: LoopAlgorithm.InsulinModel { + let model: LoopKit.InsulinModel + + init(_ model: LoopKit.InsulinModel) { + self.model = model + } + + func percentEffectRemaining(at time: TimeInterval) -> Double { + model.percentEffectRemaining(at: time) + } + + var effectDuration: TimeInterval { model.effectDuration } + + var delay: TimeInterval { model.delay } +} + +extension Array { + func algorithmSchedule() -> [LoopAlgorithm.AbsoluteScheduleValue] where Element == LoopKit.AbsoluteScheduleValue { + map { LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value) } + } + + /// Glucose quantities (or sensitivities) in mg/dL + func algorithmMgdlSchedule() -> [LoopAlgorithm.AbsoluteScheduleValue] where Element == LoopKit.AbsoluteScheduleValue { + map { LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value.doubleValue(for: .milligramsPerDeciliter)) } + } + + func algorithmMgdlRanges() -> LoopAlgorithm.GlucoseRangeTimeline where Element == LoopKit.AbsoluteScheduleValue> { + map { + LoopAlgorithm.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, + value: $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter)...$0.value.upperBound.doubleValue(for: .milligramsPerDeciliter)) + } + } +} + +// LoopAlgorithm's results, as LoopKit values for storage and charts. + +extension LoopAlgorithm.GlucoseEffect { + var loopKit: LoopKit.GlucoseEffect { + LoopKit.GlucoseEffect(startDate: startDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) + } +} + +extension LoopAlgorithm.PredictedGlucoseValue { + var loopKit: LoopKit.PredictedGlucoseValue { + LoopKit.PredictedGlucoseValue(startDate: startDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) + } +} + +extension LoopAlgorithm.GlucoseEffectVelocity { + var loopKit: LoopKit.GlucoseEffectVelocity { + LoopKit.GlucoseEffectVelocity(startDate: startDate, endDate: endDate, + quantity: LoopQuantity(unit: .milligramsPerDeciliterPerSecond, doubleValue: mgdlPerSecond)) + } +} + +extension LoopAlgorithm.CarbValue { + var loopKit: LoopKit.CarbValue { + LoopKit.CarbValue(startDate: startDate, endDate: endDate, value: value) + } +} + +extension LoopAlgorithm.InsulinValue { + var loopKit: LoopKit.InsulinValue { + LoopKit.InsulinValue(startDate: startDate, value: value) + } +} + +extension LoopAlgorithm.BasalRelativeDose { + var chartedDose: ChartedDose { + let type: ChartedDose.DoseType + switch self.type { + case .bolus: type = .bolus + case .basal: type = .basal + } + return ChartedDose(type: type, startDate: startDate, endDate: endDate, volume: volume, netBasalUnits: netBasalUnits) + } +} + +extension LoopAlgorithm.GlucoseChange { + var loopKit: LoopKit.GlucoseChange { + LoopKit.GlucoseChange(startDate: startDate, endDate: endDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) + } +} + +extension LoopAlgorithm.SimpleGlucoseValue { + var loopKit: LoopKit.SimpleGlucoseValue { + LoopKit.SimpleGlucoseValue(startDate: startDate, endDate: endDate, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl)) + } +} + +extension LoopAlgorithm.TempBasalRecommendation { + var loopKit: LoopKit.TempBasalRecommendation { + LoopKit.TempBasalRecommendation(unitsPerHour: unitsPerHour, duration: duration) + } +} + +extension LoopAlgorithm.AutomaticDoseRecommendation.Direction { + var loopKit: LoopKit.AutomaticDoseRecommendation.Direction { + switch self { + case .decrease: .decrease + case .neutral: .neutral + case .increase: .increase + } + } +} + +extension LoopAlgorithm.AutomaticDoseRecommendation { + var loopKit: LoopKit.AutomaticDoseRecommendation { + LoopKit.AutomaticDoseRecommendation(basalAdjustment: basalAdjustment.loopKit, direction: direction.loopKit, bolusUnits: bolusUnits) + } +} + +extension LoopAlgorithm.BolusRecommendationNotice { + var loopKit: LoopKit.BolusRecommendationNotice { + switch self { + case .glucoseBelowSuspendThreshold(let minGlucose): + return .glucoseBelowSuspendThreshold(minGlucose: minGlucose.loopKit) + case .currentGlucoseBelowTarget(let glucose): + return .currentGlucoseBelowTarget(glucose: glucose.loopKit) + case .predictedGlucoseBelowTarget(let minGlucose): + return .predictedGlucoseBelowTarget(minGlucose: minGlucose.loopKit) + case .predictedGlucoseInRange: + return .predictedGlucoseInRange + case .allGlucoseBelowTarget(let minGlucose): + return .allGlucoseBelowTarget(minGlucose: minGlucose.loopKit) + } + } +} + +extension LoopAlgorithm.ManualBolusRecommendation { + var loopKit: LoopKit.ManualBolusRecommendation { + LoopKit.ManualBolusRecommendation(amount: amount, notice: notice?.loopKit) + } +} + +extension Array { + func loopKitSchedule() -> [LoopKit.AbsoluteScheduleValue] where Element == LoopAlgorithm.AbsoluteScheduleValue { + map { LoopKit.AbsoluteScheduleValue(startDate: $0.startDate, endDate: $0.endDate, value: $0.value) } + } +} + +extension LoopKit.TempBasalRecommendation { + var algorithm: LoopAlgorithm.TempBasalRecommendation { + LoopAlgorithm.TempBasalRecommendation(unitsPerHour: unitsPerHour, duration: duration) + } +} diff --git a/Loop/Models/AutomationHistoryEntry.swift b/Loop/Models/AutomationHistoryEntry.swift index 4a989a67e8..55ef3cedcf 100644 --- a/Loop/Models/AutomationHistoryEntry.swift +++ b/Loop/Models/AutomationHistoryEntry.swift @@ -7,7 +7,7 @@ // import Foundation -import LoopAlgorithm +import LoopKit public struct AutomationHistoryEntry: Codable, Hashable { var startDate: Date diff --git a/Loop/Models/BolusDosingDecision.swift b/Loop/Models/BolusDosingDecision.swift index d9b8a47073..c45317720b 100644 --- a/Loop/Models/BolusDosingDecision.swift +++ b/Loop/Models/BolusDosingDecision.swift @@ -7,7 +7,6 @@ // import LoopKit -import LoopAlgorithm struct BolusDosingDecision { enum Reason: String { diff --git a/Loop/Models/CarbEntryChange.swift b/Loop/Models/CarbEntryChange.swift index a858fa4a5b..155ad34b53 100644 --- a/Loop/Models/CarbEntryChange.swift +++ b/Loop/Models/CarbEntryChange.swift @@ -6,7 +6,6 @@ // import Foundation -import LoopAlgorithm import LoopKit /// A carb entry about to be saved: a new entry, or new content for a stored one. diff --git a/Loop/Models/CrashRecoveryManager.swift b/Loop/Models/CrashRecoveryManager.swift index 5df015dac9..94d984d863 100644 --- a/Loop/Models/CrashRecoveryManager.swift +++ b/Loop/Models/CrashRecoveryManager.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm class CrashRecoveryManager { diff --git a/Loop/Models/Deeplink.swift b/Loop/Models/Deeplink.swift index aaede07661..c19023f04c 100644 --- a/Loop/Models/Deeplink.swift +++ b/Loop/Models/Deeplink.swift @@ -7,7 +7,7 @@ // import Foundation -import LoopAlgorithm +import LoopKit enum Deeplink: Hashable { enum Host: String, CaseIterable { diff --git a/Loop/Models/GlucoseDisplay.swift b/Loop/Models/GlucoseDisplay.swift index fe81a05b34..79720e9f1a 100644 --- a/Loop/Models/GlucoseDisplay.swift +++ b/Loop/Models/GlucoseDisplay.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit struct GlucoseDisplay: GlucoseDisplayable { diff --git a/Loop/Models/GlucoseEffectVelocity.swift b/Loop/Models/GlucoseEffectVelocity.swift index 6a731611bf..a93f3335fa 100644 --- a/Loop/Models/GlucoseEffectVelocity.swift +++ b/Loop/Models/GlucoseEffectVelocity.swift @@ -6,7 +6,6 @@ // import LoopKit -import LoopAlgorithm extension GlucoseEffectVelocity: @retroactive RawRepresentable { diff --git a/Loop/Models/GlucoseStatistics.swift b/Loop/Models/GlucoseStatistics.swift index 7e832c3723..c321b6d854 100644 --- a/Loop/Models/GlucoseStatistics.swift +++ b/Loop/Models/GlucoseStatistics.swift @@ -7,7 +7,6 @@ import Foundation import LoopKit -import LoopAlgorithm /// Glucose ranges for the time-in-range breakdown, using the international /// consensus thresholds (mg/dL) shared by the AGP report: diff --git a/Loop/Models/InsulinDeliveryHistory.swift b/Loop/Models/InsulinDeliveryHistory.swift index 72603ebd81..55febcdbf1 100644 --- a/Loop/Models/InsulinDeliveryHistory.swift +++ b/Loop/Models/InsulinDeliveryHistory.swift @@ -7,7 +7,6 @@ import Foundation import LoopKit -import LoopAlgorithm struct InsulinDeliveryTotals: Equatable { var basal: Double = 0 diff --git a/Loop/Models/InsulinDeliveryLogEvent.swift b/Loop/Models/InsulinDeliveryLogEvent.swift index 9305601a9a..b7855b4d18 100644 --- a/Loop/Models/InsulinDeliveryLogEvent.swift +++ b/Loop/Models/InsulinDeliveryLogEvent.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm struct InsulinDeliveryLogEvent: Hashable, Identifiable { enum EventType: Hashable { diff --git a/Loop/Models/LoopConstants.swift b/Loop/Models/LoopConstants.swift index 42b38b273b..a7b484475f 100644 --- a/Loop/Models/LoopConstants.swift +++ b/Loop/Models/LoopConstants.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit import UIKit diff --git a/Loop/Models/ManualBolusRecommendation.swift b/Loop/Models/ManualBolusRecommendation.swift index 4594bae7f8..879c59c53d 100644 --- a/Loop/Models/ManualBolusRecommendation.swift +++ b/Loop/Models/ManualBolusRecommendation.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm extension BolusRecommendationNotice { diff --git a/Loop/Models/NetBasal.swift b/Loop/Models/NetBasal.swift index 02a349a602..ff11e9e064 100644 --- a/Loop/Models/NetBasal.swift +++ b/Loop/Models/NetBasal.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm /// Max basal should generally be set, but in those cases where it isn't just use 3.0U/hr as a default top of scale, so we can show *something*. fileprivate let defaultMaxBasalForScale = 3.0 diff --git a/Loop/Models/SimpleBolusCalculator.swift b/Loop/Models/SimpleBolusCalculator.swift index 515aef3f5f..13562211d5 100644 --- a/Loop/Models/SimpleBolusCalculator.swift +++ b/Loop/Models/SimpleBolusCalculator.swift @@ -8,7 +8,6 @@ import Foundation import LoopCore -import LoopAlgorithm import LoopKit struct SimpleBolusCalculator { @@ -25,7 +24,7 @@ struct SimpleBolusCalculator { let sensitivity = sensitivitySchedule.quantity(at: date).doubleValue(for: .milligramsPerDeciliter) let correctionRange = correctionRangeSchedule.quantityRange(at: date) if (!correctionRange.contains(manualGlucose)) { - let correctionTarget = correctionRange.averageValue(for: .milligramsPerDeciliter) + let correctionTarget = (correctionRange.lowerBound.doubleValue(for: .milligramsPerDeciliter) + correctionRange.upperBound.doubleValue(for: .milligramsPerDeciliter)) / 2 let correctionBolus = (manualGlucose.doubleValue(for: .milligramsPerDeciliter) - correctionTarget) / sensitivity if correctionBolus >= 0 { let activeInsulin = max(0, activeInsulin.doubleValue(for: .internationalUnit)) diff --git a/Loop/Models/SimpleInsulinDose.swift b/Loop/Models/SimpleInsulinDose.swift index f0cd1048dc..c3963b47ce 100644 --- a/Loop/Models/SimpleInsulinDose.swift +++ b/Loop/Models/SimpleInsulinDose.swift @@ -19,7 +19,7 @@ struct SimpleInsulinDose: InsulinDose { var startDate: Date var endDate: Date var volume: Double - var insulinModel: InsulinModel + var insulinModel: LoopAlgorithm.InsulinModel } extension DoseEntry { @@ -36,14 +36,14 @@ extension DoseEntry { return deliveredUnits ?? programmedUnits } - func simpleDose(with model: InsulinModel) -> SimpleInsulinDose { + func simpleDose(with model: LoopKit.InsulinModel) -> SimpleInsulinDose { SimpleInsulinDose( deliveryType: deliveryType, automatic: automatic ?? false, startDate: startDate, endDate: endDate, volume: volume, - insulinModel: model + insulinModel: AlgorithmInsulinModel(model) ) } } diff --git a/Loop/Models/StoredDataAlgorithmInput.swift b/Loop/Models/StoredDataAlgorithmInput.swift index 8d8441c45a..1708a98a55 100644 --- a/Loop/Models/StoredDataAlgorithmInput.swift +++ b/Loop/Models/StoredDataAlgorithmInput.swift @@ -25,15 +25,15 @@ struct StoredDataAlgorithmInput: AlgorithmInput { var predictionStart: Date - var basal: [AbsoluteScheduleValue] + var basal: [LoopAlgorithm.AbsoluteScheduleValue] - var sensitivity: [AbsoluteScheduleValue] + var sensitivityMgdlPerUnit: [LoopAlgorithm.AbsoluteScheduleValue] - var carbRatio: [AbsoluteScheduleValue] + var carbRatio: [LoopAlgorithm.AbsoluteScheduleValue] - var target: GlucoseRangeTimeline + var targetMgdl: LoopAlgorithm.GlucoseRangeTimeline - var suspendThreshold: LoopQuantity? + var suspendThresholdMgdl: Double? var maxBolus: Double @@ -45,7 +45,7 @@ struct StoredDataAlgorithmInput: AlgorithmInput { var carbAbsorptionModel: CarbAbsorptionModel - var recommendationInsulinModel: InsulinModel + var recommendationInsulinModel: LoopAlgorithm.InsulinModel var recommendationType: DoseRecommendationType diff --git a/Loop/Models/WatchContext+LoopKit.swift b/Loop/Models/WatchContext+LoopKit.swift index 997124203d..b03f2b375d 100644 --- a/Loop/Models/WatchContext+LoopKit.swift +++ b/Loop/Models/WatchContext+LoopKit.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm import LoopCore extension WatchContext { diff --git a/Loop/View Controllers/CarbAbsorptionViewController.swift b/Loop/View Controllers/CarbAbsorptionViewController.swift index 3effa663a9..bcdbad11ef 100644 --- a/Loop/View Controllers/CarbAbsorptionViewController.swift +++ b/Loop/View Controllers/CarbAbsorptionViewController.swift @@ -9,10 +9,10 @@ import SwiftUI import Intents import LoopCore import LoopKit +import LoopAlgorithm import LoopKitUI import LoopUI import os.log -import LoopAlgorithm private extension RefreshContext { @@ -147,7 +147,7 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif charts.updateEndDate(chartStartDate.addingTimeInterval(.hours(totalHours+1))) // When there is no data, this allows presenting current hour + 1 let midnight = Calendar.current.startOfDay(for: Date()) - let listStart = min(midnight, chartStartDate, Date(timeIntervalSinceNow: -CarbMath.maximumAbsorptionTimeInterval)) + let listStart = min(midnight, chartStartDate, Date(timeIntervalSinceNow: -LoopCoreConstants.maximumCarbAbsorptionTimeInterval)) let listEnd = Date().addingTimeInterval(LoopConstants.maxCarbEntryFutureTime) let shouldUpdateGlucose = currentContext.contains(.glucose) @@ -322,8 +322,8 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif if let absorption = status.absorption { // Absorbed value - let observedProgress = Float(absorption.observedProgress.doubleValue(for: .percent)) - let observedCarbs = absorption.observed + let observedProgress = Float(absorption.observedFraction) + let observedCarbs = LoopQuantity(unit: .gram, doubleValue: absorption.observedGrams) if let observedCarbsText = carbFormatter.string(from: observedCarbs) { cell.observedValueText = String( @@ -341,7 +341,7 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif } cell.observedProgress = observedProgress - cell.clampedProgress = Float(absorption.observedProgress.doubleValue(for: .percent)) + cell.clampedProgress = Float(absorption.observedFraction) cell.observedDateText = absorptionFormatter.string(from: absorption.estimatedDate.duration) // Absorbed time diff --git a/Loop/View Controllers/GlucoseThresholdTableViewController.swift b/Loop/View Controllers/GlucoseThresholdTableViewController.swift index cb6bbb7011..b26b4455d2 100644 --- a/Loop/View Controllers/GlucoseThresholdTableViewController.swift +++ b/Loop/View Controllers/GlucoseThresholdTableViewController.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit import LoopKitUI import UIKit diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index c3ddaa7a78..134a33c863 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -8,11 +8,11 @@ import LoopCore import LoopKit +import LoopAlgorithm import LoopKitUI import LoopUI import UIKit import os.log -import LoopAlgorithm private extension RefreshContext { @@ -126,18 +126,18 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable glucoseSamples = algoInput.glucoseHistory.filterDateRange(self.chartStartDate, nil) } - self.retrospectiveGlucoseDiscrepancies = algoOutput?.effects.retrospectiveGlucoseDiscrepancies - totalRetrospectiveCorrection = algoOutput?.effects.totalRetrospectiveCorrectionEffect + self.retrospectiveGlucoseDiscrepancies = algoOutput?.effects.retrospectiveGlucoseDiscrepancies.map(\.loopKit) + totalRetrospectiveCorrection = algoOutput?.effects.totalRetrospectiveCorrectionEffect.map { LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0) } self.guardAndRunway = algoOutput?.guardAndRunway - self.glucoseChart.setPredictedGlucoseValues(algoOutput?.predictedGlucose ?? []) + self.glucoseChart.setPredictedGlucoseValues((algoOutput?.predictedGlucose ?? []).map(\.loopKit)) do { var glucose = try algoInput?.predictGlucose(effectsOptions: self.selectedInputs.algorithmEffectOptions) ?? [] if self.selectedInputs.contains(.guardAndRunway), let guardAndRunway = self.guardAndRunway { glucose = guardAndRunway.guarded(glucose) } - self.glucoseChart.setAlternatePredictedGlucoseValues(glucose) + self.glucoseChart.setAlternatePredictedGlucoseValues(glucose.map(\.loopKit)) } catch { self.refreshContext.update(with: .status) self.glucoseChart.setAlternatePredictedGlucoseValues([]) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 70d95eebd3..ece45a5df3 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -18,7 +18,6 @@ import SwiftCharts import os.log import Combine import WidgetKit -import LoopAlgorithm private extension RefreshContext { static let all: Set = [.status, .glucose, .insulin, .carbs, .targets] @@ -546,7 +545,7 @@ final class StatusTableViewController: LoopChartsTableViewController { let automaticDosingEnabled = settingsManager.dosingEnabled let state = await loopManager.algorithmDisplayState - predictedGlucoseValues = state.output?.predictedGlucose ?? [] + predictedGlucoseValues = state.output?.predictedGlucose.map(\.loopKit) ?? [] /// Update the status HUDs immediately let lastLoopError: Error? diff --git a/Loop/View Models/BolusEntryViewModel.swift b/Loop/View Models/BolusEntryViewModel.swift index 3b0408a901..aa59941c5d 100644 --- a/Loop/View Models/BolusEntryViewModel.swift +++ b/Loop/View Models/BolusEntryViewModel.swift @@ -12,11 +12,11 @@ import Intents import os.log import LoopCore import LoopKit +import LoopAlgorithm import LoopKitUI import LoopUI import SwiftUI import SwiftCharts -import LoopAlgorithm @MainActor protocol BolusEntryViewModelDelegate: AnyObject { @@ -530,7 +530,7 @@ final class BolusEntryViewModel: ObservableObject { startDate: startDate, endDate: startDate, volume: enteredBolus.doubleValue(for: .internationalUnit), - insulinModel: insulinModel + insulinModel: AlgorithmInsulinModel(insulinModel) ) let (glucoseHistory, prediction) = try await delegate.generatePrediction( @@ -737,12 +737,12 @@ extension BolusEntryViewModel { var isGlucoseDataStale: Bool { guard let latestGlucoseDataDate = delegate?.mostRecentGlucoseDataDate else { return true } - return now().timeIntervalSince(latestGlucoseDataDate) > LoopAlgorithm.inputDataRecencyInterval + return now().timeIntervalSince(latestGlucoseDataDate) > LoopCoreConstants.inputDataRecencyInterval } var isPumpDataStale: Bool { guard let latestPumpDataDate = delegate?.mostRecentPumpDataDate else { return true } - return now().timeIntervalSince(latestPumpDataDate) > LoopAlgorithm.inputDataRecencyInterval + return now().timeIntervalSince(latestPumpDataDate) > LoopCoreConstants.inputDataRecencyInterval } var isManualGlucosePromptVisible: Bool { diff --git a/Loop/View Models/CarbEntryViewModel.swift b/Loop/View Models/CarbEntryViewModel.swift index 719a2daa22..7b84705527 100644 --- a/Loop/View Models/CarbEntryViewModel.swift +++ b/Loop/View Models/CarbEntryViewModel.swift @@ -10,7 +10,6 @@ import SwiftUI import LoopKit import Combine import LoopCore -import LoopAlgorithm import os.log @MainActor diff --git a/Loop/View Models/FavoriteFoodAddEditViewModel.swift b/Loop/View Models/FavoriteFoodAddEditViewModel.swift index b3f70fe3cf..1140db0df0 100644 --- a/Loop/View Models/FavoriteFoodAddEditViewModel.swift +++ b/Loop/View Models/FavoriteFoodAddEditViewModel.swift @@ -8,7 +8,6 @@ import SwiftUI import LoopKit -import LoopAlgorithm final class FavoriteFoodAddEditViewModel: ObservableObject { enum Alert: Identifiable { diff --git a/Loop/View Models/FavoriteFoodInsightsViewModel.swift b/Loop/View Models/FavoriteFoodInsightsViewModel.swift index 79337f5acf..04e87d13be 100644 --- a/Loop/View Models/FavoriteFoodInsightsViewModel.swift +++ b/Loop/View Models/FavoriteFoodInsightsViewModel.swift @@ -7,8 +7,8 @@ // import LoopKit -import LoopKitUI import LoopAlgorithm +import LoopKitUI import os.log import Combine diff --git a/Loop/View Models/FavoriteFoodsViewModel.swift b/Loop/View Models/FavoriteFoodsViewModel.swift index 4b425ccee7..6dfebb3fc9 100644 --- a/Loop/View Models/FavoriteFoodsViewModel.swift +++ b/Loop/View Models/FavoriteFoodsViewModel.swift @@ -7,7 +7,6 @@ // import SwiftUI -import LoopAlgorithm import LoopKit import Combine import os.log diff --git a/Loop/View Models/ManualEntryDoseViewModel.swift b/Loop/View Models/ManualEntryDoseViewModel.swift index c721e05814..d66b40b5bd 100644 --- a/Loop/View Models/ManualEntryDoseViewModel.swift +++ b/Loop/View Models/ManualEntryDoseViewModel.swift @@ -15,7 +15,6 @@ import LoopKit import LoopKitUI import LoopUI import SwiftUI -import LoopAlgorithm enum ManualEntryDoseViewModelError: Error { case notAuthenticated @@ -237,7 +236,7 @@ final class ManualEntryDoseViewModel: ObservableObject { startDate: selectedDoseDate, endDate: selectedDoseDate, volume: enteredBolus.doubleValue(for: .internationalUnit), - insulinModel: insulinModel + insulinModel: AlgorithmInsulinModel(insulinModel) ) self.activeInsulin = state.activeInsulin?.quantity @@ -251,6 +250,7 @@ final class ManualEntryDoseViewModel: ObservableObject { predictedGlucoseValues = try input .addingDose(dose: enteredBolusDose) .predictGlucose() + .map(\.loopKit) } catch { predictedGlucoseValues = [] } diff --git a/Loop/View Models/SettingsViewModel.swift b/Loop/View Models/SettingsViewModel.swift index 9c78a8c46c..626ae21e28 100644 --- a/Loop/View Models/SettingsViewModel.swift +++ b/Loop/View Models/SettingsViewModel.swift @@ -7,7 +7,6 @@ // import Combine -import LoopAlgorithm import LoopCore import LoopKit import LoopKitUI diff --git a/Loop/View Models/SimpleBolusViewModel.swift b/Loop/View Models/SimpleBolusViewModel.swift index 4053621700..2555bd5416 100644 --- a/Loop/View Models/SimpleBolusViewModel.swift +++ b/Loop/View Models/SimpleBolusViewModel.swift @@ -14,7 +14,6 @@ import SwiftUI import LoopCore import Intents import LocalAuthentication -import LoopAlgorithm @MainActor protocol SimpleBolusViewModelDelegate: AnyObject { diff --git a/Loop/View Models/StatisticsViewModel.swift b/Loop/View Models/StatisticsViewModel.swift index 575ffb49b4..5b386adf6c 100644 --- a/Loop/View Models/StatisticsViewModel.swift +++ b/Loop/View Models/StatisticsViewModel.swift @@ -7,7 +7,6 @@ import Foundation import LoopKit -import LoopAlgorithm @MainActor final class StatisticsViewModel: ObservableObject { diff --git a/Loop/Views/BolusEntryView.swift b/Loop/Views/BolusEntryView.swift index ef658d724b..6023a11e1c 100644 --- a/Loop/Views/BolusEntryView.swift +++ b/Loop/Views/BolusEntryView.swift @@ -7,7 +7,6 @@ // import Combine -import LoopAlgorithm import SwiftUI import LoopKit import LoopKitUI diff --git a/Loop/Views/BolusProgressTableViewCell.swift b/Loop/Views/BolusProgressTableViewCell.swift index f8d801da55..9d1d6f1071 100644 --- a/Loop/Views/BolusProgressTableViewCell.swift +++ b/Loop/Views/BolusProgressTableViewCell.swift @@ -9,7 +9,6 @@ import Foundation import LoopKit import LoopUI -import LoopAlgorithm import MKRingProgressView diff --git a/Loop/Views/Charts/CarbEffectChartView.swift b/Loop/Views/Charts/CarbEffectChartView.swift index 9a98113535..762253c6bb 100644 --- a/Loop/Views/Charts/CarbEffectChartView.swift +++ b/Loop/Views/Charts/CarbEffectChartView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm struct CarbEffectChartView: View { let chartManager: ChartsManager diff --git a/Loop/Views/Charts/DoseChartView.swift b/Loop/Views/Charts/DoseChartView.swift index 9161436813..a76db12785 100644 --- a/Loop/Views/Charts/DoseChartView.swift +++ b/Loop/Views/Charts/DoseChartView.swift @@ -8,8 +8,8 @@ import SwiftUI import LoopKit -import LoopKitUI import LoopAlgorithm +import LoopKitUI struct DoseChartView: View { let chartManager: ChartsManager @@ -20,7 +20,7 @@ struct DoseChartView: View { var body: some View { LoopChartView(chartManager: chartManager, dateInterval: dateInterval, isInteractingWithChart: $isInteractingWithChart) { doseChart in - doseChart.doseEntries = doses + doseChart.doseEntries = doses.map(\.chartedDose) } } } diff --git a/Loop/Views/Charts/GlucoseCarbChartView.swift b/Loop/Views/Charts/GlucoseCarbChartView.swift index 70f1f5c4ad..6be5364304 100644 --- a/Loop/Views/Charts/GlucoseCarbChartView.swift +++ b/Loop/Views/Charts/GlucoseCarbChartView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm struct GlucoseCarbChartView: View { let chartManager: ChartsManager diff --git a/Loop/Views/Charts/IOBChartView.swift b/Loop/Views/Charts/IOBChartView.swift index e164a42045..1909e4465a 100644 --- a/Loop/Views/Charts/IOBChartView.swift +++ b/Loop/Views/Charts/IOBChartView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm struct IOBChartView: View { let chartManager: ChartsManager diff --git a/Loop/Views/Charts/PredictedGlucoseChartView.swift b/Loop/Views/Charts/PredictedGlucoseChartView.swift index e223a2c3cc..992870ece0 100644 --- a/Loop/Views/Charts/PredictedGlucoseChartView.swift +++ b/Loop/Views/Charts/PredictedGlucoseChartView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm struct PredictedGlucoseChartView: View { let chartManager: ChartsManager diff --git a/Loop/Views/CurrentSetupView.swift b/Loop/Views/CurrentSetupView.swift index 6e1ead3add..18b737c22f 100644 --- a/Loop/Views/CurrentSetupView.swift +++ b/Loop/Views/CurrentSetupView.swift @@ -7,8 +7,8 @@ import SwiftUI import LoopKit -import LoopKitUI import LoopAlgorithm +import LoopKitUI struct CurrentSetupView: View { @StateObject private var viewModel: CurrentSetupViewModel @@ -269,6 +269,8 @@ extension ExperimentOption { return NSLocalizedString("Glucose Based Partial Application", comment: "Title of glucose based partial application experiment") case .integralRetrospectiveCorrection: return NSLocalizedString("Integral Retrospective Correction", comment: "Title of integral retrospective correction experiment") + case .slowRetrospectiveCorrection: + return NSLocalizedString("Slow Retrospective Correction", comment: "Title of slow retrospective correction experiment") } } } diff --git a/Loop/Views/Favorite Foods/FavoriteFoodInsightsChartsView.swift b/Loop/Views/Favorite Foods/FavoriteFoodInsightsChartsView.swift index 8a67b7c519..be6c9656b9 100644 --- a/Loop/Views/Favorite Foods/FavoriteFoodInsightsChartsView.swift +++ b/Loop/Views/Favorite Foods/FavoriteFoodInsightsChartsView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm import Combine struct FavoriteFoodsInsightsChartsView: View { diff --git a/Loop/Views/Favorite Foods/FavoriteFoodInsightsView.swift b/Loop/Views/Favorite Foods/FavoriteFoodInsightsView.swift index 582a661f3a..f2403563f1 100644 --- a/Loop/Views/Favorite Foods/FavoriteFoodInsightsView.swift +++ b/Loop/Views/Favorite Foods/FavoriteFoodInsightsView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm struct FavoriteFoodInsightsView: View { @StateObject private var viewModel: FavoriteFoodInsightsViewModel diff --git a/Loop/Views/GlucoseAlertSettingsView.swift b/Loop/Views/GlucoseAlertSettingsView.swift index 27f800355b..4a2913d7f2 100644 --- a/Loop/Views/GlucoseAlertSettingsView.swift +++ b/Loop/Views/GlucoseAlertSettingsView.swift @@ -9,7 +9,6 @@ import SwiftUI import AVFoundation -import LoopAlgorithm import LoopKit import LoopKitUI diff --git a/Loop/Views/Insulin Delivery Log/InsulinDeliveryEventDetailsView.swift b/Loop/Views/Insulin Delivery Log/InsulinDeliveryEventDetailsView.swift index 841e0f55b7..6b78459187 100644 --- a/Loop/Views/Insulin Delivery Log/InsulinDeliveryEventDetailsView.swift +++ b/Loop/Views/Insulin Delivery Log/InsulinDeliveryEventDetailsView.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import SwiftUI diff --git a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLog.swift b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLog.swift index eeac82bee4..3f78360175 100644 --- a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLog.swift +++ b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLog.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogEventRow.swift b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogEventRow.swift index 17d02c99ed..4a8eef476b 100644 --- a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogEventRow.swift +++ b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogEventRow.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift index 2a225f8de8..ca089e7b95 100644 --- a/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift +++ b/Loop/Views/Insulin Delivery Log/InsulinDeliveryLogViewModel.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import os.log diff --git a/Loop/Views/Insulin Delivery Log/InsulinDeliveryOverview.swift b/Loop/Views/Insulin Delivery Log/InsulinDeliveryOverview.swift index 0d9a78cc77..61dd13855f 100644 --- a/Loop/Views/Insulin Delivery Log/InsulinDeliveryOverview.swift +++ b/Loop/Views/Insulin Delivery Log/InsulinDeliveryOverview.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import SwiftUI import LoopCore diff --git a/Loop/Views/ManualEntryDoseView.swift b/Loop/Views/ManualEntryDoseView.swift index dabb704a04..443ae29eda 100644 --- a/Loop/Views/ManualEntryDoseView.swift +++ b/Loop/Views/ManualEntryDoseView.swift @@ -7,7 +7,6 @@ // import Combine -import LoopAlgorithm import SwiftUI import LoopKit import LoopKitUI diff --git a/Loop/Views/ManualGlucoseEntryRow.swift b/Loop/Views/ManualGlucoseEntryRow.swift index c3cda677fa..dd59bd09a9 100644 --- a/Loop/Views/ManualGlucoseEntryRow.swift +++ b/Loop/Views/ManualGlucoseEntryRow.swift @@ -8,7 +8,6 @@ import Foundation import SwiftUI -import LoopAlgorithm import LoopKit import LoopKitUI import Combine diff --git a/Loop/Views/Presets/Components/PresetDetentView.swift b/Loop/Views/Presets/Components/PresetDetentView.swift index 0c74ad9f03..96a8e384f2 100644 --- a/Loop/Views/Presets/Components/PresetDetentView.swift +++ b/Loop/Views/Presets/Components/PresetDetentView.swift @@ -10,7 +10,6 @@ import LoopKit import LoopKitUI import SwiftUI import LoopCore -import LoopAlgorithm struct PresetDetentView: View { diff --git a/Loop/Views/Presets/EditPresetRangeView.swift b/Loop/Views/Presets/EditPresetRangeView.swift index 14096ea9aa..3535cbe02f 100644 --- a/Loop/Views/Presets/EditPresetRangeView.swift +++ b/Loop/Views/Presets/EditPresetRangeView.swift @@ -6,7 +6,6 @@ // Copyright © 2024 LoopKit Authors. All rights reserved. // import SwiftUI -import LoopAlgorithm import LoopKit import LoopKitUI diff --git a/Loop/Views/Presets/PresetPerformanceHistoryView.swift b/Loop/Views/Presets/PresetPerformanceHistoryView.swift index c0eec2209d..fc8a0f800c 100644 --- a/Loop/Views/Presets/PresetPerformanceHistoryView.swift +++ b/Loop/Views/Presets/PresetPerformanceHistoryView.swift @@ -6,7 +6,6 @@ // Copyright © 2026 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/Presets/PresetsHistoryView.swift b/Loop/Views/Presets/PresetsHistoryView.swift index 4d55c29b41..519fa620c4 100644 --- a/Loop/Views/Presets/PresetsHistoryView.swift +++ b/Loop/Views/Presets/PresetsHistoryView.swift @@ -6,7 +6,6 @@ // Copyright © 2024 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift b/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift index 111f6e27f2..6a00cfffd0 100644 --- a/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift +++ b/Loop/Views/Presets/PresetsPerformanceHistoryViewModel.swift @@ -6,7 +6,6 @@ // Copyright © 2026 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit @MainActor diff --git a/Loop/Views/Presets/PresetsView.swift b/Loop/Views/Presets/PresetsView.swift index db474c6340..6a8fdcf9e5 100644 --- a/Loop/Views/Presets/PresetsView.swift +++ b/Loop/Views/Presets/PresetsView.swift @@ -6,7 +6,6 @@ // Copyright © 2024 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/Presets/Training/Components/TherapySettingsExampleView.swift b/Loop/Views/Presets/Training/Components/TherapySettingsExampleView.swift index ec4593809e..9e5cbe786e 100644 --- a/Loop/Views/Presets/Training/Components/TherapySettingsExampleView.swift +++ b/Loop/Views/Presets/Training/Components/TherapySettingsExampleView.swift @@ -6,7 +6,6 @@ // Copyright © 2024 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/Presets/Training/PresetsTrainingContent.swift b/Loop/Views/Presets/Training/PresetsTrainingContent.swift index cff2c42de2..eb1a4a13a0 100644 --- a/Loop/Views/Presets/Training/PresetsTrainingContent.swift +++ b/Loop/Views/Presets/Training/PresetsTrainingContent.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/Loop/Views/SimpleBolusView.swift b/Loop/Views/SimpleBolusView.swift index a26032029f..736f75c278 100644 --- a/Loop/Views/SimpleBolusView.swift +++ b/Loop/Views/SimpleBolusView.swift @@ -10,7 +10,6 @@ import SwiftUI import LoopKit import LoopKitUI import LoopCore -import LoopAlgorithm struct SimpleBolusView: View { @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference diff --git a/Loop/Views/Statistics/AGPChartView.swift b/Loop/Views/Statistics/AGPChartView.swift index 53db129c10..55bac2b930 100644 --- a/Loop/Views/Statistics/AGPChartView.swift +++ b/Loop/Views/Statistics/AGPChartView.swift @@ -7,7 +7,6 @@ import SwiftUI import LoopKit -import LoopAlgorithm import LoopUI /// The ambulatory glucose profile, styled after the IDC / captūrAGP report. diff --git a/Loop/Views/Statistics/SetupComparisonCharts.swift b/Loop/Views/Statistics/SetupComparisonCharts.swift index 3c078947d8..d6e104f0f6 100644 --- a/Loop/Views/Statistics/SetupComparisonCharts.swift +++ b/Loop/Views/Statistics/SetupComparisonCharts.swift @@ -7,7 +7,6 @@ import SwiftUI import LoopKit -import LoopAlgorithm /// A horizontal time-in-range bar for one period, lows on the left. struct TimeInRangeStripe: View { diff --git a/Loop/Views/Statistics/StatisticsView.swift b/Loop/Views/Statistics/StatisticsView.swift index 23b41652fc..1befaa5062 100644 --- a/Loop/Views/Statistics/StatisticsView.swift +++ b/Loop/Views/Statistics/StatisticsView.swift @@ -8,7 +8,6 @@ import SwiftUI import LoopKit import LoopKitUI -import LoopAlgorithm import LoopUI /// "How am I doing?" overview — an Ambulatory Glucose Profile report: summary diff --git a/Loop/Views/Statistics/TimeInRangeBar.swift b/Loop/Views/Statistics/TimeInRangeBar.swift index 29d83cbe47..171c28b041 100644 --- a/Loop/Views/Statistics/TimeInRangeBar.swift +++ b/Loop/Views/Statistics/TimeInRangeBar.swift @@ -6,7 +6,7 @@ // import SwiftUI -import LoopAlgorithm +import LoopKit import LoopKitUI import LoopUI diff --git a/Loop/Views/StatusTableView.swift b/Loop/Views/StatusTableView.swift index be4012a8f2..a09e8b9690 100644 --- a/Loop/Views/StatusTableView.swift +++ b/Loop/Views/StatusTableView.swift @@ -6,7 +6,6 @@ // Copyright © 2024 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit import LoopKitUI import SwiftUI diff --git a/LoopCore/LoopCoreConstants.swift b/LoopCore/LoopCoreConstants.swift index 6d8edfea82..0b94b11e70 100644 --- a/LoopCore/LoopCoreConstants.swift +++ b/LoopCore/LoopCoreConstants.swift @@ -15,6 +15,12 @@ public enum LoopCoreConstants { /// The amount of time in the future a glucose value should be considered valid public static let futureGlucoseDataInterval = TimeInterval(minutes: 5) + /// How old glucose and pump data can be for the algorithm to run; matches LoopAlgorithm's limit + public static let inputDataRecencyInterval = TimeInterval(minutes: 15) + + /// The longest a carb entry can take to absorb; matches LoopAlgorithm's limit + public static let maximumCarbAbsorptionTimeInterval = TimeInterval(hours: 10) + public static let defaultCarbAbsorptionTimes: DefaultAbsorptionTimes = (fast: .minutes(30), medium: .hours(3), slow: .hours(5)) /// How much historical glucose to include in a dosing decision diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift index ba83a7aa55..43d1afa93a 100644 --- a/LoopCore/LoopSettings.swift +++ b/LoopCore/LoopSettings.swift @@ -7,7 +7,6 @@ import HealthKit import LoopKit -import LoopAlgorithm public extension AutomaticDosingStrategy { var title: String { diff --git a/LoopCore/Models/WatchContext.swift b/LoopCore/Models/WatchContext.swift index 9a0e37ad3b..a83fe31f63 100644 --- a/LoopCore/Models/WatchContext.swift +++ b/LoopCore/Models/WatchContext.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm public enum InsulinDeliveryWatchState: Int, Equatable { case neutralNoOverride diff --git a/LoopCore/Models/WatchPredictedGlucose.swift b/LoopCore/Models/WatchPredictedGlucose.swift index b17ca89c76..4f62763893 100644 --- a/LoopCore/Models/WatchPredictedGlucose.swift +++ b/LoopCore/Models/WatchPredictedGlucose.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm public struct WatchPredictedGlucose: Equatable { public let values: [PredictedGlucoseValue] diff --git a/LoopCore/NSUserDefaults.swift b/LoopCore/NSUserDefaults.swift index 8ed715f823..0975cefd9a 100644 --- a/LoopCore/NSUserDefaults.swift +++ b/LoopCore/NSUserDefaults.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm extension UserDefaults { diff --git a/LoopTests/Managers/CGMStalenessMonitorTests.swift b/LoopTests/Managers/CGMStalenessMonitorTests.swift index 5971d75daa..3f4853ab10 100644 --- a/LoopTests/Managers/CGMStalenessMonitorTests.swift +++ b/LoopTests/Managers/CGMStalenessMonitorTests.swift @@ -9,7 +9,6 @@ import XCTest import Foundation import LoopKit -import LoopAlgorithm @testable import Loop class CGMStalenessMonitorTests: XCTestCase { diff --git a/LoopTests/Managers/DeviceDataManagerTests.swift b/LoopTests/Managers/DeviceDataManagerTests.swift index b3725a8545..bf12ef5ede 100644 --- a/LoopTests/Managers/DeviceDataManagerTests.swift +++ b/LoopTests/Managers/DeviceDataManagerTests.swift @@ -8,7 +8,6 @@ import XCTest import HealthKit -import LoopAlgorithm import LoopKit import LoopKitUI import LoopCore diff --git a/LoopTests/Managers/DoseEnactorTests.swift b/LoopTests/Managers/DoseEnactorTests.swift index 5609d03952..39b408de6b 100644 --- a/LoopTests/Managers/DoseEnactorTests.swift +++ b/LoopTests/Managers/DoseEnactorTests.swift @@ -9,7 +9,6 @@ import XCTest import Foundation import LoopKit -import LoopAlgorithm @testable import Loop diff --git a/LoopTests/Managers/GlucoseAlertManagerTests.swift b/LoopTests/Managers/GlucoseAlertManagerTests.swift index 7091479a3f..ba53cb6733 100644 --- a/LoopTests/Managers/GlucoseAlertManagerTests.swift +++ b/LoopTests/Managers/GlucoseAlertManagerTests.swift @@ -6,7 +6,6 @@ // import XCTest -import LoopAlgorithm import LoopKit import LoopKitUI @testable import Loop diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index 60a5e08c68..fab964e866 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -7,8 +7,8 @@ // import XCTest -import LoopKit import LoopAlgorithm +import LoopKit @testable import LoopCore @testable import Loop @@ -185,7 +185,7 @@ class LoopDataManagerTests: XCTestCase { let insulinSensitivitySchedule = InsulinSensitivitySchedule( unit: .milligramsPerDeciliter, dailyItems: [ - RepeatingScheduleValue(startTime: 0, value: predictionInput.sensitivity.first!.value.doubleValue(for: .milligramsPerDeciliter)) + RepeatingScheduleValue(startTime: 0, value: predictionInput.sensitivityMgdlPerUnit.first!.value) ], timeZone: .utcTimeZone )! @@ -230,7 +230,7 @@ class LoopDataManagerTests: XCTestCase { for (expected, calculated) in zip(expectedPredictedGlucose, predictedGlucose!) { XCTAssertEqual(expected.startDate, calculated.startDate) - XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: defaultAccuracy) + XCTAssertEqual(expected.quantity.doubleValue(for: .milligramsPerDeciliter), calculated.mgdl, accuracy: defaultAccuracy) } await loopDataManager.loop() @@ -255,6 +255,82 @@ class LoopDataManagerTests: XCTestCase { XCTAssertEqual(0.2, deliveryDelegate.lastEnact.bolus!, accuracy: defaultAccuracy) } + private func mmol(_ mgdl: Double) -> Double { + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl).doubleValue(for: .millimolesPerLiter) + } + + /// The setUp therapy settings, expressed in mmol/L + private func useMmolSettings() { + settingsProvider.settings.insulinSensitivitySchedule = InsulinSensitivitySchedule( + unit: .millimolesPerLiter, + dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: mmol(45)), + RepeatingScheduleValue(startTime: 32400, value: mmol(55)) + ], + timeZone: .utcTimeZone + )! + settingsProvider.settings.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: .millimolesPerLiter, dailyItems: [ + RepeatingScheduleValue(startTime: TimeInterval(0), value: DoubleRange(minValue: mmol(100), maxValue: mmol(110))), + RepeatingScheduleValue(startTime: TimeInterval(28800), value: DoubleRange(minValue: mmol(90), maxValue: mmol(100))), + RepeatingScheduleValue(startTime: TimeInterval(75600), value: DoubleRange(minValue: mmol(100), maxValue: mmol(110))) + ], timeZone: .utcTimeZone)! + settingsProvider.settings.suspendThreshold = GlucoseThreshold(unit: .millimolesPerLiter, value: mmol(75)) + } + + func testMmolSettingsDoseTheSameAsMgdl() async throws { + useMmolSettings() + glucoseStore.storedGlucose = [ + StoredGlucoseSample(startDate: d(.minutes(-1)), quantity: LoopQuantity(unit: .millimolesPerLiter, doubleValue: mmol(120))), + ] + + let input = try await loopDataManager.fetchData(for: now) + let sensitivityNow = input.sensitivityMgdlPerUnit.first { $0.startDate <= now && $0.endDate > now }!.value + let targetNow = input.targetMgdl.first { $0.startDate <= now && $0.endDate > now }!.value + XCTAssertEqual(sensitivityNow, 55, accuracy: 1e-9) + XCTAssertEqual(targetNow.lowerBound, 90, accuracy: 1e-9) + XCTAssertEqual(targetNow.upperBound, 100, accuracy: 1e-9) + XCTAssertEqual(input.suspendThresholdMgdl!, 75, accuracy: 1e-9) + + await loopDataManager.updateDisplayState() + XCTAssertEqual(120, loopDataManager.eventualBG!, accuracy: 1e-9) + + await loopDataManager.loop() + XCTAssertEqual(0.2, deliveryDelegate.lastEnact.bolus!, accuracy: defaultAccuracy) + } + + func testMmolSettingsRecommendTheSameManualBolusAsMgdl() async throws { + let values: [Double] = [100, 130, 160, 190] + func history(_ unit: LoopUnit) -> [StoredGlucoseSample] { + values.enumerated().map { index, mgdl in + let value = unit == .milligramsPerDeciliter ? mgdl : mmol(mgdl) + return StoredGlucoseSample(startDate: d(.minutes(Double(5 * index - 18))), quantity: LoopQuantity(unit: unit, doubleValue: value)) + } + } + let carbs = CarbEntryChange.add(NewCarbEntry(syncIdentifier: "mmol-test", quantity: .carbs(value: 30), startDate: d(.minutes(-1)), foodType: nil, absorptionTime: .hours(3))) + + glucoseStore.storedGlucose = history(.milligramsPerDeciliter) + let mgdlRecommendation = try await loopDataManager.recommendManualBolus(carbEntryChange: carbs)! + let mgdlPreview = try await loopDataManager.generatePrediction(carbEntryChange: carbs, potentialDose: nil, manualGlucose: nil).predictedGlucose + + useMmolSettings() + glucoseStore.storedGlucose = history(.millimolesPerLiter) + let mmolRecommendation = try await loopDataManager.recommendManualBolus(carbEntryChange: carbs)! + let mmolPreview = try await loopDataManager.generatePrediction(carbEntryChange: carbs, potentialDose: nil, manualGlucose: nil).predictedGlucose + + XCTAssertGreaterThan(mgdlRecommendation.amount, 0) + XCTAssertEqual(mmolRecommendation.amount, mgdlRecommendation.amount, accuracy: 1e-9) + XCTAssertEqual(mmolRecommendation.notice == nil, mgdlRecommendation.notice == nil) + XCTAssertEqual(mmolPreview.count, mgdlPreview.count) + for (mmolValue, mgdlValue) in zip(mmolPreview, mgdlPreview) { + XCTAssertEqual(mmolValue.startDate, mgdlValue.startDate) + XCTAssertEqual(mmolValue.quantity.doubleValue(for: .milligramsPerDeciliter), mgdlValue.quantity.doubleValue(for: .milligramsPerDeciliter), accuracy: 1e-9) + } + } + + func testLoopCoreConstantsMatchTheAlgorithm() { + XCTAssertEqual(LoopCoreConstants.inputDataRecencyInterval, Algorithm.inputDataRecencyInterval) + XCTAssertEqual(LoopCoreConstants.maximumCarbAbsorptionTimeInterval, LoopAlgorithm.CarbMath.maximumAbsorptionTimeInterval) + } func testHighAndFalling() async { glucoseStore.storedGlucose = [ @@ -513,9 +589,9 @@ class LoopDataManagerTests: XCTestCase { let input = try await loopDataManager.fetchData(for: now, ensureCarbCoverageStart: entryDate) let ratio = try XCTUnwrap(input.carbRatio.closestPrior(to: entryDate)) - let sensitivity = try XCTUnwrap(input.sensitivity.closestPrior(to: entryDate)) + let sensitivity = try XCTUnwrap(input.sensitivityMgdlPerUnit.closestPrior(to: entryDate)) XCTAssertEqual(ratio.value, settingsProvider.settings.carbRatioSchedule!.value(at: entryDate) / 2) - XCTAssertEqual(sensitivity.value.doubleValue(for: .milligramsPerDeciliter), + XCTAssertEqual(sensitivity.value, settingsProvider.settings.insulinSensitivitySchedule!.value(at: entryDate) / 2) } @@ -523,7 +599,7 @@ class LoopDataManagerTests: XCTestCase { let normal = try await loopDataManager.fetchData(for: now) let recent = try await loopDataManager.fetchData(for: now, ensureCarbCoverageStart: d(.minutes(-5))) XCTAssertEqual(normal.carbRatio, recent.carbRatio) - XCTAssertEqual(normal.sensitivity, recent.sensitivity) + XCTAssertEqual(normal.sensitivityMgdlPerUnit, recent.sensitivityMgdlPerUnit) } private func assertMissingCarbScheduleThrows(_ detail: ConfigurationErrorDetail, preview: Bool) async throws { @@ -574,9 +650,9 @@ class LoopDataManagerTests: XCTestCase { func testFetchDataWithHighInsulinNeedsPresetMitigation() async throws { var input = try await loopDataManager.fetchData(for: now) - XCTAssertEqual(input.target.count, 1) - XCTAssertEqual(input.target[0].value.doubleRange(for: .milligramsPerDeciliter), DoubleRange(minValue: 90, maxValue: 100)) - XCTAssertEqual(input.suspendThreshold?.doubleValue(for: .milligramsPerDeciliter), 75.0) + XCTAssertEqual(input.targetMgdl.count, 1) + XCTAssertEqual(input.targetMgdl[0].value, 90...100) + XCTAssertEqual(input.suspendThresholdMgdl, 75.0) let override = TemporaryScheduleOverride( context: .custom, @@ -593,9 +669,9 @@ class LoopDataManagerTests: XCTestCase { temporaryPresetsManager.scheduleOverride = override input = try await loopDataManager.fetchData(for: now) - XCTAssertEqual(input.target.count, 1) - XCTAssertEqual(input.target[0].value.doubleRange(for: .milligramsPerDeciliter), DoubleRange(minValue: 110, maxValue: 110)) - XCTAssertEqual(input.suspendThreshold?.doubleValue(for: .milligramsPerDeciliter), 110.0) + XCTAssertEqual(input.targetMgdl.count, 1) + XCTAssertEqual(input.targetMgdl[0].value, 110...110) + XCTAssertEqual(input.suspendThresholdMgdl, 110.0) } } @@ -634,7 +710,7 @@ extension LoopQuantity { extension LoopDataManager { var eventualBG: Double? { - displayState.output?.predictedGlucose.last?.quantity.doubleValue(for: .milligramsPerDeciliter) + displayState.output?.predictedGlucose.last?.mgdl } } @@ -642,9 +718,7 @@ extension StoredGlucoseSample { static func from(fixture: FixtureGlucoseSample) -> StoredGlucoseSample { return StoredGlucoseSample( startDate: fixture.startDate, - quantity: fixture.quantity, - condition: fixture.condition, - trendRate: fixture.trendRate, + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: fixture.mgdl), isDisplayOnly: fixture.isDisplayOnly, wasUserEntered: fixture.wasUserEntered ) @@ -668,7 +742,7 @@ extension StoredCarbEntry { static func from(fixture: FixtureCarbEntry) -> StoredCarbEntry { return StoredCarbEntry( startDate: fixture.startDate, - quantity: fixture.quantity, + quantity: LoopQuantity(unit: .gram, doubleValue: fixture.grams), foodType: fixture.foodType, absorptionTime: fixture.absorptionTime ) diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index 66e0ea6664..ab07e9cb87 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -7,22 +7,18 @@ // import XCTest +import LoopAlgorithm import LoopCore import LoopKit -import LoopAlgorithm @testable import Loop -fileprivate class MockGlucoseSample: GlucoseSampleValue { +fileprivate class MockGlucoseSample: LoopAlgorithm.GlucoseSampleValue { let provenanceIdentifier = "" let isDisplayOnly: Bool let wasUserEntered: Bool - let condition: GlucoseCondition? = nil - let trendRate: LoopQuantity? = nil - var trend: LoopKit.GlucoseTrend? - var syncIdentifier: String? - let quantity: LoopQuantity = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 100) + let mgdl: Double = 100 let startDate: Date init(startDate: Date, isDisplayOnly: Bool = false, wasUserEntered: Bool = false) { @@ -222,17 +218,17 @@ class MealDetectionManagerTests: XCTestCase { doses: [], carbEntries: testType.carbEntries.map { $0.asStoredCarbEntry }, predictionStart: date, - basal: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: 1.0)])!.between(start: historyStart, end: date), - sensitivity: testType.insulinSensitivitySchedule.quantitiesBetween(start: historyStart, end: date), - carbRatio: testType.carbSchedule.between(start: historyStart, end: date), - target: glucoseTarget!.quantityBetween(start: historyStart, end: date), - suspendThreshold: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 65), + basal: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: 1.0)])!.between(start: historyStart, end: date).algorithmSchedule(), + sensitivityMgdlPerUnit: testType.insulinSensitivitySchedule.quantitiesBetween(start: historyStart, end: date).algorithmMgdlSchedule(), + carbRatio: testType.carbSchedule.between(start: historyStart, end: date).algorithmSchedule(), + targetMgdl: glucoseTarget!.quantityBetween(start: historyStart, end: date).algorithmMgdlRanges(), + suspendThresholdMgdl: 65, maxBolus: maximumBolus!, maxBasalRate: maximumBasalRatePerHour, useIntegralRetrospectiveCorrection: false, includePositiveVelocityAndRC: true, carbAbsorptionModel: .piecewiseLinear, - recommendationInsulinModel: ExponentialInsulinModelPreset.rapidActingAdult.model, + recommendationInsulinModel: AlgorithmInsulinModel(ExponentialInsulinModelPreset.rapidActingAdult.model), recommendationType: .automaticBolus) // These tests don't actually run the loop algorithm directly; they were written to take ICE from fixtures, compute carb effects, and subtract them. @@ -243,13 +239,13 @@ class MealDetectionManagerTests: XCTestCase { let carbStatus = carbEntries.map( to: counteractionEffects, carbRatio: algorithmInput.carbRatio, - insulinSensitivity: algorithmInput.sensitivity + insulinSensitivityMgdlPerUnit: algorithmInput.sensitivityMgdlPerUnit ) let carbEffects = carbStatus.dynamicGlucoseEffects( - from: date.addingTimeInterval(-IntegralRetrospectiveCorrection.retrospectionInterval), + from: date.addingTimeInterval(-.minutes(180)), carbRatios: algorithmInput.carbRatio, - insulinSensitivities: algorithmInput.sensitivity, + insulinSensitivitiesMgdlPerUnit: algorithmInput.sensitivityMgdlPerUnit, absorptionModel: algorithmInput.carbAbsorptionModel.model ) @@ -279,15 +275,15 @@ class MealDetectionManagerTests: XCTestCase { } - private func counteractionEffects(for testType: MissedMealTestType) -> [GlucoseEffectVelocity] { + private func counteractionEffects(for testType: MissedMealTestType) -> [LoopAlgorithm.GlucoseEffectVelocity] { let fixture: [JSONDictionary] = loadFixture(testType.counteractionEffectFixture) let dateFormatter = ISO8601DateFormatter.localTimeDate() return fixture.map { - GlucoseEffectVelocity(startDate: dateFormatter.date(from: $0["startDate"] as! String)!, - endDate: dateFormatter.date(from: $0["endDate"] as! String)!, - quantity: LoopQuantity(unit: LoopUnit(from: $0["unit"] as! String), - doubleValue:$0["value"] as! Double)) + LoopAlgorithm.GlucoseEffectVelocity(startDate: dateFormatter.date(from: $0["startDate"] as! String)!, + endDate: dateFormatter.date(from: $0["endDate"] as! String)!, + mgdlPerSecond: LoopQuantity(unit: LoopUnit(from: $0["unit"] as! String), + doubleValue: $0["value"] as! Double).doubleValue(for: .milligramsPerDeciliterPerSecond)) } } diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index 93aaa73068..51331229e6 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -8,7 +8,6 @@ import HealthKit import LoopKit -import LoopAlgorithm @testable import Loop class MockDoseStore: DoseStoreProtocol { diff --git a/LoopTests/Mock Stores/MockGlucoseStore.swift b/LoopTests/Mock Stores/MockGlucoseStore.swift index eb4c488841..5ec9452116 100644 --- a/LoopTests/Mock Stores/MockGlucoseStore.swift +++ b/LoopTests/Mock Stores/MockGlucoseStore.swift @@ -7,7 +7,6 @@ // import LoopKit -import LoopAlgorithm @testable import Loop class MockGlucoseStore: GlucoseStoreProtocol { diff --git a/LoopTests/Mocks/LoopControlMock.swift b/LoopTests/Mocks/LoopControlMock.swift index af720c315a..3c03d8f9ee 100644 --- a/LoopTests/Mocks/LoopControlMock.swift +++ b/LoopTests/Mocks/LoopControlMock.swift @@ -8,7 +8,6 @@ import XCTest import Foundation -import LoopAlgorithm import LoopKit @testable import Loop diff --git a/LoopTests/Mocks/MockDeliveryDelegate.swift b/LoopTests/Mocks/MockDeliveryDelegate.swift index 9881a8053a..1f37b4bbd1 100644 --- a/LoopTests/Mocks/MockDeliveryDelegate.swift +++ b/LoopTests/Mocks/MockDeliveryDelegate.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm @testable import Loop class MockDeliveryDelegate: DeliveryDelegate { diff --git a/LoopTests/Mocks/MockSettingsProvider.swift b/LoopTests/Mocks/MockSettingsProvider.swift index ae8b194957..975a63dff1 100644 --- a/LoopTests/Mocks/MockSettingsProvider.swift +++ b/LoopTests/Mocks/MockSettingsProvider.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm @testable import Loop @Observable diff --git a/LoopTests/Models/GlucoseStatisticsTests.swift b/LoopTests/Models/GlucoseStatisticsTests.swift index 0ad0a77b14..bac7131796 100644 --- a/LoopTests/Models/GlucoseStatisticsTests.swift +++ b/LoopTests/Models/GlucoseStatisticsTests.swift @@ -8,7 +8,6 @@ import XCTest import LoopKit import LoopKitUI -import LoopAlgorithm @testable import Loop final class GlucoseStatisticsTests: XCTestCase { diff --git a/LoopTests/Models/InsulinDeliveryHistoryTests.swift b/LoopTests/Models/InsulinDeliveryHistoryTests.swift index 150161c6db..ddd22f0141 100644 --- a/LoopTests/Models/InsulinDeliveryHistoryTests.swift +++ b/LoopTests/Models/InsulinDeliveryHistoryTests.swift @@ -6,8 +6,8 @@ // import XCTest -import LoopKit import LoopAlgorithm +import LoopKit @testable import Loop final class InsulinDeliveryHistoryTests: XCTestCase { @@ -23,11 +23,11 @@ final class InsulinDeliveryHistoryTests: XCTestCase { } private func bolus(_ units: Double, at start: Date, duration: TimeInterval = 0, automatic: Bool = false) -> SimpleInsulinDose { - SimpleInsulinDose(deliveryType: .bolus, automatic: automatic, startDate: start, endDate: start.addingTimeInterval(duration), volume: units, insulinModel: ExponentialInsulinModelPreset.rapidActingAdult) + SimpleInsulinDose(deliveryType: .bolus, automatic: automatic, startDate: start, endDate: start.addingTimeInterval(duration), volume: units, insulinModel: LoopAlgorithm.ExponentialInsulinModelPreset.rapidActingAdult) } private func tempBasal(rate: Double, from start: Date, to end: Date) -> SimpleInsulinDose { - SimpleInsulinDose(deliveryType: .basal, automatic: true, startDate: start, endDate: end, volume: rate * end.timeIntervalSince(start).hours, insulinModel: ExponentialInsulinModelPreset.rapidActingAdult) + SimpleInsulinDose(deliveryType: .basal, automatic: true, startDate: start, endDate: end, volume: rate * end.timeIntervalSince(start).hours, insulinModel: LoopAlgorithm.ExponentialInsulinModelPreset.rapidActingAdult) } private func scheduledBasal(_ rate: Double, from start: Date, to end: Date) -> [AbsoluteScheduleValue] { diff --git a/LoopTests/Models/SetBolusUserInfoTests.swift b/LoopTests/Models/SetBolusUserInfoTests.swift index 7ce903faa8..b4565319eb 100644 --- a/LoopTests/Models/SetBolusUserInfoTests.swift +++ b/LoopTests/Models/SetBolusUserInfoTests.swift @@ -7,7 +7,6 @@ // import XCTest -import LoopAlgorithm import LoopKit import LoopCore @testable import Loop diff --git a/LoopTests/Models/SetupChangeTests.swift b/LoopTests/Models/SetupChangeTests.swift index 6abfc608ca..4f39d2d889 100644 --- a/LoopTests/Models/SetupChangeTests.swift +++ b/LoopTests/Models/SetupChangeTests.swift @@ -7,7 +7,6 @@ import XCTest import LoopKit -import LoopAlgorithm @testable import Loop final class SetupChangeTests: XCTestCase { diff --git a/LoopTests/Models/SetupComparisonTests.swift b/LoopTests/Models/SetupComparisonTests.swift index f01cded2ad..30ccdb6462 100644 --- a/LoopTests/Models/SetupComparisonTests.swift +++ b/LoopTests/Models/SetupComparisonTests.swift @@ -6,8 +6,8 @@ // import XCTest -import LoopKit import LoopAlgorithm +import LoopKit @testable import Loop final class SetupComparisonTests: XCTestCase { @@ -88,7 +88,7 @@ final class SetupComparisonTests: XCTestCase { let now = changeDate.addingTimeInterval(.days(2)) let start = changeDate - .days(21) let glucose = samples(from: start, to: now) { _ in 120 } - let bolus = SimpleInsulinDose(deliveryType: .bolus, automatic: false, startDate: changeDate + .hours(1), endDate: changeDate + .hours(1), volume: 4, insulinModel: ExponentialInsulinModelPreset.rapidActingAdult) + let bolus = SimpleInsulinDose(deliveryType: .bolus, automatic: false, startDate: changeDate + .hours(1), endDate: changeDate + .hours(1), volume: 4, insulinModel: LoopAlgorithm.ExponentialInsulinModelPreset.rapidActingAdult) let history = InsulinDeliveryHistory( doses: [bolus], scheduledBasal: [AbsoluteScheduleValue(startDate: start, endDate: now, value: 1.0)], diff --git a/LoopTests/Models/SimpleBolusCalculatorTests.swift b/LoopTests/Models/SimpleBolusCalculatorTests.swift index e143bfea83..a03d96db8a 100644 --- a/LoopTests/Models/SimpleBolusCalculatorTests.swift +++ b/LoopTests/Models/SimpleBolusCalculatorTests.swift @@ -8,7 +8,6 @@ import Foundation import XCTest -import LoopAlgorithm import LoopKit @testable import Loop diff --git a/LoopTests/Models/TempBasalRecommendationTests.swift b/LoopTests/Models/TempBasalRecommendationTests.swift index 51366a5fcc..05778fc0ec 100644 --- a/LoopTests/Models/TempBasalRecommendationTests.swift +++ b/LoopTests/Models/TempBasalRecommendationTests.swift @@ -8,7 +8,6 @@ import XCTest import LoopKit -import LoopAlgorithm @testable import Loop class TempBasalRecommendationTests: XCTestCase { diff --git a/LoopTests/Models/TemporaryScheduleOverrideAlgorithmTests.swift b/LoopTests/Models/TemporaryScheduleOverrideAlgorithmTests.swift new file mode 100644 index 0000000000..6c6b435e74 --- /dev/null +++ b/LoopTests/Models/TemporaryScheduleOverrideAlgorithmTests.swift @@ -0,0 +1,139 @@ +// +// TemporaryScheduleOverrideAlgorithmTests.swift +// LoopTests +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import XCTest +import LoopAlgorithm +@testable import LoopKit +@testable import Loop + +class TemporaryScheduleOverrideAlgorithmTests: XCTestCase { + + let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: .currentFixed) + + private func date(at time: String) -> Date { + return dateFormatter.date(from: "2019-01-01T\(time):00")! + } + + func testOverrideScheduleAnnotatingReservoirSplitsDose() { + let schedule = BasalRateSchedule(dailyItems: [ + RepeatingScheduleValue(startTime: 0, value: 0.225), + RepeatingScheduleValue(startTime: 3600.0, value: 0.18000000000000002), + RepeatingScheduleValue(startTime: 10800.0, value: 0.135), + RepeatingScheduleValue(startTime: 12689.855275034904, value: 0.15), + RepeatingScheduleValue(startTime: 21600.0, value: 0.2), + RepeatingScheduleValue(startTime: 32400.0, value: 0.2), + RepeatingScheduleValue(startTime: 50400.0, value: 0.2), + RepeatingScheduleValue(startTime: 52403.79680299759, value: 0.16000000000000003), + RepeatingScheduleValue(startTime: 63743.58014559746, value: 0.2), + RepeatingScheduleValue(startTime: 63743.58014583588, value: 0.16000000000000003), + RepeatingScheduleValue(startTime: 69968.05249071121, value: 0.2), + RepeatingScheduleValue(startTime: 69968.05249094963, value: 0.18000000000000002), + RepeatingScheduleValue(startTime: 79200.0, value: 0.225), + ])! + + let start = date(at: "19:25") + + let dose = FixtureInsulinDose( + deliveryType: .basal, + startDate: start, + endDate: date(at: "19:30"), + volume: 0.8, + insulinType: .novolog + ) + + let timeline = schedule.between(start: start, end: start.addingTimeInterval(InsulinMath.longestInsulinActivityDuration)).algorithmSchedule() + + let annotated = [dose].annotated(with: timeline) + + XCTAssertEqual(3, annotated.count) + XCTAssertEqual(dose.volume, annotated.map { $0.volume }.reduce(0, +)) + } + + func testPreMealPreset() { + let now = ISO8601DateFormatter().date(from: "2020-03-11T12:13:14-0700")! + + var input = AlgorithmInputFixture.mock(for: now) + input.recommendationType = .tempBasal + + func d(_ interval: TimeInterval) -> Date { + return now.addingTimeInterval(interval) + } + + // Flat, in range bg. + input.glucoseHistory = [ + FixtureGlucoseSample(startDate: d(.minutes(-19)), mgdl: 105), + FixtureGlucoseSample(startDate: d(.minutes(-14)), mgdl: 105), + FixtureGlucoseSample(startDate: d(.minutes(-9)), mgdl: 105), + FixtureGlucoseSample(startDate: d(.minutes(-4)), mgdl: 105), + ] + + let scheduledRange = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 100)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 110) + let overrideRange = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 80)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 90) + + let overrideStartTime = d(.minutes(-10)) + + let overrides: [TemporaryScheduleOverride] = [ + .init( + context: .preMeal, + settings: .init(targetRange: overrideRange), + startDate: overrideStartTime, + duration: .finite(.hours(1)), + enactTrigger: .local, + syncIdentifier: UUID() + ) + ] + + let targetTimeline: [LoopKit.AbsoluteScheduleValue>] = [ + AbsoluteScheduleValue(startDate: d(.hours(-2)), endDate: d(.hours(10)), value: scheduledRange) + ] + + input.targetMgdl = overrides.applyTarget(over: targetTimeline, at: now).algorithmMgdlRanges() + + input.doses = [] + input.carbEntries = [] + + let output = Algorithm.run(input: input) + + let recommendedRate = output.recommendation!.automatic!.basalAdjustment.unitsPerHour + let activeInsulin = output.activeInsulin! + XCTAssertEqual(activeInsulin, 0) + XCTAssertEqual(recommendedRate, 1.727, accuracy: 0.01) + } +} + +fileprivate extension AlgorithmInputFixture { + /// Mocks stable, in range glucose, no insulin, no carbs, with reasonable settings + static func mock(for now: Date = Date()) -> AlgorithmInputFixture { + + func d(_ interval: TimeInterval) -> Date { + return now.addingTimeInterval(interval) + } + + let forecastEnd = now.addingTimeInterval(InsulinMath.defaultInsulinActivityDuration).dateCeiledToTimeInterval(GlucoseMath.defaultDelta) + + return AlgorithmInputFixture( + predictionStart: now, + glucoseHistory: [ + FixtureGlucoseSample(startDate: d(.minutes(-19)), mgdl: 100), + FixtureGlucoseSample(startDate: d(.minutes(-14)), mgdl: 120), + FixtureGlucoseSample(startDate: d(.minutes(-9)), mgdl: 140), + FixtureGlucoseSample(startDate: d(.minutes(-4)), mgdl: 160), + ], + doses: [], + carbEntries: [], + basal: [LoopAlgorithm.AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 1.0)], + sensitivityMgdlPerUnit: [LoopAlgorithm.AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: forecastEnd, value: 55)], + carbRatio: [LoopAlgorithm.AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 10)], + targetMgdl: [LoopAlgorithm.AbsoluteScheduleValue(startDate: d(.hours(-10)), endDate: now, value: 100...110)], + suspendThresholdMgdl: 65, + maxBolus: 6, + maxBasalRate: 8, + recommendationInsulinType: .novolog, + recommendationType: .tempBasal + ) + } +} diff --git a/LoopTests/Models/WatchHistoricalGlucoseTest.swift b/LoopTests/Models/WatchHistoricalGlucoseTest.swift index 6d0f85be19..9d485def90 100644 --- a/LoopTests/Models/WatchHistoricalGlucoseTest.swift +++ b/LoopTests/Models/WatchHistoricalGlucoseTest.swift @@ -8,7 +8,6 @@ import XCTest import HealthKit -import LoopAlgorithm import LoopKit @testable import Loop diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 0b939ea0fa..0287fbedbe 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -7,12 +7,12 @@ // import HealthKit +import LoopAlgorithm import LoopCore import LoopKit import LoopKitUI import SwiftUI import XCTest -import LoopAlgorithm @testable import Loop @@ -193,7 +193,7 @@ class BolusEntryViewModelTests: XCTestCase { let input = try await delegate.fetchData(for: Self.exampleStartDate, presumePresetEndingNow: false, ensureDosingCoverageStart: nil, ensureCarbCoverageStart: nil, projectOngoingDoses: false) let prediction = try input.predictGlucose() await bolusEntryViewModel.update() - XCTAssertEqual(prediction, bolusEntryViewModel.predictedGlucoseValues.map { PredictedGlucoseValue(startDate: $0.startDate, quantity: $0.quantity) }) + XCTAssertEqual(prediction, bolusEntryViewModel.predictedGlucoseValues.map { LoopAlgorithm.PredictedGlucoseValue(startDate: $0.startDate, mgdl: $0.quantity.doubleValue(for: .milligramsPerDeciliter)) }) } catch { XCTFail("Unable to generate prediction") } @@ -205,7 +205,7 @@ class BolusEntryViewModelTests: XCTestCase { let prediction = try input.predictGlucose() await bolusEntryViewModel.update() bolusEntryViewModel.manualGlucoseQuantity = Self.exampleManualGlucoseQuantity - XCTAssertEqual(prediction, bolusEntryViewModel.predictedGlucoseValues.map { PredictedGlucoseValue(startDate: $0.startDate, quantity: $0.quantity) }) + XCTAssertEqual(prediction, bolusEntryViewModel.predictedGlucoseValues.map { LoopAlgorithm.PredictedGlucoseValue(startDate: $0.startDate, mgdl: $0.quantity.doubleValue(for: .milligramsPerDeciliter)) }) } catch { XCTFail("Unable to generate prediction") } @@ -302,7 +302,7 @@ class BolusEntryViewModelTests: XCTestCase { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendation = ManualBolusRecommendation(amount: 1.25) - delegate.algorithmOutput.recommendationResult = .success(.init(manual: recommendation)) + delegate.recommendationResult = .success(recommendation) await bolusEntryViewModel.update() @@ -325,7 +325,7 @@ class BolusEntryViewModelTests: XCTestCase { amount: 1.25, notice: BolusRecommendationNotice.glucoseBelowSuspendThreshold(minGlucose: Self.exampleGlucoseValue) ) - delegate.algorithmOutput.recommendationResult = .success(.init(manual: recommendation)) + delegate.recommendationResult = .success(recommendation) await bolusEntryViewModel.update() XCTAssertTrue(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -338,7 +338,7 @@ class BolusEntryViewModelTests: XCTestCase { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) delegate.settings.suspendThreshold = nil let recommendation = ManualBolusRecommendation(amount: 1.25, notice: BolusRecommendationNotice.glucoseBelowSuspendThreshold(minGlucose: Self.exampleGlucoseValue)) - delegate.algorithmOutput.recommendationResult = .success(.init(manual: recommendation)) + delegate.recommendationResult = .success(recommendation) await bolusEntryViewModel.update() XCTAssertTrue(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -350,7 +350,7 @@ class BolusEntryViewModelTests: XCTestCase { func testUpdateRecommendedBolusWithOtherNotice() async throws { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendation = ManualBolusRecommendation(amount: 1.25, notice: BolusRecommendationNotice.currentGlucoseBelowTarget(glucose: Self.exampleGlucoseValue)) - delegate.algorithmOutput.recommendationResult = .success(.init(manual: recommendation)) + delegate.recommendationResult = .success(recommendation) await bolusEntryViewModel.update() XCTAssertTrue(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -361,11 +361,11 @@ class BolusEntryViewModelTests: XCTestCase { func testMissingCarbScheduleClearsPreviousRecommendation() async throws { for detail: ConfigurationErrorDetail in [.carbRatioSchedule, .insulinSensitivitySchedule] { - delegate.algorithmOutput.recommendationResult = .success(.init(manual: ManualBolusRecommendation(amount: 1.25))) + delegate.recommendationResult = .success(ManualBolusRecommendation(amount: 1.25)) await bolusEntryViewModel.update() XCTAssertNotNil(bolusEntryViewModel.recommendedBolus) - delegate.algorithmOutput.recommendationResult = .failure(LoopError.configurationError(detail)) + delegate.recommendationResult = .failure(LoopError.configurationError(detail)) await bolusEntryViewModel.update() XCTAssertNil(bolusEntryViewModel.recommendedBolus) XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) @@ -374,7 +374,7 @@ class BolusEntryViewModelTests: XCTestCase { func testUpdateRecommendedBolusThrowsMissingDataError() async throws { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) - delegate.algorithmOutput.recommendationResult = .failure(LoopError.missingDataError(.glucose)) + delegate.recommendationResult = .failure(LoopError.missingDataError(.glucose)) await bolusEntryViewModel.update() XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -384,7 +384,7 @@ class BolusEntryViewModelTests: XCTestCase { func testUpdateRecommendedBolusThrowsPumpDataTooOld() async throws { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) - delegate.algorithmOutput.recommendationResult = .failure(LoopError.pumpDataTooOld(date: now)) + delegate.recommendationResult = .failure(LoopError.pumpDataTooOld(date: now)) await bolusEntryViewModel.update() XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -394,7 +394,7 @@ class BolusEntryViewModelTests: XCTestCase { func testUpdateRecommendedBolusThrowsGlucoseTooOld() async throws { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) - delegate.algorithmOutput.recommendationResult = .failure(LoopError.glucoseTooOld(date: now)) + delegate.recommendationResult = .failure(LoopError.glucoseTooOld(date: now)) await bolusEntryViewModel.update() XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -404,7 +404,7 @@ class BolusEntryViewModelTests: XCTestCase { func testUpdateRecommendedBolusThrowsInvalidFutureGlucose() async throws { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) - delegate.algorithmOutput.recommendationResult = .failure(LoopError.invalidFutureGlucose(date: now)) + delegate.recommendationResult = .failure(LoopError.invalidFutureGlucose(date: now)) await bolusEntryViewModel.update() XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -414,7 +414,7 @@ class BolusEntryViewModelTests: XCTestCase { func testUpdateRecommendedBolusThrowsOtherError() async throws { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) - delegate.algorithmOutput.recommendationResult = .failure(LoopError.pumpSuspended) + delegate.recommendationResult = .failure(LoopError.pumpSuspended) await bolusEntryViewModel.update() XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendedBolus = bolusEntryViewModel.recommendedBolus @@ -436,7 +436,7 @@ class BolusEntryViewModelTests: XCTestCase { XCTAssertFalse(bolusEntryViewModel.isBolusRecommended) let recommendation = ManualBolusRecommendation(amount: 1.25) - delegate.algorithmOutput.recommendationResult = .success(.init(manual: recommendation)) + delegate.recommendationResult = .success(recommendation) await bolusEntryViewModel.update() XCTAssertTrue(bolusEntryViewModel.isBolusRecommended) @@ -887,16 +887,16 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { carbEntries: [], predictionStart: Date(), basal: [], - sensitivity: [], + sensitivityMgdlPerUnit: [], carbRatio: [], - target: [], - suspendThreshold: nil, + targetMgdl: [], + suspendThresholdMgdl: nil, maxBolus: 3, maxBasalRate: 6, useIntegralRetrospectiveCorrection: false, includePositiveVelocityAndRC: true, carbAbsorptionModel: .piecewiseLinear, - recommendationInsulinModel: ExponentialInsulinModelPreset.rapidActingAdult, + recommendationInsulinModel: LoopAlgorithm.ExponentialInsulinModelPreset.rapidActingAdult, recommendationType: .manualBolus, automaticBolusApplicationFactor: 0.4 ) @@ -971,14 +971,7 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { return (historicGlucose: loopStateInput.glucoseHistory, predictedGlucose: prediction) } - var algorithmOutput: AlgorithmOutput = AlgorithmOutput( - recommendationResult: .success(.init()), - predictedGlucose: [], - effects: LoopAlgorithmEffects.emptyMock, - dosesRelativeToBasal: [], - activeInsulin: nil, - activeCarbs: nil - ) + var recommendationResult: Result = .success(nil) var manualGlucoseSampleForBolusRecommendation: NewGlucoseSample? var carbEntryChangeForBolusRecommendation: CarbEntryChange? @@ -992,9 +985,9 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { manualGlucoseSampleForBolusRecommendation = manualGlucoseSample carbEntryChangeForBolusRecommendation = carbEntryChange - switch algorithmOutput.recommendationResult { + switch recommendationResult { case .success(let recommendation): - return recommendation.manual + return recommendation case .failure(let error): throw error } @@ -1047,20 +1040,6 @@ extension ManualBolusRecommendationWithDate: @retroactive Equatable { } } -extension LoopAlgorithmEffects { - public static var emptyMock: LoopAlgorithmEffects { - return LoopAlgorithmEffects( - insulin: [], - carbs: [], - carbStatus: [], - retrospectiveCorrection: [], - momentum: [], - insulinCounteraction: [], - retrospectiveGlucoseDiscrepancies: [] - ) - } -} - extension CarbEntryUpdate { static func mock(_ grams: Double, at date: Date) -> CarbEntryUpdate { CarbEntryUpdate( diff --git a/LoopTests/ViewModels/CGMStatusHUDViewModelTests.swift b/LoopTests/ViewModels/CGMStatusHUDViewModelTests.swift index 8f9bd9d3ac..bd78d1ac9f 100644 --- a/LoopTests/ViewModels/CGMStatusHUDViewModelTests.swift +++ b/LoopTests/ViewModels/CGMStatusHUDViewModelTests.swift @@ -7,7 +7,6 @@ // import XCTest -import LoopAlgorithm import LoopKit @testable import LoopUI diff --git a/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift b/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift index 4eaa0fbb3b..15a1cc4414 100644 --- a/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift +++ b/LoopTests/ViewModels/ManualEntryDoseViewModelTests.swift @@ -9,7 +9,6 @@ import LoopCore import LoopKit import XCTest -import LoopAlgorithm @testable import Loop @@ -87,7 +86,7 @@ fileprivate class MockManualEntryDoseViewModelDelegate: ManualDoseViewModelDeleg } func insulinActivityDuration(for type: InsulinType?) -> TimeInterval { - return InsulinMath.defaultInsulinActivityDuration + return ExponentialInsulinModelPreset.rapidActingAdult.effectDuration } var algorithmDisplayState = AlgorithmDisplayState() diff --git a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift index c29861c24a..65bf01a50d 100644 --- a/LoopTests/ViewModels/SimpleBolusViewModelTests.swift +++ b/LoopTests/ViewModels/SimpleBolusViewModelTests.swift @@ -11,7 +11,6 @@ import HealthKit import LoopKit import LoopKitUI import LoopCore -import LoopAlgorithm @testable import Loop diff --git a/LoopUI/ViewModel/CGMStatusHUDViewModel.swift b/LoopUI/ViewModel/CGMStatusHUDViewModel.swift index 2d6f04bec7..c71781ee9e 100644 --- a/LoopUI/ViewModel/CGMStatusHUDViewModel.swift +++ b/LoopUI/ViewModel/CGMStatusHUDViewModel.swift @@ -6,7 +6,6 @@ // Copyright © 2020 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import LoopKit public class CGMStatusHUDViewModel { diff --git a/LoopUI/Views/CGMStatusHUDView.swift b/LoopUI/Views/CGMStatusHUDView.swift index 47af3dd61e..5692149f1d 100644 --- a/LoopUI/Views/CGMStatusHUDView.swift +++ b/LoopUI/Views/CGMStatusHUDView.swift @@ -7,7 +7,6 @@ // import UIKit -import LoopAlgorithm import LoopKit import LoopKitUI diff --git a/LoopUI/Views/GlucoseHUDView.swift b/LoopUI/Views/GlucoseHUDView.swift index e54bdf2db1..73fd6620fe 100644 --- a/LoopUI/Views/GlucoseHUDView.swift +++ b/LoopUI/Views/GlucoseHUDView.swift @@ -7,7 +7,6 @@ // import UIKit -import LoopAlgorithm import LoopKit import LoopKitUI diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index f0a5c19744..7ce17628a5 100644 --- a/WatchApp Extension/ComplicationController.swift +++ b/WatchApp Extension/ComplicationController.swift @@ -9,8 +9,8 @@ import ClockKit import WatchKit import LoopKit +import LoopCore import os.log -import LoopAlgorithm final class ComplicationController: NSObject, CLKComplicationDataSource { @@ -101,7 +101,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { let template = CLKComplicationTemplate.templateForFamily(complication.family, from: context, at: timelineDate, - recencyInterval: LoopAlgorithm.inputDataRecencyInterval, + recencyInterval: LoopCoreConstants.inputDataRecencyInterval, chartGenerator: self.makeChart) { switch complication.family { @@ -132,7 +132,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { var futureChangeDates: [Date] = [ // Stale glucose date: just a second after glucose expires - glucoseDate + LoopAlgorithm.inputDataRecencyInterval + 1, + glucoseDate + LoopCoreConstants.inputDataRecencyInterval + 1, ] if let loopLastRunDate = context.loopLastRunDate { @@ -148,7 +148,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { if let template = CLKComplicationTemplate.templateForFamily(complication.family, from: context, at: futureChangeDate, - recencyInterval: LoopAlgorithm.inputDataRecencyInterval, + recencyInterval: LoopCoreConstants.inputDataRecencyInterval, chartGenerator: self.makeChart) { template.tintColor = UIColor.tintColor diff --git a/WatchApp Extension/ExtensionDelegate.swift b/WatchApp Extension/ExtensionDelegate.swift index ea1f3ac607..5d7a1b361f 100644 --- a/WatchApp Extension/ExtensionDelegate.swift +++ b/WatchApp Extension/ExtensionDelegate.swift @@ -9,7 +9,6 @@ import WatchConnectivity import WatchKit import HealthKit -import LoopAlgorithm import Intents import os import os.log diff --git a/WatchApp Extension/Extensions/CLKComplicationTemplate.swift b/WatchApp Extension/Extensions/CLKComplicationTemplate.swift index d2b9933a1a..c6984f4f7c 100644 --- a/WatchApp Extension/Extensions/CLKComplicationTemplate.swift +++ b/WatchApp Extension/Extensions/CLKComplicationTemplate.swift @@ -10,7 +10,6 @@ import ClockKit import LoopKit import Foundation import LoopCore -import LoopAlgorithm extension CLKComplicationTemplate { diff --git a/WatchApp Extension/Extensions/EnvironmentValues+GlucoseDisplayUnit.swift b/WatchApp Extension/Extensions/EnvironmentValues+GlucoseDisplayUnit.swift index f7a035aab1..6762b11b54 100644 --- a/WatchApp Extension/Extensions/EnvironmentValues+GlucoseDisplayUnit.swift +++ b/WatchApp Extension/Extensions/EnvironmentValues+GlucoseDisplayUnit.swift @@ -7,7 +7,7 @@ // import SwiftUI -import LoopAlgorithm +import LoopKit @MainActor private struct GlucoseDisplayUnitKey: @preconcurrency EnvironmentKey { diff --git a/WatchApp Extension/Extensions/GlucoseCondition.swift b/WatchApp Extension/Extensions/GlucoseCondition.swift index 5dae0e1a7a..85fdf7ee60 100644 --- a/WatchApp Extension/Extensions/GlucoseCondition.swift +++ b/WatchApp Extension/Extensions/GlucoseCondition.swift @@ -7,7 +7,7 @@ // import Foundation -import LoopAlgorithm +import LoopKit extension GlucoseCondition { var localizedDescription: String { diff --git a/WatchApp Extension/Extensions/WatchContext+WatchApp.swift b/WatchApp Extension/Extensions/WatchContext+WatchApp.swift index 0f3b85fb7d..4a710d3c07 100644 --- a/WatchApp Extension/Extensions/WatchContext+WatchApp.swift +++ b/WatchApp Extension/Extensions/WatchContext+WatchApp.swift @@ -7,7 +7,6 @@ // import Foundation -import LoopAlgorithm import LoopKit import LoopCore diff --git a/WatchApp Extension/Managers/ComplicationChartManager.swift b/WatchApp Extension/Managers/ComplicationChartManager.swift index 4be94984ff..8be5fbb5b1 100644 --- a/WatchApp Extension/Managers/ComplicationChartManager.swift +++ b/WatchApp Extension/Managers/ComplicationChartManager.swift @@ -10,7 +10,6 @@ import Foundation import UIKit import WatchKit import LoopKit -import LoopAlgorithm private let textInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift index ab548b96c0..bd1e022d96 100644 --- a/WatchApp Extension/Managers/LoopDataManager.swift +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -12,7 +12,6 @@ import LoopKit import LoopCore import WatchConnectivity import os.log -import LoopAlgorithm import UserNotifications import WatchKit @@ -224,7 +223,7 @@ extension LoopDataManager { func requestCarbBackfill() { dispatchPrecondition(condition: .onQueue(.main)) - let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -CarbMath.maximumAbsorptionTimeInterval)) + let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -LoopCoreConstants.maximumCarbAbsorptionTimeInterval)) let userInfo = CarbBackfillRequestUserInfo(startDate: start) WCSession.default.sendCarbBackfillRequestMessage(userInfo) { (result) in switch result { diff --git a/WatchApp Extension/Models/GlucoseChartData.swift b/WatchApp Extension/Models/GlucoseChartData.swift index 27903af5b6..f0d5d389b2 100644 --- a/WatchApp Extension/Models/GlucoseChartData.swift +++ b/WatchApp Extension/Models/GlucoseChartData.swift @@ -8,7 +8,6 @@ import Foundation import LoopKit -import LoopAlgorithm struct GlucoseChartData { diff --git a/WatchApp Extension/Models/GlucoseChartScaler.swift b/WatchApp Extension/Models/GlucoseChartScaler.swift index 979eeb5e74..6e0c54eadd 100644 --- a/WatchApp Extension/Models/GlucoseChartScaler.swift +++ b/WatchApp Extension/Models/GlucoseChartScaler.swift @@ -10,7 +10,6 @@ import Foundation import CoreGraphics import LoopKit import WatchKit -import LoopAlgorithm enum CoordinateSystem { diff --git a/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift b/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift index ab5ea5800b..410e62cbcd 100644 --- a/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift +++ b/WatchApp Extension/Scenes/GlucoseChartValueHashable.swift @@ -6,7 +6,6 @@ // import LoopKit -import LoopAlgorithm protocol GlucoseChartValueHashable { diff --git a/WatchApp Extension/View Models/CarbAndBolusFlowViewModel.swift b/WatchApp Extension/View Models/CarbAndBolusFlowViewModel.swift index e5d5cb2742..5ed0dd9675 100644 --- a/WatchApp Extension/View Models/CarbAndBolusFlowViewModel.swift +++ b/WatchApp Extension/View Models/CarbAndBolusFlowViewModel.swift @@ -8,7 +8,6 @@ import Foundation import Combine -import LoopAlgorithm import WatchKit import WatchConnectivity import LoopKit diff --git a/WatchApp Extension/Views/CarbList.swift b/WatchApp Extension/Views/CarbList.swift index 3377ae6d40..aa8ccd7aa6 100644 --- a/WatchApp Extension/Views/CarbList.swift +++ b/WatchApp Extension/Views/CarbList.swift @@ -8,7 +8,7 @@ import SwiftUI import LoopKit -import LoopAlgorithm +import LoopCore struct CarbList: View { @Environment(LoopDataManager.self) var loopManager @@ -29,7 +29,7 @@ struct CarbList: View { @State var entries: [StoredCarbEntry] = [] private func reloadCarbEntries() async { - let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -CarbMath.maximumAbsorptionTimeInterval)) + let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -LoopCoreConstants.maximumCarbAbsorptionTimeInterval)) entries = (try? await loopManager.carbStore.getCarbEntries(start: start)) ?? [] } diff --git a/WatchApp Extension/Views/ChartPageView.swift b/WatchApp Extension/Views/ChartPageView.swift index 5833781ef5..0a04879cb7 100644 --- a/WatchApp Extension/Views/ChartPageView.swift +++ b/WatchApp Extension/Views/ChartPageView.swift @@ -9,7 +9,6 @@ import SwiftUI import LoopKit import LoopCore -import LoopAlgorithm import SpriteKit struct ChartPageView: View { diff --git a/WatchApp Extension/Views/PresetWatchCard.swift b/WatchApp Extension/Views/PresetWatchCard.swift index 90367e4330..0c95e9f2b9 100644 --- a/WatchApp Extension/Views/PresetWatchCard.swift +++ b/WatchApp Extension/Views/PresetWatchCard.swift @@ -6,7 +6,6 @@ // Copyright © 2025 LoopKit Authors. All rights reserved. // -import LoopAlgorithm import SwiftUI import LoopKit import LoopCore