@@ -12,6 +12,58 @@ import LoopAlgorithm
12
12
13
13
@testable import LoopKit
14
14
15
+ fileprivate struct SimpleInsulinDose : InsulinDose {
16
+ var deliveryType : InsulinDeliveryType
17
+ var startDate : Date
18
+ var endDate : Date
19
+ var volume : Double
20
+ var insulinModel : InsulinModel
21
+ }
22
+
23
+ fileprivate struct StoredDataAlgorithmInput : AlgorithmInput {
24
+ typealias CarbType = StoredCarbEntry
25
+
26
+ typealias GlucoseType = StoredGlucoseSample
27
+
28
+ typealias InsulinDoseType = SimpleInsulinDose
29
+
30
+ var glucoseHistory : [ StoredGlucoseSample ]
31
+
32
+ var doses : [ SimpleInsulinDose ]
33
+
34
+ var carbEntries : [ StoredCarbEntry ]
35
+
36
+ var predictionStart : Date
37
+
38
+ var basal : [ AbsoluteScheduleValue < Double > ]
39
+
40
+ var sensitivity : [ AbsoluteScheduleValue < LoopQuantity > ]
41
+
42
+ var carbRatio : [ AbsoluteScheduleValue < Double > ]
43
+
44
+ var target : GlucoseRangeTimeline
45
+
46
+ var suspendThreshold : LoopQuantity ?
47
+
48
+ var maxBolus : Double
49
+
50
+ var maxBasalRate : Double
51
+
52
+ var useIntegralRetrospectiveCorrection : Bool
53
+
54
+ var includePositiveVelocityAndRC : Bool
55
+
56
+ var carbAbsorptionModel : CarbAbsorptionModel
57
+
58
+ var recommendationInsulinModel : InsulinModel
59
+
60
+ var recommendationType : DoseRecommendationType
61
+
62
+ var automaticBolusApplicationFactor : Double ?
63
+
64
+ let useMidAbsorptionISF : Bool = true
65
+ }
66
+
15
67
extension TimeZone {
16
68
static var fixtureTimeZone : TimeZone {
17
69
return TimeZone ( secondsFromGMT: 25200 ) ! // -0700
@@ -366,7 +418,7 @@ class TemporaryScheduleOverrideTests: XCTestCase {
366
418
XCTAssertEqual ( expectedValues, values)
367
419
}
368
420
369
- func testTargetOverride ( ) {
421
+ func testTargetOverridePremeal ( ) {
370
422
let scheduledRange = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 100 ) ... LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 110 )
371
423
let overrideRange = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 80 ) ... LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 90 )
372
424
@@ -444,6 +496,189 @@ class TemporaryScheduleOverrideTests: XCTestCase {
444
496
values = applied. map { $0. value }
445
497
XCTAssertEqual ( [ scheduledRange] , values)
446
498
}
499
+
500
+ func testTargetOverrideWorkoutPrediction( ) {
501
+ let startOfDay = Calendar . current. startOfDay ( for: Date ( ) )
502
+ let now = startOfDay. addingTimeInterval ( . hours( 12 ) )
503
+ let scheduledRange = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 100 ) ... LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 110 )
504
+ let overrideRange = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 140 ) ... LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 160 )
505
+
506
+ let overrideDuration = TimeInterval ( hours: 2 )
507
+ var overrides : [ TemporaryScheduleOverride ] = [
508
+ . init(
509
+ context: . legacyWorkout,
510
+ settings: . init( targetRange: overrideRange) ,
511
+ startDate: now,
512
+ duration: . finite( overrideDuration) ,
513
+ enactTrigger: . local,
514
+ syncIdentifier: UUID ( )
515
+ )
516
+ ]
517
+
518
+ // sensitivity with overrides
519
+ let sensitivity1Value = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 45 )
520
+ let sensitivity2Value = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 55 )
521
+ let sensitivity1End = TimeInterval ( hours: 9 )
522
+ let sensitivity2End = TimeInterval ( hours: 24 )
523
+ let sensitivity : [ AbsoluteScheduleValue < LoopQuantity > ] = [
524
+ AbsoluteScheduleValue ( startDate: startOfDay, endDate: startOfDay. addingTimeInterval ( sensitivity1End) , value: sensitivity1Value) ,
525
+ AbsoluteScheduleValue ( startDate: startOfDay. addingTimeInterval ( . hours( 9 ) ) , endDate: startOfDay. addingTimeInterval ( sensitivity2End) , value: sensitivity2Value) ,
526
+ ]
527
+ let sensitivityWithOverrides = overrides. applySensitivity ( over: sensitivity)
528
+ XCTAssertEqual ( sensitivityWithOverrides. count, 4 )
529
+ XCTAssertEqual ( sensitivityWithOverrides [ 0 ] . startDate, startOfDay)
530
+ XCTAssertEqual ( sensitivityWithOverrides [ 0 ] . endDate, startOfDay. addingTimeInterval ( sensitivity1End) )
531
+ XCTAssertEqual ( sensitivityWithOverrides [ 0 ] . value, sensitivity1Value)
532
+ XCTAssertEqual ( sensitivityWithOverrides [ 1 ] . startDate, startOfDay. addingTimeInterval ( sensitivity1End) )
533
+ XCTAssertEqual ( sensitivityWithOverrides [ 1 ] . endDate, now)
534
+ XCTAssertEqual ( sensitivityWithOverrides [ 1 ] . value, sensitivity2Value)
535
+ XCTAssertEqual ( sensitivityWithOverrides [ 2 ] . startDate, now)
536
+ XCTAssertEqual ( sensitivityWithOverrides [ 2 ] . endDate, now. addingTimeInterval ( overrideDuration) )
537
+ XCTAssertEqual ( sensitivityWithOverrides [ 2 ] . value, sensitivity2Value)
538
+ XCTAssertEqual ( sensitivityWithOverrides [ 3 ] . startDate, now. addingTimeInterval ( overrideDuration) )
539
+ XCTAssertEqual ( sensitivityWithOverrides [ 3 ] . endDate, startOfDay. addingTimeInterval ( sensitivity2End) )
540
+ XCTAssertEqual ( sensitivityWithOverrides [ 3 ] . value, sensitivity2Value)
541
+
542
+ // Basal with overrides
543
+ let basal1Value = 1.0
544
+ let basal2Value = 0.85
545
+ let basal1End = TimeInterval ( hours: 17 )
546
+ let basal2End = TimeInterval ( hours: 24 )
547
+ let basal : [ AbsoluteScheduleValue < Double > ] = [
548
+ AbsoluteScheduleValue ( startDate: startOfDay, endDate: startOfDay. addingTimeInterval ( basal1End) , value: basal1Value) ,
549
+ AbsoluteScheduleValue ( startDate: startOfDay. addingTimeInterval ( basal1End) , endDate: startOfDay. addingTimeInterval ( basal2End) , value: basal2Value) ,
550
+ ]
551
+ let basalWithOverrides = overrides. applyBasal ( over: basal)
552
+ XCTAssertEqual ( basalWithOverrides. count, 4 )
553
+ XCTAssertEqual ( basalWithOverrides [ 0 ] . startDate, startOfDay)
554
+ XCTAssertEqual ( basalWithOverrides [ 0 ] . endDate, now)
555
+ XCTAssertEqual ( basalWithOverrides [ 0 ] . value, basal1Value)
556
+ XCTAssertEqual ( basalWithOverrides [ 1 ] . startDate, now)
557
+ XCTAssertEqual ( basalWithOverrides [ 1 ] . endDate, now. addingTimeInterval ( overrideDuration) )
558
+ XCTAssertEqual ( basalWithOverrides [ 1 ] . value, basal1Value)
559
+ XCTAssertEqual ( basalWithOverrides [ 2 ] . startDate, now. addingTimeInterval ( overrideDuration) )
560
+ XCTAssertEqual ( basalWithOverrides [ 2 ] . endDate, startOfDay. addingTimeInterval ( basal1End) )
561
+ XCTAssertEqual ( basalWithOverrides [ 2 ] . value, basal1Value)
562
+ XCTAssertEqual ( basalWithOverrides [ 3 ] . startDate, startOfDay. addingTimeInterval ( basal1End) )
563
+ XCTAssertEqual ( basalWithOverrides [ 3 ] . endDate, startOfDay. addingTimeInterval ( basal2End) )
564
+ XCTAssertEqual ( basalWithOverrides [ 3 ] . value, basal2Value)
565
+
566
+ // carb ratio with overrides
567
+ let carbRatio1Value = 10.0
568
+ let carbRatio1End = TimeInterval ( hours: 24 )
569
+ let carbRatio : [ AbsoluteScheduleValue < Double > ] = [
570
+ AbsoluteScheduleValue ( startDate: startOfDay, endDate: startOfDay. addingTimeInterval ( carbRatio1End) , value: carbRatio1Value) ,
571
+ ]
572
+ let carbRatioWithOverrides = overrides. applyBasal ( over: carbRatio)
573
+ XCTAssertEqual ( carbRatioWithOverrides. count, 3 )
574
+ XCTAssertEqual ( carbRatioWithOverrides [ 0 ] . startDate, startOfDay)
575
+ XCTAssertEqual ( carbRatioWithOverrides [ 0 ] . endDate, now)
576
+ XCTAssertEqual ( carbRatioWithOverrides [ 0 ] . value, carbRatio1Value)
577
+ XCTAssertEqual ( carbRatioWithOverrides [ 1 ] . startDate, now)
578
+ XCTAssertEqual ( carbRatioWithOverrides [ 1 ] . endDate, now. addingTimeInterval ( overrideDuration) )
579
+ XCTAssertEqual ( carbRatioWithOverrides [ 1 ] . value, carbRatio1Value)
580
+ XCTAssertEqual ( carbRatioWithOverrides [ 2 ] . startDate, now. addingTimeInterval ( overrideDuration) )
581
+ XCTAssertEqual ( carbRatioWithOverrides [ 2 ] . endDate, startOfDay. addingTimeInterval ( carbRatio1End) )
582
+ XCTAssertEqual ( carbRatioWithOverrides [ 2 ] . value, carbRatio1Value)
583
+
584
+ // target with overrides
585
+ let targetDuration = TimeInterval . hours ( 8 )
586
+ let target = [
587
+ AbsoluteScheduleValue (
588
+ startDate: now. addingTimeInterval ( - overrideDuration) ,
589
+ endDate: now. addingTimeInterval ( targetDuration) ,
590
+ value: scheduledRange
591
+ ) ,
592
+ ]
593
+
594
+ var targetWithOverrides = overrides. applyTarget ( over: target, at: now)
595
+ var expetedRange = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 100.0 ) ... LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 110.0 )
596
+ XCTAssertEqual ( targetWithOverrides. count, 2 )
597
+ XCTAssertEqual ( targetWithOverrides. first? . value, expetedRange)
598
+ XCTAssertEqual ( targetWithOverrides. first? . startDate, now. addingTimeInterval ( - overrideDuration) )
599
+ expetedRange = LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 140.0 ) ... LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 160.0 )
600
+ XCTAssertEqual ( targetWithOverrides. first? . endDate, now)
601
+ XCTAssertEqual ( targetWithOverrides. last? . value, expetedRange)
602
+ XCTAssertEqual ( targetWithOverrides. last? . startDate, now)
603
+ XCTAssertEqual ( targetWithOverrides. last? . endDate, now. addingTimeInterval ( targetDuration) )
604
+
605
+ // algorithm input
606
+ let doses : [ DoseEntry ] = [
607
+ DoseEntry ( type: . tempBasal, startDate: now. addingTimeInterval ( - overrideDuration) , value: 0 , unit: . unitsPerHour) ,
608
+ DoseEntry ( type: . tempBasal, startDate: now. addingTimeInterval ( - ( overrideDuration - . minutes( 30 ) * 1 ) ) , value: 1.15 , unit: . unitsPerHour) ,
609
+ DoseEntry ( type: . basal, startDate: now. addingTimeInterval ( - ( overrideDuration - . minutes( 30 ) * 2 ) ) , value: 1 , unit: . unitsPerHour) ,
610
+ DoseEntry ( type: . tempBasal, startDate: now. addingTimeInterval ( - ( overrideDuration - . minutes( 30 ) * 3 ) ) , value: 0.95 , unit: . unitsPerHour) ,
611
+ DoseEntry ( type: . tempBasal, startDate: now. addingTimeInterval ( - ( overrideDuration - . minutes( 30 ) * 4 ) ) , value: 0.80 , unit: . unitsPerHour) ,
612
+ ]
613
+ let dosesWithModel = doses. map { SimpleInsulinDose ( deliveryType: $0. type == . bolus ? . bolus : . basal, startDate: $0. startDate, endDate: $0. endDate, volume: $0. deliveredUnits ?? $0. programmedUnits, insulinModel: ExponentialInsulinModelPreset . rapidActingAdult. model) }
614
+
615
+ let correctionRange = target. closestPrior ( to: now) ? . value
616
+ let carbEntry = StoredCarbEntry ( startDate: now, quantity: LoopQuantity ( unit: . gram, doubleValue: 10.0 ) )
617
+
618
+ let glucoseValues : [ Double ] = [ 146 , 143 , 141 , 137 , 134 , 131 , 128 , 124 , 121 , 117 ,
619
+ 114 , 110 , 107 , 104 , 101 , 98 , 95 , 92 , 90 , 88 ,
620
+ 86 , 84 , 83 , 82 , 81 , 80 , 80 , 80 , 80 , 81 ,
621
+ 81 , 81 , 82 , 82 , 83 , 84 , 85 , 85 , 87 , 87 ,
622
+ 89 , 89 , 90 , 91 , 91 , 92 , 94 , 94 , 98 ]
623
+ let timeIntervalStepSize = TimeInterval ( minutes: 5 )
624
+ var glucoseHistory : [ StoredGlucoseSample ] = [ ]
625
+ var currentDate = Date ( ) . addingTimeInterval ( - 1 * timeIntervalStepSize*Double( glucoseValues. count) )
626
+ for (index, glucoseValue) in glucoseValues. enumerated ( ) {
627
+ let uuid = UUID ( )
628
+ currentDate = currentDate. addingTimeInterval ( timeIntervalStepSize)
629
+ var trendRate = 4.0 / timeIntervalStepSize
630
+ if index < glucoseValues. count - 1 {
631
+ trendRate = ( glucoseValues [ index+ 1 ] - glucoseValues[ index] ) / timeIntervalStepSize
632
+ }
633
+ glucoseHistory. append (
634
+ StoredGlucoseSample ( uuid: uuid,
635
+ provenanceIdentifier: " org.tidepool.Loop " ,
636
+ syncIdentifier: uuid. uuidString,
637
+ syncVersion: 1 ,
638
+ startDate: currentDate,
639
+ quantity: LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: glucoseValue) ,
640
+ trend: trendRate > 0 ? . upUp : . downDown,
641
+ trendRate: LoopQuantity ( unit: . milligramsPerDeciliterPerMinute, doubleValue: trendRate) ) )
642
+ }
643
+
644
+ let effectiveBolusApplicationFactor : Double ? = LoopAlgorithm . defaultBolusPartialApplicationFactor
645
+
646
+ var input = StoredDataAlgorithmInput (
647
+ glucoseHistory: glucoseHistory,
648
+ doses: dosesWithModel,
649
+ carbEntries: [ carbEntry] ,
650
+ predictionStart: now,
651
+ basal: basalWithOverrides,
652
+ sensitivity: sensitivityWithOverrides,
653
+ carbRatio: carbRatioWithOverrides,
654
+ target: targetWithOverrides,
655
+ suspendThreshold: LoopQuantity ( unit: . milligramsPerDeciliter, doubleValue: 75 ) ,
656
+ maxBolus: 10 ,
657
+ maxBasalRate: 5 ,
658
+ useIntegralRetrospectiveCorrection: false ,
659
+ includePositiveVelocityAndRC: true ,
660
+ carbAbsorptionModel: . linear,
661
+ recommendationInsulinModel: ExponentialInsulinModel ( actionDuration: 21600 , peakActivityTime: 4500 , delay: 600 ) ,
662
+ recommendationType: . manualBolus,
663
+ automaticBolusApplicationFactor: effectiveBolusApplicationFactor)
664
+
665
+ let prediction = LoopAlgorithm . generatePrediction (
666
+ start: input. predictionStart,
667
+ glucoseHistory: input. glucoseHistory,
668
+ doses: input. doses,
669
+ carbEntries: input. carbEntries,
670
+ basal: input. basal,
671
+ sensitivity: input. sensitivity,
672
+ carbRatio: input. carbRatio,
673
+ algorithmEffectsOptions: . all,
674
+ useIntegralRetrospectiveCorrection: input. useIntegralRetrospectiveCorrection,
675
+ includingPositiveVelocityAndRC: input. includePositiveVelocityAndRC,
676
+ useMidAbsorptionISF: input. useMidAbsorptionISF,
677
+ carbAbsorptionModel: input. carbAbsorptionModel. model)
678
+
679
+ XCTAssertTrue ( prediction. glucose. last!. quantity > overrideRange. lowerBound)
680
+ XCTAssertTrue ( prediction. glucose. last!. quantity < overrideRange. upperBound)
681
+ }
447
682
448
683
func testPreMealPreset( ) {
449
684
let now = ISO8601DateFormatter ( ) . date ( from: " 2020-03-11T12:13:14-0700 " ) !
0 commit comments