-
Notifications
You must be signed in to change notification settings - Fork 514
/
determine-basal.js
2051 lines (1811 loc) · 100 KB
/
determine-basal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
Determine Basal
Released under MIT license. See the accompanying LICENSE.txt file for
full terms and conditions
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// Define various functions used later on, in the main function determine_basal() below
var round_basal = require('../round-basal');
// Rounds value to 'digits' decimal places
function round(value, digits) {
if (! digits) { digits = 0; }
var scale = Math.pow(10, digits);
return Math.round(value * scale) / scale;
}
// we expect BG to rise or fall at the rate of BGI,
// adjusted by the rate at which BG would need to rise /
// fall to get eventualBG to target over 2 hours
function calculate_expected_delta(target_bg, eventual_bg, bgi) {
// (hours * mins_per_hour) / 5 = how many 5 minute periods in 2h = 24
var five_min_blocks = (2 * 60) / 5;
var target_delta = target_bg - eventual_bg;
return /* expectedDelta */ round(bgi + (target_delta / five_min_blocks), 1);
}
function convert_bg(value, profile)
{
if (profile.out_units === "mmol/L")
{
return round(value * 0.0555,1);
}
else
{
return Math.round(value);
}
}
function enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_bg, oref_variables, time) {
if (oref_variables.smbIsScheduledOff){
/* Below logic is related to profile overrides which can disable SMBs or disable them for a scheduled window.
* SMBs will be disabled from [start, end), such that if an SMB is scheduled to be disabled from 10 AM to 2 PM,
* an SMB will not be allowed from 10:00:00 until 1:59:59.
*/
let currentHour = new Date(time.getHours());
let startTime = oref_variables.start;
let endTime = oref_variables.end;
if (startTime < endTime && (currentHour >= startTime && currentHour < endTime)) {
console.error("SMB disabled: current time is in SMB disabled scheduled")
return false
} else if (startTime > endTime && (currentHour >= startTime || currentHour < endTime)) {
console.error("SMB disabled: current time is in SMB disabled scheduled")
return false
} else if (startTime == 0 && endTime == 0) {
console.error("SMB disabled: current time is in SMB disabled scheduled")
return false;
} else if (startTime == endTime && currentHour == startTime) {
console.error("SMB disabled: current time is in SMB disabled scheduled")
return false;
}
}
// disable SMB when a high temptarget is set
if (! microBolusAllowed) {
console.error("SMB disabled (!microBolusAllowed)");
return false;
} else if (! profile.allowSMB_with_high_temptarget && profile.temptargetSet && target_bg > 100) {
console.error("SMB disabled due to high temptarget of " + target_bg);
return false;
} else if (meal_data.bwFound === true && profile.A52_risk_enable === false) {
console.error("SMB disabled due to Bolus Wizard activity in the last 6 hours.");
return false;
// Disable if invalid CGM reading (HIGH)
} else if (bg == 400) {
console.error("Invalid CGM (HIGH). SMBs disabled.");
return false;
}
// enable SMB/UAM if always-on (unless previously disabled for high temptarget)
if (profile.enableSMB_always === true) {
if (meal_data.bwFound) {
console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard");
} else {
console.error("SMB enabled due to enableSMB_always");
}
return true;
}
// enable SMB/UAM (if enabled in preferences) while we have COB
if (profile.enableSMB_with_COB === true && meal_data.mealCOB) {
if (meal_data.bwCarbs) {
console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard");
} else {
console.error("SMB enabled for COB of " + meal_data.mealCOB);
}
return true;
}
// enable SMB/UAM (if enabled in preferences) for a full 6 hours after any carb entry
// (6 hours is defined in carbWindow in lib/meal/total.js)
if (profile.enableSMB_after_carbs === true && meal_data.carbs ) {
if (meal_data.bwCarbs) {
console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard");
} else {
console.error("SMB enabled for 6h after carb entry");
}
return true;
}
// enable SMB/UAM (if enabled in preferences) if a low temptarget is set
if (profile.enableSMB_with_temptarget === true && (profile.temptargetSet && target_bg < 100)) {
if (meal_data.bwFound) {
console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard");
} else {
console.error("SMB enabled for temptarget of " + convert_bg(target_bg, profile));
}
return true;
}
// enable SMB if high bg is found
if (profile.enableSMB_high_bg === true && high_bg !== null && bg >= high_bg) {
console.error("Checking BG to see if High for SMB enablement.");
console.error("Current BG", bg, " | High BG ", high_bg);
if (meal_data.bwFound) {
console.error("Warning: High BG SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard");
} else {
console.error("High BG detected. Enabling SMB.");
}
return true;
}
console.error("SMB disabled (no enableSMB preferences active or no condition satisfied)");
return false;
}
var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, microBolusAllowed, reservoir_data, currentTime, pumphistory, preferences, basalprofile, oref2_variables, middleWare) {
var profileTarget = profile.min_bg;
var overrideTarget = oref2_variables.overrideTarget;
if (overrideTarget != 0 && overrideTarget != 6 && oref2_variables.useOverride && !profile.temptargetSet) {
profileTarget = overrideTarget;
}
const smbIsOff = oref2_variables.smbIsOff;
const advancedSettings = oref2_variables.advancedSettings;
const isfAndCr = oref2_variables.isfAndCr;
const isf = oref2_variables.isf;
const cr_ = oref2_variables.cr;
const smbIsScheduledOff = oref2_variables.smbIsScheduledOff;
const start = oref2_variables.start;
var end = oref2_variables.end;
const smbMinutes = oref2_variables.smbMinutes;
const uamMinutes = oref2_variables.uamMinutes;
var dynISFenabled = preferences.useNewFormula
var insulinForManualBolus = 0;
var manualBolusErrorString = 0;
var threshold = profileTarget;
var systemTime = new Date();
if (currentTime) {
systemTime = new Date(currentTime);
}
// tdd past 24 hours
var pumpData = 0;
var logtdd = "";
var logBasal = "";
var logBolus = "";
var logTempBasal = "";
var dataLog = "";
var logOutPut = "";
var tddReason = "";
var current = 0;
var tdd = 0;
var insulin = 0;
var tempInsulin = 0;
var bolusInsulin = 0;
var scheduledBasalInsulin = 0;
var quota = 0;
const weightedAverage = oref2_variables.weightedAverage;
var overrideFactor = 1;
var sensitivity = profile.sens;
var carbRatio = profile.carb_ratio;
if (oref2_variables.useOverride) {
overrideFactor = oref2_variables.overridePercentage / 100;
if (isfAndCr) {
sensitivity /= overrideFactor;
carbRatio /= overrideFactor;
} else {
if (cr_) { carbRatio /= overrideFactor; }
if (isf) { sensitivity /= overrideFactor; }
}
}
const weightPercentage = profile.weightPercentage;
const average_total_data = oref2_variables.average_total_data;
function addTimeToDate(objDate, _hours) {
var ms = objDate.getTime();
var add_ms = _hours * 36e5;
var newDateObj = new Date(ms + add_ms);
return newDateObj;
}
function subtractTimeFromDate(date, hours_) {
var ms_ = date.getTime();
var add_ms_ = hours_ * 36e5;
var new_date = new Date(ms_ - add_ms_);
return new_date;
}
function accountForIncrements(insulin) {
// If you have not set this to.0.1 in Trio settings, this will be set to 0.05 (Omnipods) in code.
var minimalDose = profile.bolus_increment;
if (minimalDose != 0.1) {
minimalDose = 0.05;
}
var incrementsRaw = insulin / minimalDose;
if (incrementsRaw >= 1) {
var incrementsRounded = Math.floor(incrementsRaw);
return round(incrementsRounded * minimalDose, 5);
} else { return 0; }
}
function makeBaseString(base_timeStamp) {
function addZero(i) {
if (i < 10) { i = "0" + i }
return i;
}
let hour = addZero(base_timeStamp.getHours());
let minutes = addZero(base_timeStamp.getMinutes());
let seconds = "00";
let string = hour + ":" + minutes + ":" + seconds;
return string;
}
function timeDifferenceOfString(string1, string2) {
//Base time strings are in "00:00:00" format
var time1 = new Date("1/1/1999 " + string1);
var time2 = new Date("1/1/1999 " + string2);
var ms1 = time1.getTime();
var ms2 = time2.getTime();
var difference = (ms1 - ms2) / 36e5;
return difference;
}
// In case the autosens.min/max limits are reversed:
const minLimitChris = Math.min(profile.autosens_min, profile.autosens_max);
const maxLimitChris = Math.max(profile.autosens_min, profile.autosens_max);
// Turn off when autosens.min = autosens.max
if (maxLimitChris == minLimitChris || maxLimitChris < 1 || minLimitChris > 1) {
dynISFenabled = false;
console.log("Dynamic ISF disabled due to current autosens settings");
}
function calcScheduledBasalInsulin(lastRealTempTime, addedLastTempTime) {
var totalInsulin = 0;
var old = addedLastTempTime;
var totalDuration = (lastRealTempTime - addedLastTempTime) / 36e5;
var basDuration = 0;
var totalDurationCheck = totalDuration;
var durationCurrentSchedule = 0;
do {
if (totalDuration > 0) {
var baseTime_ = makeBaseString(old);
//Default basalrate in case none is found...
var basalScheduledRate_ = basalprofile[0].rate;
for (let m = 0; m < basalprofile.length; m++) {
var timeToTest = basalprofile[m].start;
if (baseTime_ == timeToTest) {
if (m + 1 < basalprofile.length) {
let end = basalprofile[m+1].start;
let start = basalprofile[m].start;
durationCurrentSchedule = timeDifferenceOfString(end, start);
if (totalDuration >= durationCurrentSchedule) {
basDuration = durationCurrentSchedule;
} else if (totalDuration < durationCurrentSchedule) {
basDuration = totalDuration;
}
}
else if (m + 1 == basalprofile.length) {
let end = basalprofile[0].start;
let start = basalprofile[m].start;
// First schedule is 00:00:00. Changed places of start and end here.
durationCurrentSchedule = 24 - (timeDifferenceOfString(start, end));
if (totalDuration >= durationCurrentSchedule) {
basDuration = durationCurrentSchedule;
} else if (totalDuration < durationCurrentSchedule) {
basDuration = totalDuration;
}
}
basalScheduledRate_ = basalprofile[m].rate;
totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
totalDuration -= basDuration;
console.log("Dynamic ratios log: scheduled insulin added: " + accountForIncrements(basalScheduledRate_ * basDuration) + " U. Bas duration: " + basDuration.toPrecision(3) + " h. Base Rate: " + basalScheduledRate_ + " U/h" + ". Time :" + baseTime_);
// Move clock to new date
old = addTimeToDate(old, basDuration);
}
else if (baseTime_ > timeToTest) {
if (m + 1 < basalprofile.length) {
var timeToTest2 = basalprofile[m+1].start
if (baseTime_ < timeToTest2) {
// durationCurrentSchedule = timeDifferenceOfString(end, start);
durationCurrentSchedule = timeDifferenceOfString(timeToTest2, baseTime_);
if (totalDuration >= durationCurrentSchedule) {
basDuration = durationCurrentSchedule;
} else if (totalDuration < durationCurrentSchedule) {
basDuration = totalDuration;
}
basalScheduledRate_ = basalprofile[m].rate;
totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
totalDuration -= basDuration;
console.log("Dynamic ratios log: scheduled insulin added: " + accountForIncrements(basalScheduledRate_ * basDuration) + " U. Bas duration: " + basDuration.toPrecision(3) + " h. Base Rate: " + basalScheduledRate_ + " U/h" + ". Time :" + baseTime_);
// Move clock to new date
old = addTimeToDate(old, basDuration);
}
}
else if (m == basalprofile.length - 1) {
// let start = basalprofile[m].start;
let start = baseTime_;
// First schedule is 00:00:00. Changed places of start and end here.
durationCurrentSchedule = timeDifferenceOfString("23:59:59", start);
if (totalDuration >= durationCurrentSchedule) {
basDuration = durationCurrentSchedule;
} else if (totalDuration < durationCurrentSchedule) {
basDuration = totalDuration;
}
basalScheduledRate_ = basalprofile[m].rate;
totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
totalDuration -= basDuration;
console.log("Dynamic ratios log: scheduled insulin added: " + accountForIncrements(basalScheduledRate_ * basDuration) + " U. Bas duration: " + basDuration.toPrecision(3) + " h. Base Rate: " + basalScheduledRate_ + " U/h" + ". Time :" + baseTime_);
// Move clock to new date
old = addTimeToDate(old, basDuration);
}
}
}
}
//totalDurationCheck to avoid infinite loop
} while (totalDuration > 0 && totalDuration < totalDurationCheck);
// amount of insulin according to pump basal rate schedules
return totalInsulin;
}
// Check that there is enough pump history data (>21 hours) for tdd calculation. Estimate the missing hours (24-pumpData) using hours with scheduled basal rates. Not perfect, but sometimes the
// pump history in FAX is only 22-23.5 hours, even when you've been looping with FAX for many days. This is to reduce the error from just using pump history as data source as much as possible.
// AT basal rates are not used for this estimation, instead the basal rates in pump settings.
// Check for empty pump history (new FAX loopers). If empty: don't use dynamic settings!
if (!pumphistory.length) {
console.log("Pumphistory is empty!");
dynISFenabled = false;
enableDynamicCR = false;
} else if (dynISFenabled) {
let phLastEntry = pumphistory.length - 1;
var endDate = new Date(pumphistory[phLastEntry].timestamp);
var startDate = new Date(pumphistory[0].timestamp);
// If latest pump event is a temp basal
if (pumphistory[0]._type == "TempBasalDuration") {
startDate = new Date();
}
pumpData = (startDate - endDate) / 36e5;
if (pumpData < 23.9 && pumpData > 21) {
var missingHours = 24 - pumpData;
// Makes new end date for a total time duration of exakt 24 hour.
var endDate_ = subtractTimeFromDate(endDate, missingHours);
// endDate - endDate_ = missingHours
scheduledBasalInsulin = calcScheduledBasalInsulin(endDate, endDate_);
dataLog = "24 hours of data is required for an accurate tdd calculation. Currently only " + pumpData.toPrecision(3) + " hours of pump history data are available. Using your pump scheduled basals to fill in the missing hours. Scheduled basals added: " + scheduledBasalInsulin.toPrecision(5) + " U. ";
} else if (pumpData < 21) {
dynISFenabled = false;
enableDynamicCR = false;
} else { dataLog = ""; }
}
// Calculate tdd ----------------------------------------------------------------------
if (dynISFenabled) {
//Bolus:
for (let i = 0; i < pumphistory.length; i++) {
if (pumphistory[i]._type == "Bolus") {
bolusInsulin += pumphistory[i].amount;
}
}
// Temp basals:
for (let j = 1; j < pumphistory.length; j++) {
if (pumphistory[j]._type == "TempBasal" && pumphistory[j].rate > 0) {
current = j;
quota = pumphistory[j].rate;
var duration = pumphistory[j-1]['duration (min)'] / 60;
var origDur = duration;
var pastTime = new Date(pumphistory[j-1].timestamp);
var morePresentTime = new Date(pastTime);
var substractTimeOfRewind = 0;
// If temp basal hasn't yet ended, use now as end date for calculation
do {
j--;
if (j == 0) {
morePresentTime = new Date();
break;
}
else if (pumphistory[j]._type == "TempBasal" || pumphistory[j]._type == "PumpSuspend") {
morePresentTime = new Date(pumphistory[j].timestamp);
break;
}
// During the time the Medtronic pumps are rewinded and primed, this duration of suspened insulin delivery needs to be accounted for.
var pp = j-2;
if (pp >= 0) {
if (pumphistory[pp]._type == "Rewind") {
let rewindTimestamp = pumphistory[pp].timestamp;
// There can be several Prime events
while (pp - 1 >= 0) {
pp -= 1;
if (pumphistory[pp]._type == "Prime") {
substractTimeOfRewind = (pumphistory[pp].timestamp - rewindTimestamp) / 36e5;
} else { break }
}
// If Medtronic user forgets to insert infusion set
if (substractTimeOfRewind >= duration) {
morePresentTime = new Date(rewindTimestamp);
substractTimeOfRewind = 0;
}
}
}
} while (j > 0);
var diff = (morePresentTime - pastTime) / 36e5;
if (diff < origDur) {
duration = diff;
}
insulin = quota * (duration - substractTimeOfRewind);
tempInsulin += accountForIncrements(insulin);
j = current;
}
}
// Check and count for when basals are delivered with a scheduled basal rate.
// 1. Check for 0 temp basals with 0 min duration. This is for when ending a manual temp basal and (perhaps) continuing in open loop for a while.
// 2. Check for temp basals that completes. This is for when disconnected from link/iphone, or when in open loop.
// 3. Account for a punp suspension. This is for when pod screams or when MDT or pod is manually suspended.
// 4. Account for a pump resume (in case pump/cgm is disconnected before next loop).
// To do: are there more circumstances when scheduled basal rates are used? Do we need to care about "Prime" and "Rewind" with MDT pumps?
//
for (let k = 0; k < pumphistory.length; k++) {
// Check for 0 temp basals with 0 min duration.
insulin = 0;
if (pumphistory[k]['duration (min)'] == 0 || pumphistory[k]._type == "PumpResume") {
let time1 = new Date(pumphistory[k].timestamp);
let time2 = new Date(time1);
let l = k;
do {
if (l > 0) {
--l;
if (pumphistory[l]._type == "TempBasal") {
time2 = new Date(pumphistory[l].timestamp);
break;
}
}
} while (l > 0);
// duration of current scheduled basal in h
let basDuration = (time2 - time1) / 36e5;
if (basDuration > 0) {
scheduledBasalInsulin += calcScheduledBasalInsulin(time2, time1);
}
}
}
// Check for temp basals that completes
for (let n = pumphistory.length -1; n > 0; n--) {
if (pumphistory[n]._type == "TempBasalDuration") {
// duration in hours
let oldBasalDuration = pumphistory[n]['duration (min)'] / 60;
// time of old temp basal
let oldTime = new Date(pumphistory[n].timestamp);
var newTime = new Date(oldTime);
let o = n;
do {
--o;
if (o >= 0) {
if (pumphistory[o]._type == "TempBasal" || pumphistory[o]._type == "PumpSuspend") {
// time of next (new) temp basal or a pump suspension
newTime = new Date(pumphistory[o].timestamp);
break;
}
}
} while (o > 0);
// When latest temp basal is index 0 in pump history
if (n == 0 && pumphistory[0]._type == "TempBasalDuration") {
newTime = new Date();
oldBasalDuration = pumphistory[n]['duration (min)'] / 60;
}
let tempBasalTimeDifference = (newTime - oldTime) / 36e5;
let timeOfbasal = tempBasalTimeDifference - oldBasalDuration;
// if duration of scheduled basal is more than 0
if (timeOfbasal > 0) {
// Timestamp after completed temp basal
let timeOfScheduledBasal = addTimeToDate(oldTime, oldBasalDuration);
scheduledBasalInsulin += calcScheduledBasalInsulin(newTime, timeOfScheduledBasal);
}
}
}
tdd = bolusInsulin + tempInsulin + scheduledBasalInsulin;
var insulin_ = {
TDD: round(tdd, 5),
bolus: round(bolusInsulin, 5),
temp_basal: round(tempInsulin, 5),
scheduled_basal: round(scheduledBasalInsulin, 5)
}
if (pumpData > 21) {
logBolus = ". Bolus insulin: " + bolusInsulin.toPrecision(5) + " U";
logTempBasal = ". Temporary basal insulin: " + tempInsulin.toPrecision(5) + " U";
logBasal = ". Insulin with scheduled basal rate: " + scheduledBasalInsulin.toPrecision(5) + " U";
logtdd = " TDD past 24h is: " + tdd.toPrecision(5) + " U";
logOutPut = dataLog + logtdd + logBolus + logTempBasal + logBasal;
tddReason = ", TDD: " + round(tdd,2) + " U, " + round(bolusInsulin/tdd*100,0) + "% Bolus " + round((tempInsulin+scheduledBasalInsulin)/tdd*100,0) + "% Basal";
} else { tddReason = ", TDD: Not enough pumpData (< 21h)"; }
}
var tdd_before = tdd;
// -------------------- END OF TDD ----------------------------------------------------
// Dynamic ratios
const BG = glucose_status.glucose;
const useDynamicCR = preferences.enableDynamicCR;
const adjustmentFactor = preferences.adjustmentFactor;
const adjustmentFactorSigmoid = preferences.adjustmentFactorSigmoid;
const enable_sigmoid = preferences.sigmoid;
const currentMinTarget = profileTarget;
var exerciseSetting = false;
var log = "";
var tdd24h_14d_Ratio = 1;
var basal_ratio_log = "";
if (average_total_data > 0) {
tdd24h_14d_Ratio = weightedAverage / average_total_data;
}
// respect autosens_max/min for tdd24h_14d_Ratio, used to adjust basal similarly as autosens
if (tdd24h_14d_Ratio > 1) {
tdd24h_14d_Ratio = Math.min(tdd24h_14d_Ratio, profile.autosens_max);
tdd24h_14d_Ratio = round(tdd24h_14d_Ratio,2);
basal_ratio_log = "Basal adjustment with a 24 hour to total average (up to 14 days of data) TDD ratio (limited by Autosens max setting). Basal Ratio: " + tdd24h_14d_Ratio + ". Upper limit = Autosens max (" + profile.autosens_max + ")";
}
else if (tdd24h_14d_Ratio < 1) {
tdd24h_14d_Ratio = Math.max(tdd24h_14d_Ratio, profile.autosens_min);
tdd24h_14d_Ratio = round(tdd24h_14d_Ratio,2);
basal_ratio_log = "Basal adjustment with a 24 hour to to total average (up to 14 days of data) TDD ratio (limited by Autosens min setting). Basal Ratio: " + tdd24h_14d_Ratio + ". Lower limit = Autosens min (" + profile.autosens_min + ")";
}
else {
basal_ratio_log = "Basal adjusted with a 24 hour to total average (up to 14 days of data) TDD ratio: " + tdd24h_14d_Ratio;
}
basal_ratio_log = ", Basal ratio: " + tdd24h_14d_Ratio;
// One of two exercise settings (they share the same purpose)
if (profile.high_temptarget_raises_sensitivity || profile.exercise_mode || oref2_variables.isEnabled) {
exerciseSetting = true;
}
// Turn off Chris' formula when using a temp target >= 118 (6.5 mol/l) and if an exercise setting is enabled.
if (currentMinTarget >= 118 && exerciseSetting) {
dynISFenabled = false;
log = "Dynamic ISF temporarily off due to a high temp target/exercising. Current min target: " + currentMinTarget;
}
var startLog = ", Dynamic ratios log: ";
var afLog = ", AF: " + (enable_sigmoid ? adjustmentFactorSigmoid : adjustmentFactor);
var bgLog = "BG: " + BG + " mg/dl (" + (BG * 0.0555).toPrecision(2) + " mmol/l)";
var formula = "";
var weightLog = "";
// Insulin curve
const curve = preferences.curve;
const ipt = profile.insulinPeakTime;
const ucpk = preferences.useCustomPeakTime;
var insulinFactor = 55; // deafult (120-65)
var insulinPA = 65; // default (Novorapid/Novolog)
switch (curve) {
case "rapid-acting":
insulinPA = 65;
break;
case "ultra-rapid":
insulinPA = 50;
break;
}
if (ucpk) {
insulinFactor = 120 - ipt;
console.log("Custom insulinpeakTime set to :" + ipt + ", insulinFactor: " + insulinFactor);
} else {
insulinFactor = 120 - insulinPA;
console.log("insulinFactor set to : " + insulinFactor);
}
// Use weighted TDD average
tdd_before = tdd;
if (weightPercentage < 1 && weightedAverage > 0) {
tdd = weightedAverage;
console.log("Using weighted TDD average: " + round(tdd,2) + " U, instead of past 24 h (" + round(tdd_before,2) + " U), weight: " + weightPercentage);
weightLog = ", Weighted TDD: " + round(tdd,2) + " U";
}
// Modified Chris Wilson's' formula with added adjustmentFactor for tuning and use of the autosens.ratio:
// var newRatio = profile.sens * adjustmentFactor * tdd * BG / 277700;
//
// New logarithmic formula : var newRatio = profile.sens * adjustmentFactor * tdd * ln(( BG/insulinFactor) + 1 )) / 1800
//
var sigmoidLog = ""
if (dynISFenabled) {
// Logarithmic
if (!enable_sigmoid) {
var newRatio = sensitivity * adjustmentFactor * tdd * Math.log(BG/insulinFactor+1) / 1800;
formula = ", Logarithmic formula";
}
// Sigmoid
else {
const as_min = minLimitChris;
const autosens_interval = maxLimitChris - as_min;
//Blood glucose deviation from set target (the lower BG target) converted to mmol/l to fit current formula.
const bg_dev = (BG - profileTarget) * 0.0555;
// Account for TDD of insulin. Compare last 2 hours with total data (up to 14 days)
var tdd_factor = tdd24h_14d_Ratio; // weighted average TDD / total data average TDD
var max_minus_one = maxLimitChris - 1;
// Avoid division by 0
if (maxLimitChris == 1) {
max_minus_one = maxLimitChris + 0.01 - 1;
}
//Makes sigmoid factor(y) = 1 when BG deviation(x) = 0.
const fix_offset = (Math.log10(1/max_minus_one-as_min/max_minus_one) / Math.log10(Math.E));
//Exponent used in sigmoid formula
const exponent = bg_dev * adjustmentFactorSigmoid * tdd_factor + fix_offset;
// The sigmoid function
const sigmoid_factor = autosens_interval / (1 + Math.exp(-exponent)) + as_min;
newRatio = sigmoid_factor;
formula = ", Sigmoid function";
// Dynamic CR will be processed further down
}
}
var cr = carbRatio;
const cr_before = round(carbRatio, 1);
var log_isfCR = "";
var limitLog = "";
if (dynISFenabled && tdd > 0) {
log_isfCR = ", Dynamic ISF/CR: On/";
// Respect autosens.max and autosens.min limitLogs
if (newRatio > maxLimitChris) {
log = ", Dynamic ISF limited by autosens_max setting: " + maxLimitChris + " (" + round(newRatio,2) + "), ";
limitLog = ", Autosens/Dynamic Limit: " + maxLimitChris + " (" + round(newRatio,2) + ")";
newRatio = maxLimitChris;
} else if (newRatio < minLimitChris) {
log = ", Dynamic ISF limited by autosens_min setting: " + minLimitChris + " (" + round(newRatio,2) + "). ";
limitLog = ", Autosens/Dynamic Limit: " + minLimitChris + " (" + round(newRatio,2) + ")";
newRatio = minLimitChris;
}
// Dynamic CR (Test)
if (useDynamicCR) {
log_isfCR += "On";
var dynCR = newRatio;
/*
// Lessen the ratio used by half, if newRatio > 1.
if (newRatio > 1) {
dynCR = (newRatio - 1) / 2 + 1;
}
cr = round(cr/dynCR, 2);
var logCR = " CR: " + cr + " g/U";
carbRatio = cr;
*/
carbRatio /= dynCR;
var logCR = ". New Dynamic CR: " + round(carbRatio, 1) + " g/U";
} else {
logCR = " CR: " + cr + " g/U";
log_isfCR += "Off";
}
const isf = sensitivity / newRatio;
// Set the new ratio
autosens_data.ratio = newRatio;
sigmoidLog = ". Using Sigmoid function, the autosens ratio has been adjusted with sigmoid factor to: " + round(autosens_data.ratio, 2) + ". New ISF = " + round(isf, 2) + " mg/dl (" + round(0.0555 * isf, 2) + " (mmol/l)" + ". CR adjusted from " + round(cr_before,2) + " to " + round(carbRatio,2);
if (!enable_sigmoid) {
log += ", Dynamic autosens.ratio set to " + round(newRatio,2) + " with ISF: " + isf.toPrecision(3) + " mg/dl/U (" + (isf * 0.0555).toPrecision(3) + " mmol/l/U)";
} else { log += sigmoidLog }
logOutPut += startLog + bgLog + afLog + formula + log + log_isfCR + logCR + weightLog;
} else { logOutPut += startLog + "Dynamic Settings disabled"; }
console.log(logOutPut);
if (!dynISFenabled && !useDynamicCR) {
tddReason += "";
} else if (dynISFenabled && profile.tddAdjBasal) {
tddReason += log_isfCR + formula + limitLog + afLog + basal_ratio_log;
}
else if (dynISFenabled && !profile.tddAdjBasal) { tddReason += log_isfCR + formula + limitLog + afLog; }
if (0.5 !== profile.smb_delivery_ratio) {
tddReason += ", SMB Ratio: " + Math.min(profile.smb_delivery_ratio, 1);
}
// Not confident but something like this in iAPS v3.0.3
if (middleWare !== "" && middleWare !== "Nothing changed"){
tddReason += ", Middleware: " + middleWare;
}
// --------------- END OF DYNAMIC RATIOS CALCULATION ------ A FEW LINES ADDED ALSO AT LINE NR 1136 and 1178 ------------------------------------------------
// Set variables required for evaluating error conditions
var rT = {}; //short for requestedTemp
var deliverAt = new Date(systemTime);
if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') {
rT.error ='Error: could not get current basal rate';
return rT;
}
var profile_current_basal = round_basal(profile.current_basal, profile) * overrideFactor;
var basal = profile_current_basal;
// Print Current Override factor, if any
if (oref2_variables.useOverride) {
if (oref2_variables.duration == 0) {
console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Duration: " + "Enabled indefinitely");
} else
console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Expires in: " + oref2_variables.duration + " min.");
}
var bgTime = new Date(glucose_status.date);
var minAgo = round( (systemTime - bgTime) / 60 / 1000 ,1);
var bg = glucose_status.glucose;
var noise = glucose_status.noise;
// Prep various delta variables.
var tick;
if (glucose_status.delta > -0.5) {
tick = "+" + round(glucose_status.delta,0);
} else {
tick = round(glucose_status.delta,0);
}
//var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta);
var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta);
var minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta);
var maxDelta = Math.max(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta);
// Cancel high temps (and replace with neutral) or shorten long zero temps for various error conditions
// 38 is an xDrip error state that usually indicates sensor failure
// all other BG values between 11 and 37 mg/dL reflect non-error-code BG values, so we should zero temp for those
// First, print out different explanations for each different error condition
if (bg <= 10 || bg === 38 || noise >= 3) { //Dexcom is in ??? mode or calibrating, or xDrip reports high noise
rT.reason = "CGM is calibrating, in ??? state, or noise is high";
}
var tooflat=false;
if (bg > 60 && glucose_status.delta == 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1 && bg != 400) {
if (glucose_status.device == "fakecgm") {
console.error("CGM data is unchanged (" + convert_bg(bg,profile) + "+" + convert_bg(glucose_status.delta,profile)+ ") for 5m w/ " + convert_bg(glucose_status.short_avgdelta,profile) + " mg/dL ~15m change & " + convert_bg(glucose_status.long_avgdelta,2) + " mg/dL ~45m change");
console.error("Simulator mode detected (" + glucose_status.device + "): continuing anyway");
} else if (bg != 400) {
tooflat=true;
}
}
if (minAgo > 12 || minAgo < -5) { // Dexcom data is too old, or way in the future
rT.reason = "If current system time " + systemTime + " is correct, then BG data is too old. The last BG data was read "+minAgo+"m ago at "+bgTime;
// if BG is too old/noisy, or is completely unchanging, cancel any high temps and shorten any long zero temps
} else if ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 && bg != 400 ) {
if ( glucose_status.last_cal && glucose_status.last_cal < 3 ) {
rT.reason = "CGM was just calibrated";
} else {
rT.reason = "CGM data is unchanged (" + convert_bg(bg,profile) + "+" + convert_bg(glucose_status.delta,profile) + ") for 5m w/ " + convert_bg(glucose_status.short_avgdelta,profile) + " mg/dL ~15m change & " + convert_bg(glucose_status.long_avgdelta,profile) + " mg/dL ~45m change";
}
}
if (bg != 400) {
if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 ) ) {
if (currenttemp.rate >= basal) { // high temp is running
rT.reason += ". Canceling high temp basal of " + currenttemp.rate;
rT.deliverAt = deliverAt;
rT.temp = 'absolute';
rT.duration = 0;
rT.rate = 0;
return rT;
// don't use setTempBasal(), as it has logic that allows <120% high temps to continue running
//return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
} else if ( currenttemp.rate === 0 && currenttemp.duration > 30 ) { //shorten long zero temps to 30m
rT.reason += ". Shortening " + currenttemp.duration + "m long zero temp to 30m. ";
rT.deliverAt = deliverAt;
rT.temp = 'absolute';
rT.duration = 30;
rT.rate = 0;
return rT;
// don't use setTempBasal(), as it has logic that allows long zero temps to continue running
//return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp);
} else { //do nothing.
rT.reason += ". Temp " + currenttemp.rate + " <= current basal " + basal + "U/hr; doing nothing. ";
return rT;
}
}
}
// Get configured target, and return if unable to do so.
// This should occur after checking that we're not in one of the CGM-data-related error conditions handled above,
// and before using target_bg to adjust sensitivityRatio below.
var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver
// if min and max are set, then set target to their average
var target_bg;
var min_bg;
var max_bg;
var high_bg;
if (typeof profileTarget !== 'undefined') {
min_bg = profileTarget;
}
if (typeof profile.max_bg !== 'undefined') {
max_bg = profileTarget;
}
if (typeof profile.enableSMB_high_bg_target !== 'undefined') {
high_bg = profile.enableSMB_high_bg_target;
}
if (typeof profileTarget !== 'undefined') {
target_bg = profileTarget;
} else {
rT.error ='Error: could not determine target_bg. ';
return rT;
}
// Calculate sensitivityRatio based on temp targets, if applicable, or using the value calculated by autosens
// var sensitivityRatio;
var high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity || oref2_variables.isEnabled;
var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled target (which might change)
var halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%)
// 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limitLoged to autosens_max (1.2x by default)
//if ( profile.half_basal_exercise_target ) {
halfBasalTarget = profile.half_basal_exercise_target;
//}
if (oref2_variables.isEnabled) {
const newHalfBasalTarget = oref2_variables.hbt;
console.log("Half Basal Target used: " + convert_bg(newHalfBasalTarget, profile) + " " + profile.out_units);
halfBasalTarget = newHalfBasalTarget;
} else { console.log("Default Half Basal Target used: " + convert_bg(halfBasalTarget, profile) + " " + profile.out_units) }
if ( high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget ||
profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget ||
oref2_variables.isEnabled && profile.temptargetSet && target_bg < normalTarget ) {
// w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44
// e.g.: Sensitivity ratio set to 0.8 based on temp target of 120; Adjusting basal from 1.65 to 1.35; ISF from 58.9 to 73.6
//sensitivityRatio = 2/(2+(target_bg-normalTarget)/40);
var c = halfBasalTarget - normalTarget;
// getting multiplication less or equal to 0 means that we have a really low target with a really low halfBasalTarget
// with low TT and lowTTlowersSensitivity we need autosens_max as a value
// we use multiplication instead of the division to avoid "division by zero error"
if (c * (c + target_bg-normalTarget) <= 0.0) {
sensitivityRatio = profile.autosens_max;
}
else {
sensitivityRatio = c/(c+target_bg-normalTarget);
}
// limit sensitivityRatio to profile.autosens_max (1.2x by default)
sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max);
sensitivityRatio = round(sensitivityRatio,2);
process.stderr.write("Sensitivity ratio set to "+sensitivityRatio+" based on temp target of "+target_bg+"; ");
}
else if (typeof autosens_data !== 'undefined' && autosens_data) {
sensitivityRatio = autosens_data.ratio;
// Override Profile.Target
if (overrideTarget !== 0 && overrideTarget !== 6 && overrideTarget !== profile.min_bg && !profile.temptargetSet) {
target_bg = overrideTarget;
console.log("Current Override Profile Target: " + convert_bg(overrideTarget, profile) + " " + profile.out_units);
}
process.stderr.write("Autosens ratio: "+sensitivityRatio+"; ");
}
// Increase the dynamic ratio when using a low temp target
if (profile.temptargetSet && target_bg < normalTarget && dynISFenabled && BG >= target_bg) {
if (sensitivityRatio < newRatio) {
autosens_data.ratio = newRatio * (normalTarget/target_bg);
//Use autosesns.max limit
autosens_data.ratio = Math.min(autosens_data.ratio, profile.autosens_max);
sensitivityRatio = round(autosens_data.ratio, 2);
console.log("Dynamic ratio increased from " + round(newRatio, 2) + " to " + round(autosens_data.ratio,2) + " due to a low temp target (" + target_bg + ").");
}
}
if (sensitivityRatio && !dynISFenabled) { // Disable adjustment of basal by sensitivityRatio when using dISF
basal = profile.current_basal * overrideFactor * sensitivityRatio;
basal = round_basal(basal, profile);
}
else if (dynISFenabled && profile.tddAdjBasal) {
basal = profile.current_basal * tdd24h_14d_Ratio * overrideFactor;
basal = round_basal(basal, profile);
if (average_total_data > 0) {
process.stderr.write("TDD-adjustment of basals activated, using tdd24h_14d_Ratio " + round(tdd24h_14d_Ratio,2) + ", TDD 24h = " + round(tdd_before,2) + "U, Weighted average TDD = " + round(weightedAverage,2) + "U, (Weight percentage = " + weightPercentage + "), Total data of TDDs (up to 14 days) average = " + round(average_total_data,2) + "U. " );
if (basal !== profile_current_basal * overrideFactor) {
process.stderr.write("Adjusting basal from " + profile_current_basal * overrideFactor + " U/h to " + basal + " U/h; ");
} else { process.stderr.write("Basal unchanged: " + basal + " U/h; "); }
}
}
// Conversely, adjust BG target based on autosens ratio if no temp target is running
// adjust min, max, and target BG for sensitivity, such that 50% increase in ISF raises target from 100 to 120
if (profile.temptargetSet) {
//process.stderr.write("Temp Target set, not adjusting with autosens; ");
} else if (typeof autosens_data !== 'undefined' && autosens_data) {
if ( profile.sensitivity_raises_target && autosens_data.ratio < 1 || profile.resistance_lowers_target && autosens_data.ratio > 1 ) {
// with a target of 100, default 0.7-1.2 autosens min/max range would allow a 93-117 target range
min_bg = round((min_bg - 60) / autosens_data.ratio) + 60;
max_bg = round((max_bg - 60) / autosens_data.ratio) + 60;
var new_target_bg = round((target_bg - 60) / autosens_data.ratio) + 60;
// don't allow target_bg below 80
new_target_bg = Math.max(80, new_target_bg);
if (target_bg === new_target_bg) {
process.stderr.write("target_bg unchanged: " + convert_bg(new_target_bg, profile) + "; ");
} else {
process.stderr.write("target_bg from "+ convert_bg(new_target_bg, profile) + " to " + convert_bg(new_target_bg, profile) + "; ");
}
target_bg = new_target_bg;
}
}
// Display if differing in enacted box
var targetLog = convert_bg(target_bg, profile);
if (target_bg != profileTarget) {
if (overrideTarget !== 0 && overrideTarget !== 6 && overrideTarget !== target_bg) {
targetLog = convert_bg(profileTarget, profile) + "\u2192" + convert_bg(overrideTarget, profile) + "\u2192" + convert_bg(target_bg, profile);
} else {
targetLog = convert_bg(profileTarget, profile) + "\u2192" + convert_bg(target_bg, profile);
}