1+ // OctreotideAlgorithm.swift
2+ // Reverse-Logic control module for Octreotide infusion
3+ // Designed as a drop-in module for Loop (developer preview, simulation only).
4+ // IMPORTANT: This is a clinical experiment template. Do NOT run closed-loop on a patient without
5+ // rigorous simulation, clinical oversight, and regulatory/safety approval.
6+
7+ import Foundation
8+
9+ /// Represents a recommended change in medication delivery
10+ struct DeliveryRecommendation {
11+ let basalRate : Double // U/hr
12+ let bolus : Double // U
13+ let messages : [ String ] // User/clinician messages
14+ let trend : Double // mg/dL/min
15+ let safetyFlags : Set < SafetyFlag >
16+
17+ enum SafetyFlag : String {
18+ case insufficientHistory = " Insufficient glucose history "
19+ case rapidChange = " Rapid glucose change detected "
20+ case bolusCap = " Daily bolus cap reached "
21+ case bolusDelay = " Minimum time between boluses not met "
22+ case criticalLow = " Critical low glucose "
23+ case pdParamsInvalid = " PD parameters invalid or missing "
24+ }
25+ }
26+
27+ /// Algorithm for Octreotide (somatostatin analog) delivery based on CGM trend
28+ struct OctreotideAlgorithm {
29+ // MARK: - Tunable parameters (clinician-adjustable)
30+
31+ /// Glucose target range (mg/dL)
32+ var targetLow : Double = 70.0
33+ var targetHigh : Double = 100.0
34+ var criticalLow : Double = 65.0
35+
36+ /// Basal delivery limits (U/hr)
37+ var minBasal : Double = 0.25
38+ var maxBasal : Double = 5.0
39+
40+ /// Basal multipliers for different glucose ranges
41+ var basalMultLow : Double = 1.5 // Multiply scheduled basal by this when low
42+ var basalMultHigh : Double = 0.5 // Multiply by this when high/rising
43+
44+ /// Trend thresholds (mg/dL per min) - negative is falling
45+ var trendFallFast : Double = - 1.0
46+ var trendRiseFast : Double = 1.0
47+
48+ /// Bolus configuration
49+ var bolusUnit : Double = 1.0 // Standard bolus size (U)
50+ var bolusRepeatDelay : TimeInterval = 15 * 60 // Min seconds between boluses
51+ var maxDailyBolus : Double = 30.0 // Maximum total bolus units per day
52+
53+ /// Pharmacodynamic parameters (required for safety)
54+ var pdOnsetMinutes : Double = 30.0 // Time to start of action
55+ var pdPeakMinutes : Double = 90.0 // Time to peak action
56+ var pdDurationMinutes : Double = 240.0 // Total duration of action
57+
58+ // MARK: - Utility
59+
60+ private func clamp( _ value: Double , _ minVal: Double , _ maxVal: Double ) -> Double {
61+ return max ( minVal, min ( value, maxVal) )
62+ }
63+
64+ /// Compute trend in mg/dL per minute using 3 samples
65+ /// - Parameter glucoseHistory: Ordered oldest to newest, ~5min spacing
66+ private func computeTrendRate( glucoseHistory: [ Double ] ) -> Double {
67+ guard glucoseHistory. count >= 3 else { return 0.0 }
68+ let last = glucoseHistory [ glucoseHistory. count - 1 ]
69+ let thirdLast = glucoseHistory [ glucoseHistory. count - 3 ]
70+ // Approximate over ~10 minutes
71+ return ( last - thirdLast) / 10.0
72+ }
73+
74+ /// Validate PD parameters are present and reasonable
75+ private func validatePDParams( ) -> Bool {
76+ guard pdOnsetMinutes > 0 ,
77+ pdPeakMinutes > pdOnsetMinutes,
78+ pdDurationMinutes > pdPeakMinutes else {
79+ return false
80+ }
81+ return true
82+ }
83+
84+ /// Main recommendation function
85+ /// - Parameters:
86+ /// - glucoseNow: Current glucose in mg/dL
87+ /// - glucoseHistory: Recent glucose samples (mg/dL), oldest->newest, ~5min spacing
88+ /// - scheduledBasal: Current scheduled basal rate (U/hr)
89+ /// - lastBolusDate: Time of last bolus (if any)
90+ /// - dailyBolusTotal: Total bolus units given today
91+ /// - Returns: Recommended basal rate, bolus amount, and status messages
92+ func computeRecommendation(
93+ glucoseNow: Double ,
94+ glucoseHistory: [ Double ] ,
95+ scheduledBasal: Double ,
96+ lastBolusDate: Date ? ,
97+ dailyBolusTotal: Double
98+ ) -> DeliveryRecommendation {
99+ // Start with current basal as baseline
100+ var recommendedBasal = scheduledBasal
101+ var recommendedBolus : Double = 0.0
102+ var messages : [ String ] = [ ]
103+ var safetyFlags : Set < DeliveryRecommendation . SafetyFlag > = [ ]
104+
105+ // Compute trend (mg/dL/min)
106+ let trend = computeTrendRate ( glucoseHistory: glucoseHistory)
107+
108+ // MARK: Safety Checks
109+
110+ // 1. Check glucose history
111+ guard glucoseHistory. count >= 3 else {
112+ safetyFlags. insert ( . insufficientHistory)
113+ return DeliveryRecommendation (
114+ basalRate: minBasal,
115+ bolus: 0 ,
116+ messages: [ " Insufficient glucose history for safe automation " ] ,
117+ trend: 0 ,
118+ safetyFlags: safetyFlags
119+ )
120+ }
121+
122+ // 2. Validate PD parameters
123+ guard validatePDParams ( ) else {
124+ safetyFlags. insert ( . pdParamsInvalid)
125+ return DeliveryRecommendation (
126+ basalRate: minBasal,
127+ bolus: 0 ,
128+ messages: [ " Invalid pharmacodynamic parameters - check configuration " ] ,
129+ trend: trend,
130+ safetyFlags: safetyFlags
131+ )
132+ }
133+
134+ // 3. Check for rapid changes
135+ if abs ( trend) > max ( abs ( trendFallFast) , abs ( trendRiseFast) ) {
136+ safetyFlags. insert ( . rapidChange)
137+ messages. append ( " Rapid glucose change detected " )
138+ }
139+
140+ // 4. Check critical low
141+ if glucoseNow <= criticalLow {
142+ safetyFlags. insert ( . criticalLow)
143+ // Max out basal but no bolus
144+ recommendedBasal = maxBasal
145+ messages. append ( " Critical low - maximizing basal delivery " )
146+ return DeliveryRecommendation (
147+ basalRate: recommendedBasal,
148+ bolus: 0 ,
149+ messages: messages,
150+ trend: trend,
151+ safetyFlags: safetyFlags
152+ )
153+ }
154+
155+ // MARK: Basal Adjustments
156+
157+ // Adjust basal rate based on current glucose and trend
158+ if glucoseNow < targetLow || trend < trendFallFast {
159+ // Low or falling fast - increase basal
160+ recommendedBasal = scheduledBasal * basalMultLow
161+ messages. append ( " Increasing basal due to low/falling glucose " )
162+ } else if glucoseNow > targetHigh || trend > trendRiseFast {
163+ // High or rising fast - decrease basal
164+ recommendedBasal = scheduledBasal * basalMultHigh
165+ messages. append ( " Decreasing basal due to high/rising glucose " )
166+ }
167+
168+ // Clamp basal rate to limits
169+ recommendedBasal = clamp ( recommendedBasal, minBasal, maxBasal)
170+
171+ // MARK: Bolus Logic
172+
173+ // Helper to check bolus eligibility
174+ func canGiveBolus( ) -> Bool {
175+ if dailyBolusTotal + bolusUnit > maxDailyBolus {
176+ safetyFlags. insert ( . bolusCap)
177+ return false
178+ }
179+ if let last = lastBolusDate {
180+ if Date ( ) . timeIntervalSince ( last) < bolusRepeatDelay {
181+ safetyFlags. insert ( . bolusDelay)
182+ return false
183+ }
184+ }
185+ return true
186+ }
187+
188+ // Consider bolus for significant lows or rapid drops
189+ if ( glucoseNow < targetLow && trend < 0 ) || trend < trendFallFast {
190+ if canGiveBolus ( ) {
191+ recommendedBolus = bolusUnit
192+ messages. append ( " Recommending bolus for low/falling glucose " )
193+ } else {
194+ messages. append ( " Bolus indicated but safety cap prevents delivery " )
195+ }
196+ }
197+
198+ return DeliveryRecommendation (
199+ basalRate: recommendedBasal,
200+ bolus: recommendedBolus,
201+ messages: messages,
202+ trend: trend,
203+ safetyFlags: safetyFlags
204+ )
205+ }
206+ }
0 commit comments