Skip to content

Commit 89dd58a

Browse files
authored
LOOP-5502 Allow setting of max active insulin multiplier (#22)
* Allow setting of max active insulin multiplier * Fix formatting for older xcode * Fix formatting for older xcode
1 parent 29c7b52 commit 89dd58a

5 files changed

Lines changed: 67 additions & 35 deletions

File tree

Sources/LoopAlgorithm/AlgorithmInput.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ public protocol AlgorithmInput {
2424
var target: GlucoseRangeTimeline { get }
2525
var suspendThreshold: LoopQuantity? { get }
2626
var maxBolus: Double { get }
27+
var maxActiveInsulinMultiplier: Double? { get } // Defaults to 2 (2x maxBolus)
2728
var maxBasalRate: Double { get }
2829
var useIntegralRetrospectiveCorrection: Bool { get }
2930
var includePositiveVelocityAndRC: Bool { get }
3031
var useMidAbsorptionISF: Bool { get }
3132
var carbAbsorptionModel: CarbAbsorptionModel { get }
3233
var recommendationInsulinModel: InsulinModel { get }
3334
var recommendationType: DoseRecommendationType { get }
34-
var automaticBolusApplicationFactor: Double? { get }
35+
var automaticBolusApplicationFactor: Double? { get } // Defaults to 0.4
3536
}
3637

3738

Sources/LoopAlgorithm/AlgorithmInputFixture.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public struct AlgorithmInputFixture: AlgorithmInput {
2525
public var target: GlucoseRangeTimeline
2626
public var suspendThreshold: LoopQuantity?
2727
public var maxBolus: Double
28+
public var maxActiveInsulinMultiplier: Double?
2829
public var maxBasalRate: Double
2930
public var useIntegralRetrospectiveCorrection: Bool
3031
public var includePositiveVelocityAndRC: Bool
@@ -62,6 +63,7 @@ public struct AlgorithmInputFixture: AlgorithmInput {
6263
target: GlucoseRangeTimeline,
6364
suspendThreshold: LoopQuantity?,
6465
maxBolus: Double,
66+
maxActiveInsulinMultiplier: Double? = nil,
6567
maxBasalRate: Double,
6668
useIntegralRetrospectiveCorrection: Bool = false,
6769
useMidAbsorptionISF: Bool = false,
@@ -81,6 +83,7 @@ public struct AlgorithmInputFixture: AlgorithmInput {
8183
self.target = target
8284
self.suspendThreshold = suspendThreshold
8385
self.maxBolus = maxBolus
86+
self.maxActiveInsulinMultiplier = maxActiveInsulinMultiplier
8487
self.maxBasalRate = maxBasalRate
8588
self.useIntegralRetrospectiveCorrection = useIntegralRetrospectiveCorrection
8689
self.includePositiveVelocityAndRC = includePositiveVelocityAndRC
@@ -116,6 +119,7 @@ extension AlgorithmInputFixture: Codable {
116119
self.suspendThreshold = LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: suspendThresholdMgdl)
117120
}
118121
self.maxBolus = try container.decode(Double.self, forKey: .maxBolus)
122+
self.maxActiveInsulinMultiplier = try container.decodeIfPresent(Double.self, forKey: .maxActiveInsulinMultiplier)
119123
self.maxBasalRate = try container.decode(Double.self, forKey: .maxBasalRate)
120124
self.useIntegralRetrospectiveCorrection = try container.decodeIfPresent(Bool.self, forKey: .useIntegralRetrospectiveCorrection) ?? false
121125
self.includePositiveVelocityAndRC = try container.decodeIfPresent(Bool.self, forKey: .includePositiveVelocityAndRC) ?? true
@@ -163,6 +167,7 @@ extension AlgorithmInputFixture: Codable {
163167
try container.encode(targetMgdl, forKey: .target)
164168
try container.encode(suspendThreshold?.doubleValue(for: .milligramsPerDeciliter), forKey: .suspendThreshold)
165169
try container.encode(maxBolus, forKey: .maxBolus)
170+
try container.encode(maxActiveInsulinMultiplier, forKey: .maxActiveInsulinMultiplier)
166171
try container.encode(maxBasalRate, forKey: .maxBasalRate)
167172
if useIntegralRetrospectiveCorrection {
168173
try container.encode(useIntegralRetrospectiveCorrection, forKey: .useIntegralRetrospectiveCorrection)
@@ -187,6 +192,7 @@ extension AlgorithmInputFixture: Codable {
187192
case target
188193
case suspendThreshold
189194
case maxBolus
195+
case maxActiveInsulinMultiplier
190196
case maxBasalRate
191197
case useIntegralRetrospectiveCorrection
192198
case includePositiveVelocityAndRC

Sources/LoopAlgorithm/LoopAlgorithm.swift

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ public struct LoopAlgorithm {
374374
neutralBasalRate: Double,
375375
activeInsulin: Double,
376376
maxBolus: Double,
377-
maxBasalRate: Double
377+
maxBasalRate: Double,
378+
maxActiveInsulin: Double
378379
) -> TempBasalRecommendation {
379380

380381
var maxBasalRate = maxBasalRate
@@ -386,12 +387,11 @@ public struct LoopAlgorithm {
386387
maxBasalRate = neutralBasalRate
387388
}
388389

389-
// Enforce max IOB, calculated from the user entered maxBolus
390-
let automaticDosingIOBLimit = maxBolus * 2.0
391-
let iobHeadroom = automaticDosingIOBLimit - activeInsulin
390+
// Enforce max active insulin
391+
let activeInsulinHeadroom = maxActiveInsulin - activeInsulin
392392

393-
let maxThirtyMinuteRateToKeepIOBBelowLimit = iobHeadroom * (TimeInterval.hours(1) / tempBasalDuration) + neutralBasalRate // 30 minutes of a U/hr rate
394-
maxBasalRate = Swift.min(maxThirtyMinuteRateToKeepIOBBelowLimit, maxBasalRate)
393+
let maxThirtyMinuteRateToKeepActiveInsulinBelowLimit = activeInsulinHeadroom * (TimeInterval.hours(1) / tempBasalDuration) + neutralBasalRate // 30 minutes of a U/hr rate
394+
maxBasalRate = Swift.min(maxThirtyMinuteRateToKeepActiveInsulinBelowLimit, maxBasalRate)
395395

396396
return correction.asTempBasal(
397397
neutralBasalRate: neutralBasalRate,
@@ -407,11 +407,12 @@ public struct LoopAlgorithm {
407407
neutralBasalRate: Double,
408408
activeInsulin: Double,
409409
maxBolus: Double,
410-
maxBasalRate: Double
410+
maxBasalRate: Double,
411+
maxActiveInsulin: Double
411412
) -> AutomaticDoseRecommendation {
412413

413414

414-
let deliveryHeadroom = max(0, maxBolus * 2.0 - activeInsulin)
415+
let deliveryHeadroom = max(0, maxActiveInsulin - activeInsulin)
415416

416417
var deliveryMax = min(maxBolus * applicationFactor, deliveryHeadroom)
417418

@@ -555,6 +556,8 @@ public struct LoopAlgorithm {
555556
sensitivity: sensitivityForDosing,
556557
insulinModel: input.recommendationInsulinModel)
557558

559+
let maxActiveInsulin = input.maxBolus * (input.maxActiveInsulinMultiplier ?? 2)
560+
558561
switch input.recommendationType {
559562
case .manualBolus:
560563
let recommendation = recommendManualBolus(
@@ -570,15 +573,17 @@ public struct LoopAlgorithm {
570573
neutralBasalRate: scheduledBasalRate,
571574
activeInsulin: prediction.activeInsulin!,
572575
maxBolus: input.maxBolus,
573-
maxBasalRate: input.maxBasalRate)
576+
maxBasalRate: input.maxBasalRate,
577+
maxActiveInsulin: maxActiveInsulin)
574578
result = .success(.init(automatic: recommendation))
575579
case .tempBasal:
576580
let recommendation = recommendTempBasal(
577581
for: correction,
578582
neutralBasalRate: scheduledBasalRate,
579583
activeInsulin: prediction.activeInsulin!,
580584
maxBolus: input.maxBolus,
581-
maxBasalRate: input.maxBasalRate)
585+
maxBasalRate: input.maxBasalRate,
586+
maxActiveInsulin: maxActiveInsulin)
582587
result = .success(.init(automatic: AutomaticDoseRecommendation(basalAdjustment: recommendation, direction: .from(correction: correction))))
583588
}
584589
} catch {

Tests/LoopAlgorithmTests/CorrectionDosingTests.swift

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class CorrectionDosingTests: XCTestCase {
5757
neutralBasalRate: basalRate,
5858
activeInsulin: 0,
5959
maxBolus: 6,
60-
maxBasalRate: maxBasalRate
60+
maxBasalRate: maxBasalRate,
61+
maxActiveInsulin: 12
6162
)
6263

6364
XCTAssertEqual(recommendation.unitsPerHour, basalRate)
@@ -69,7 +70,8 @@ class CorrectionDosingTests: XCTestCase {
6970
neutralBasalRate: basalRate,
7071
activeInsulin: 0,
7172
maxBolus: 6,
72-
maxBasalRate: maxBasalRate
73+
maxBasalRate: maxBasalRate,
74+
maxActiveInsulin: 12
7375
)
7476

7577
XCTAssertEqual(automaticDose.bolusUnits, 0)
@@ -101,7 +103,8 @@ class CorrectionDosingTests: XCTestCase {
101103
neutralBasalRate: basalRate,
102104
activeInsulin: 0,
103105
maxBolus: 6,
104-
maxBasalRate: maxBasalRate
106+
maxBasalRate: maxBasalRate,
107+
maxActiveInsulin: 12
105108
)
106109

107110
XCTAssertEqual(recommendation.unitsPerHour, basalRate)
@@ -113,7 +116,8 @@ class CorrectionDosingTests: XCTestCase {
113116
neutralBasalRate: basalRate,
114117
activeInsulin: 0,
115118
maxBolus: 6,
116-
maxBasalRate: maxBasalRate
119+
maxBasalRate: maxBasalRate,
120+
maxActiveInsulin: 12
117121
)
118122

119123
XCTAssertEqual(automaticDose.bolusUnits, 0)
@@ -145,7 +149,8 @@ class CorrectionDosingTests: XCTestCase {
145149
neutralBasalRate: basalRate,
146150
activeInsulin: 0,
147151
maxBolus: 6,
148-
maxBasalRate: maxBasalRate
152+
maxBasalRate: maxBasalRate,
153+
maxActiveInsulin: 12
149154
)
150155

151156
XCTAssertEqual(recommendation.unitsPerHour, 1)
@@ -157,7 +162,8 @@ class CorrectionDosingTests: XCTestCase {
157162
neutralBasalRate: basalRate,
158163
activeInsulin: 0,
159164
maxBolus: 6,
160-
maxBasalRate: maxBasalRate
165+
maxBasalRate: maxBasalRate,
166+
maxActiveInsulin: 12
161167
)
162168

163169
XCTAssertEqual(automaticDose.bolusUnits, 0)
@@ -190,7 +196,8 @@ class CorrectionDosingTests: XCTestCase {
190196
neutralBasalRate: basalRate,
191197
activeInsulin: 0,
192198
maxBolus: 6,
193-
maxBasalRate: maxBasalRate
199+
maxBasalRate: maxBasalRate,
200+
maxActiveInsulin: 12
194201
)
195202

196203
XCTAssertEqual(recommendation.unitsPerHour, 1)
@@ -202,7 +209,8 @@ class CorrectionDosingTests: XCTestCase {
202209
neutralBasalRate: basalRate,
203210
activeInsulin: 0,
204211
maxBolus: 6,
205-
maxBasalRate: maxBasalRate
212+
maxBasalRate: maxBasalRate,
213+
maxActiveInsulin: 12
206214
)
207215

208216
XCTAssertEqual(automaticDose.bolusUnits, 0)
@@ -236,7 +244,8 @@ class CorrectionDosingTests: XCTestCase {
236244
neutralBasalRate: basalRate,
237245
activeInsulin: 0,
238246
maxBolus: 6,
239-
maxBasalRate: maxBasalRate
247+
maxBasalRate: maxBasalRate,
248+
maxActiveInsulin: 12
240249
)
241250

242251
XCTAssertEqual(recommendation.unitsPerHour, 0)
@@ -248,7 +257,8 @@ class CorrectionDosingTests: XCTestCase {
248257
neutralBasalRate: basalRate,
249258
activeInsulin: 0,
250259
maxBolus: 6,
251-
maxBasalRate: maxBasalRate
260+
maxBasalRate: maxBasalRate,
261+
maxActiveInsulin: 12
252262
)
253263

254264
XCTAssertEqual(automaticDose.bolusUnits, 0)
@@ -285,7 +295,8 @@ class CorrectionDosingTests: XCTestCase {
285295
neutralBasalRate: basalRate,
286296
activeInsulin: 0,
287297
maxBolus: 6,
288-
maxBasalRate: maxBasalRate
298+
maxBasalRate: maxBasalRate,
299+
maxActiveInsulin: 12
289300
)
290301

291302
XCTAssertEqual(recommendation.unitsPerHour, 1.0)
@@ -297,7 +308,8 @@ class CorrectionDosingTests: XCTestCase {
297308
neutralBasalRate: basalRate,
298309
activeInsulin: 0,
299310
maxBolus: 6,
300-
maxBasalRate: maxBasalRate
311+
maxBasalRate: maxBasalRate,
312+
maxActiveInsulin: 12
301313
)
302314

303315
XCTAssertEqual(automaticDose.bolusUnits, 0)
@@ -334,7 +346,8 @@ class CorrectionDosingTests: XCTestCase {
334346
neutralBasalRate: basalRate,
335347
activeInsulin: 0,
336348
maxBolus: 6,
337-
maxBasalRate: maxBasalRate
349+
maxBasalRate: maxBasalRate,
350+
maxActiveInsulin: 12
338351
)
339352

340353
XCTAssertEqual(recommendation.unitsPerHour, 3.0)
@@ -346,7 +359,8 @@ class CorrectionDosingTests: XCTestCase {
346359
neutralBasalRate: basalRate,
347360
activeInsulin: 0,
348361
maxBolus: 6,
349-
maxBasalRate: maxBasalRate
362+
maxBasalRate: maxBasalRate,
363+
maxActiveInsulin: 12
350364
)
351365

352366
XCTAssertEqual(automaticDose.bolusUnits!, 0.65, accuracy: 0.05)
@@ -380,7 +394,8 @@ class CorrectionDosingTests: XCTestCase {
380394
neutralBasalRate: basalRate,
381395
activeInsulin: 0,
382396
maxBolus: 6,
383-
maxBasalRate: maxBasalRate
397+
maxBasalRate: maxBasalRate,
398+
maxActiveInsulin: 12
384399
)
385400

386401
XCTAssertEqual(recommendation.unitsPerHour, 1.63, accuracy: 0.05)
@@ -392,7 +407,8 @@ class CorrectionDosingTests: XCTestCase {
392407
neutralBasalRate: basalRate,
393408
activeInsulin: 0,
394409
maxBolus: 6,
395-
maxBasalRate: maxBasalRate
410+
maxBasalRate: maxBasalRate,
411+
maxActiveInsulin: 12
396412
)
397413

398414
XCTAssertEqual(automaticDose.bolusUnits!, 0.10, accuracy: 0.05)
@@ -425,7 +441,8 @@ class CorrectionDosingTests: XCTestCase {
425441
neutralBasalRate: basalRate,
426442
activeInsulin: 0,
427443
maxBolus: 6,
428-
maxBasalRate: maxBasalRate
444+
maxBasalRate: maxBasalRate,
445+
maxActiveInsulin: 12
429446
)
430447

431448
XCTAssertEqual(recommendation.unitsPerHour, 1.63, accuracy: 0.05)
@@ -437,7 +454,8 @@ class CorrectionDosingTests: XCTestCase {
437454
neutralBasalRate: basalRate,
438455
activeInsulin: 0,
439456
maxBolus: 6,
440-
maxBasalRate: maxBasalRate
457+
maxBasalRate: maxBasalRate,
458+
maxActiveInsulin: 12
441459
)
442460

443461
XCTAssertEqual(automaticDose.bolusUnits!, 0.10, accuracy: 0.05)
@@ -470,7 +488,8 @@ class CorrectionDosingTests: XCTestCase {
470488
neutralBasalRate: basalRate,
471489
activeInsulin: 0,
472490
maxBolus: 6,
473-
maxBasalRate: maxBasalRate
491+
maxBasalRate: maxBasalRate,
492+
maxActiveInsulin: 12
474493
)
475494

476495
XCTAssertEqual(recommendation.unitsPerHour, 3.0, accuracy: 0.05)
@@ -482,7 +501,8 @@ class CorrectionDosingTests: XCTestCase {
482501
neutralBasalRate: basalRate,
483502
activeInsulin: 0,
484503
maxBolus: 6,
485-
maxBasalRate: maxBasalRate
504+
maxBasalRate: maxBasalRate,
505+
maxActiveInsulin: 12
486506
)
487507

488508
XCTAssertEqual(automaticDose.bolusUnits!, 0.5, accuracy: 0.05)
@@ -515,7 +535,8 @@ class CorrectionDosingTests: XCTestCase {
515535
neutralBasalRate: basalRate,
516536
activeInsulin: 0,
517537
maxBolus: 6,
518-
maxBasalRate: maxBasalRate
538+
maxBasalRate: maxBasalRate,
539+
maxActiveInsulin: 12
519540
)
520541

521542
XCTAssertEqual(recommendation.unitsPerHour, 0, accuracy: 0.05)
@@ -527,7 +548,8 @@ class CorrectionDosingTests: XCTestCase {
527548
neutralBasalRate: basalRate,
528549
activeInsulin: 0,
529550
maxBolus: 6,
530-
maxBasalRate: maxBasalRate
551+
maxBasalRate: maxBasalRate,
552+
maxActiveInsulin: 12
531553
)
532554

533555
XCTAssertEqual(automaticDose.bolusUnits!, 0.0, accuracy: 0.05)

Tests/LoopAlgorithmTests/LoopAlgorithmTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,8 @@ final class LoopAlgorithmTests: XCTestCase {
203203

204204
// 150 mg/dL is the middle of the target range
205205
XCTAssertEqual(output2.predictedGlucose.last!.quantity.doubleValue(for: .milligramsPerDeciliter), 150, accuracy: 0.1)
206-
207206
}
208207

209-
210208
func testMidAborptionISFFlag() {
211209
let now = ISO8601DateFormatter().date(from: "2024-01-03T00:00:00+0000")!
212210
var input = AlgorithmInputFixture.mock(for: now)

0 commit comments

Comments
 (0)