determine-basal.js 100 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054
  1. /*
  2. Determine Basal
  3. Released under MIT license. See the accompanying LICENSE.txt file for
  4. full terms and conditions
  5. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  6. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  7. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  8. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  9. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  10. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  11. THE SOFTWARE.
  12. */
  13. // Define various functions used later on, in the main function determine_basal() below
  14. var round_basal = require('../round-basal');
  15. // Rounds value to 'digits' decimal places
  16. function round(value, digits) {
  17. if (! digits) { digits = 0; }
  18. var scale = Math.pow(10, digits);
  19. return Math.round(value * scale) / scale;
  20. }
  21. // we expect BG to rise or fall at the rate of BGI,
  22. // adjusted by the rate at which BG would need to rise /
  23. // fall to get eventualBG to target over 2 hours
  24. function calculate_expected_delta(target_bg, eventual_bg, bgi) {
  25. // (hours * mins_per_hour) / 5 = how many 5 minute periods in 2h = 24
  26. var five_min_blocks = (2 * 60) / 5;
  27. var target_delta = target_bg - eventual_bg;
  28. return /* expectedDelta */ round(bgi + (target_delta / five_min_blocks), 1);
  29. }
  30. function convert_bg(value, profile)
  31. {
  32. if (profile.out_units === "mmol/L")
  33. {
  34. return round(value * 0.0555,1);
  35. }
  36. else
  37. {
  38. return Math.round(value);
  39. }
  40. }
  41. function enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_bg, oref_variables, time) {
  42. if (oref_variables.smbIsScheduledOff){
  43. /* Below logic is related to profile overrides which can disable SMBs or disable them for a scheduled window.
  44. * SMBs will be disabled from [start, end), such that if an SMB is scheduled to be disabled from 10 AM to 2 PM,
  45. * an SMB will not be allowed from 10:00:00 until 1:59:59.
  46. */
  47. let currentHour = new Date(time.getHours());
  48. let startTime = oref_variables.start;
  49. let endTime = oref_variables.end;
  50. if (startTime < endTime && (currentHour >= startTime && currentHour < endTime)) {
  51. console.error("SMB disabled: current time is in SMB disabled scheduled")
  52. return false
  53. } else if (startTime > endTime && (currentHour >= startTime || currentHour < endTime)) {
  54. console.error("SMB disabled: current time is in SMB disabled scheduled")
  55. return false
  56. } else if (startTime == 0 && endTime == 0) {
  57. console.error("SMB disabled: current time is in SMB disabled scheduled")
  58. return false;
  59. } else if (startTime == endTime && currentHour == startTime) {
  60. console.error("SMB disabled: current time is in SMB disabled scheduled")
  61. return false;
  62. }
  63. }
  64. // disable SMB when a high temptarget is set
  65. if (! microBolusAllowed) {
  66. console.error("SMB disabled (!microBolusAllowed)");
  67. return false;
  68. } else if (! profile.allowSMB_with_high_temptarget && profile.temptargetSet && target_bg > 100) {
  69. console.error("SMB disabled due to high temptarget of " + target_bg);
  70. return false;
  71. } else if (meal_data.bwFound === true && profile.A52_risk_enable === false) {
  72. console.error("SMB disabled due to Bolus Wizard activity in the last 6 hours.");
  73. return false;
  74. // Disable if invalid CGM reading (HIGH)
  75. } else if (bg == 400) {
  76. console.error("Invalid CGM (HIGH). SMBs disabled.");
  77. return false;
  78. }
  79. // enable SMB/UAM if always-on (unless previously disabled for high temptarget)
  80. if (profile.enableSMB_always === true) {
  81. if (meal_data.bwFound) {
  82. console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard");
  83. } else {
  84. console.error("SMB enabled due to enableSMB_always");
  85. }
  86. return true;
  87. }
  88. // enable SMB/UAM (if enabled in preferences) while we have COB
  89. if (profile.enableSMB_with_COB === true && meal_data.mealCOB) {
  90. if (meal_data.bwCarbs) {
  91. console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard");
  92. } else {
  93. console.error("SMB enabled for COB of " + meal_data.mealCOB);
  94. }
  95. return true;
  96. }
  97. // enable SMB/UAM (if enabled in preferences) for a full 6 hours after any carb entry
  98. // (6 hours is defined in carbWindow in lib/meal/total.js)
  99. if (profile.enableSMB_after_carbs === true && meal_data.carbs ) {
  100. if (meal_data.bwCarbs) {
  101. console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard");
  102. } else {
  103. console.error("SMB enabled for 6h after carb entry");
  104. }
  105. return true;
  106. }
  107. // enable SMB/UAM (if enabled in preferences) if a low temptarget is set
  108. if (profile.enableSMB_with_temptarget === true && (profile.temptargetSet && target_bg < 100)) {
  109. if (meal_data.bwFound) {
  110. console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard");
  111. } else {
  112. console.error("SMB enabled for temptarget of " + convert_bg(target_bg, profile));
  113. }
  114. return true;
  115. }
  116. // enable SMB if high bg is found
  117. if (profile.enableSMB_high_bg === true && high_bg !== null && bg >= high_bg) {
  118. console.error("Checking BG to see if High for SMB enablement.");
  119. console.error("Current BG", bg, " | High BG ", high_bg);
  120. if (meal_data.bwFound) {
  121. console.error("Warning: High BG SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard");
  122. } else {
  123. console.error("High BG detected. Enabling SMB.");
  124. }
  125. return true;
  126. }
  127. console.error("SMB disabled (no enableSMB preferences active or no condition satisfied)");
  128. return false;
  129. }
  130. 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) {
  131. var profileTarget = profile.min_bg;
  132. var overrideTarget = oref2_variables.overrideTarget;
  133. if (overrideTarget != 0 && overrideTarget != 6 && oref2_variables.useOverride && !profile.temptargetSet) {
  134. profileTarget = overrideTarget;
  135. }
  136. const smbIsOff = oref2_variables.smbIsOff;
  137. const advancedSettings = oref2_variables.advancedSettings;
  138. const isfAndCr = oref2_variables.isfAndCr;
  139. const isf = oref2_variables.isf;
  140. const cr_ = oref2_variables.cr;
  141. const smbIsScheduledOff = oref2_variables.smbIsScheduledOff;
  142. const start = oref2_variables.start;
  143. var end = oref2_variables.end;
  144. const smbMinutes = oref2_variables.smbMinutes;
  145. const uamMinutes = oref2_variables.uamMinutes;
  146. var dynISFenabled = preferences.useNewFormula
  147. var insulinForManualBolus = 0;
  148. var manualBolusErrorString = 0;
  149. var threshold = profileTarget;
  150. var systemTime = new Date();
  151. if (currentTime) {
  152. systemTime = new Date(currentTime);
  153. }
  154. // tdd past 24 hours
  155. var pumpData = 0;
  156. var logtdd = "";
  157. var logBasal = "";
  158. var logBolus = "";
  159. var logTempBasal = "";
  160. var dataLog = "";
  161. var logOutPut = "";
  162. var tddReason = "";
  163. var current = 0;
  164. var tdd = 0;
  165. var insulin = 0;
  166. var tempInsulin = 0;
  167. var bolusInsulin = 0;
  168. var scheduledBasalInsulin = 0;
  169. var quota = 0;
  170. const weightedAverage = oref2_variables.weightedAverage;
  171. var overrideFactor = 1;
  172. var sensitivity = profile.sens;
  173. var carbRatio = profile.carb_ratio;
  174. if (oref2_variables.useOverride) {
  175. overrideFactor = oref2_variables.overridePercentage / 100;
  176. if (isfAndCr) {
  177. sensitivity /= overrideFactor;
  178. carbRatio /= overrideFactor;
  179. } else {
  180. if (cr_) { carbRatio /= overrideFactor; }
  181. if (isf) { sensitivity /= overrideFactor; }
  182. }
  183. }
  184. const weightPercentage = profile.weightPercentage;
  185. const average_total_data = oref2_variables.average_total_data;
  186. function addTimeToDate(objDate, _hours) {
  187. var ms = objDate.getTime();
  188. var add_ms = _hours * 36e5;
  189. var newDateObj = new Date(ms + add_ms);
  190. return newDateObj;
  191. }
  192. function subtractTimeFromDate(date, hours_) {
  193. var ms_ = date.getTime();
  194. var add_ms_ = hours_ * 36e5;
  195. var new_date = new Date(ms_ - add_ms_);
  196. return new_date;
  197. }
  198. function accountForIncrements(insulin) {
  199. // If you have not set this to.0.1 in Trio settings, this will be set to 0.05 (Omnipods) in code.
  200. var minimalDose = profile.bolus_increment;
  201. if (minimalDose != 0.1) {
  202. minimalDose = 0.05;
  203. }
  204. var incrementsRaw = insulin / minimalDose;
  205. if (incrementsRaw >= 1) {
  206. var incrementsRounded = Math.floor(incrementsRaw);
  207. return round(incrementsRounded * minimalDose, 5);
  208. } else { return 0; }
  209. }
  210. function makeBaseString(base_timeStamp) {
  211. function addZero(i) {
  212. if (i < 10) { i = "0" + i }
  213. return i;
  214. }
  215. let hour = addZero(base_timeStamp.getHours());
  216. let minutes = addZero(base_timeStamp.getMinutes());
  217. let seconds = "00";
  218. let string = hour + ":" + minutes + ":" + seconds;
  219. return string;
  220. }
  221. function timeDifferenceOfString(string1, string2) {
  222. //Base time strings are in "00:00:00" format
  223. var time1 = new Date("1/1/1999 " + string1);
  224. var time2 = new Date("1/1/1999 " + string2);
  225. var ms1 = time1.getTime();
  226. var ms2 = time2.getTime();
  227. var difference = (ms1 - ms2) / 36e5;
  228. return difference;
  229. }
  230. // In case the autosens.min/max limits are reversed:
  231. const minLimitChris = Math.min(profile.autosens_min, profile.autosens_max);
  232. const maxLimitChris = Math.max(profile.autosens_min, profile.autosens_max);
  233. // Turn off when autosens.min = autosens.max
  234. if (maxLimitChris == minLimitChris || maxLimitChris < 1 || minLimitChris > 1) {
  235. dynISFenabled = false;
  236. console.log("Dynamic ISF disabled due to current autosens settings");
  237. }
  238. function calcScheduledBasalInsulin(lastRealTempTime, addedLastTempTime) {
  239. var totalInsulin = 0;
  240. var old = addedLastTempTime;
  241. var totalDuration = (lastRealTempTime - addedLastTempTime) / 36e5;
  242. var basDuration = 0;
  243. var totalDurationCheck = totalDuration;
  244. var durationCurrentSchedule = 0;
  245. do {
  246. if (totalDuration > 0) {
  247. var baseTime_ = makeBaseString(old);
  248. //Default basalrate in case none is found...
  249. var basalScheduledRate_ = basalprofile[0].rate;
  250. for (let m = 0; m < basalprofile.length; m++) {
  251. var timeToTest = basalprofile[m].start;
  252. if (baseTime_ == timeToTest) {
  253. if (m + 1 < basalprofile.length) {
  254. let end = basalprofile[m+1].start;
  255. let start = basalprofile[m].start;
  256. durationCurrentSchedule = timeDifferenceOfString(end, start);
  257. if (totalDuration >= durationCurrentSchedule) {
  258. basDuration = durationCurrentSchedule;
  259. } else if (totalDuration < durationCurrentSchedule) {
  260. basDuration = totalDuration;
  261. }
  262. }
  263. else if (m + 1 == basalprofile.length) {
  264. let end = basalprofile[0].start;
  265. let start = basalprofile[m].start;
  266. // First schedule is 00:00:00. Changed places of start and end here.
  267. durationCurrentSchedule = 24 - (timeDifferenceOfString(start, end));
  268. if (totalDuration >= durationCurrentSchedule) {
  269. basDuration = durationCurrentSchedule;
  270. } else if (totalDuration < durationCurrentSchedule) {
  271. basDuration = totalDuration;
  272. }
  273. }
  274. basalScheduledRate_ = basalprofile[m].rate;
  275. totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
  276. totalDuration -= basDuration;
  277. 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_);
  278. // Move clock to new date
  279. old = addTimeToDate(old, basDuration);
  280. }
  281. else if (baseTime_ > timeToTest) {
  282. if (m + 1 < basalprofile.length) {
  283. var timeToTest2 = basalprofile[m+1].start
  284. if (baseTime_ < timeToTest2) {
  285. // durationCurrentSchedule = timeDifferenceOfString(end, start);
  286. durationCurrentSchedule = timeDifferenceOfString(timeToTest2, baseTime_);
  287. if (totalDuration >= durationCurrentSchedule) {
  288. basDuration = durationCurrentSchedule;
  289. } else if (totalDuration < durationCurrentSchedule) {
  290. basDuration = totalDuration;
  291. }
  292. basalScheduledRate_ = basalprofile[m].rate;
  293. totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
  294. totalDuration -= basDuration;
  295. 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_);
  296. // Move clock to new date
  297. old = addTimeToDate(old, basDuration);
  298. }
  299. }
  300. else if (m == basalprofile.length - 1) {
  301. // let start = basalprofile[m].start;
  302. let start = baseTime_;
  303. // First schedule is 00:00:00. Changed places of start and end here.
  304. durationCurrentSchedule = timeDifferenceOfString("23:59:59", start);
  305. if (totalDuration >= durationCurrentSchedule) {
  306. basDuration = durationCurrentSchedule;
  307. } else if (totalDuration < durationCurrentSchedule) {
  308. basDuration = totalDuration;
  309. }
  310. basalScheduledRate_ = basalprofile[m].rate;
  311. totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
  312. totalDuration -= basDuration;
  313. 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_);
  314. // Move clock to new date
  315. old = addTimeToDate(old, basDuration);
  316. }
  317. }
  318. }
  319. }
  320. //totalDurationCheck to avoid infinite loop
  321. } while (totalDuration > 0 && totalDuration < totalDurationCheck);
  322. // amount of insulin according to pump basal rate schedules
  323. return totalInsulin;
  324. }
  325. // 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
  326. // 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.
  327. // AT basal rates are not used for this estimation, instead the basal rates in pump settings.
  328. // Check for empty pump history (new FAX loopers). If empty: don't use dynamic settings!
  329. if (!pumphistory.length) {
  330. console.log("Pumphistory is empty!");
  331. dynISFenabled = false;
  332. enableDynamicCR = false;
  333. } else if (dynISFenabled) {
  334. let phLastEntry = pumphistory.length - 1;
  335. var endDate = new Date(pumphistory[phLastEntry].timestamp);
  336. var startDate = new Date(pumphistory[0].timestamp);
  337. // If latest pump event is a temp basal
  338. if (pumphistory[0]._type == "TempBasalDuration") {
  339. startDate = new Date();
  340. }
  341. pumpData = (startDate - endDate) / 36e5;
  342. if (pumpData < 23.9 && pumpData > 21) {
  343. var missingHours = 24 - pumpData;
  344. // Makes new end date for a total time duration of exakt 24 hour.
  345. var endDate_ = subtractTimeFromDate(endDate, missingHours);
  346. // endDate - endDate_ = missingHours
  347. scheduledBasalInsulin = calcScheduledBasalInsulin(endDate, endDate_);
  348. 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. ";
  349. } else if (pumpData < 21) {
  350. dynISFenabled = false;
  351. enableDynamicCR = false;
  352. } else { dataLog = ""; }
  353. }
  354. // Calculate tdd ----------------------------------------------------------------------
  355. //Bolus:
  356. for (let i = 0; i < pumphistory.length; i++) {
  357. if (pumphistory[i]._type == "Bolus") {
  358. bolusInsulin += pumphistory[i].amount;
  359. }
  360. }
  361. // Temp basals:
  362. for (let j = 1; j < pumphistory.length; j++) {
  363. if (pumphistory[j]._type == "TempBasal" && pumphistory[j].rate > 0) {
  364. current = j;
  365. quota = pumphistory[j].rate;
  366. var duration = pumphistory[j-1]['duration (min)'] / 60;
  367. var origDur = duration;
  368. var pastTime = new Date(pumphistory[j-1].timestamp);
  369. var morePresentTime = new Date(pastTime);
  370. var substractTimeOfRewind = 0;
  371. // If temp basal hasn't yet ended, use now as end date for calculation
  372. do {
  373. j--;
  374. if (j == 0) {
  375. morePresentTime = new Date();
  376. break;
  377. }
  378. else if (pumphistory[j]._type == "TempBasal" || pumphistory[j]._type == "PumpSuspend") {
  379. morePresentTime = new Date(pumphistory[j].timestamp);
  380. break;
  381. }
  382. // During the time the Medtronic pumps are rewinded and primed, this duration of suspened insulin delivery needs to be accounted for.
  383. var pp = j-2;
  384. if (pp >= 0) {
  385. if (pumphistory[pp]._type == "Rewind") {
  386. let rewindTimestamp = pumphistory[pp].timestamp;
  387. // There can be several Prime events
  388. while (pp - 1 >= 0) {
  389. pp -= 1;
  390. if (pumphistory[pp]._type == "Prime") {
  391. substractTimeOfRewind = (pumphistory[pp].timestamp - rewindTimestamp) / 36e5;
  392. } else { break }
  393. }
  394. // If Medtronic user forgets to insert infusion set
  395. if (substractTimeOfRewind >= duration) {
  396. morePresentTime = new Date(rewindTimestamp);
  397. substractTimeOfRewind = 0;
  398. }
  399. }
  400. }
  401. } while (j > 0);
  402. var diff = (morePresentTime - pastTime) / 36e5;
  403. if (diff < origDur) {
  404. duration = diff;
  405. }
  406. insulin = quota * (duration - substractTimeOfRewind);
  407. tempInsulin += accountForIncrements(insulin);
  408. j = current;
  409. }
  410. }
  411. // Check and count for when basals are delivered with a scheduled basal rate.
  412. // 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.
  413. // 2. Check for temp basals that completes. This is for when disconnected from link/iphone, or when in open loop.
  414. // 3. Account for a punp suspension. This is for when pod screams or when MDT or pod is manually suspended.
  415. // 4. Account for a pump resume (in case pump/cgm is disconnected before next loop).
  416. // To do: are there more circumstances when scheduled basal rates are used? Do we need to care about "Prime" and "Rewind" with MDT pumps?
  417. //
  418. for (let k = 0; k < pumphistory.length; k++) {
  419. // Check for 0 temp basals with 0 min duration.
  420. insulin = 0;
  421. if (pumphistory[k]['duration (min)'] == 0 || pumphistory[k]._type == "PumpResume") {
  422. let time1 = new Date(pumphistory[k].timestamp);
  423. let time2 = new Date(time1);
  424. let l = k;
  425. do {
  426. if (l > 0) {
  427. --l;
  428. if (pumphistory[l]._type == "TempBasal") {
  429. time2 = new Date(pumphistory[l].timestamp);
  430. break;
  431. }
  432. }
  433. } while (l > 0);
  434. // duration of current scheduled basal in h
  435. let basDuration = (time2 - time1) / 36e5;
  436. if (basDuration > 0) {
  437. scheduledBasalInsulin += calcScheduledBasalInsulin(time2, time1);
  438. }
  439. }
  440. }
  441. // Check for temp basals that completes
  442. for (let n = pumphistory.length -1; n > 0; n--) {
  443. if (pumphistory[n]._type == "TempBasalDuration") {
  444. // duration in hours
  445. let oldBasalDuration = pumphistory[n]['duration (min)'] / 60;
  446. // time of old temp basal
  447. let oldTime = new Date(pumphistory[n].timestamp);
  448. var newTime = new Date(oldTime);
  449. let o = n;
  450. do {
  451. --o;
  452. if (o >= 0) {
  453. if (pumphistory[o]._type == "TempBasal" || pumphistory[o]._type == "PumpSuspend") {
  454. // time of next (new) temp basal or a pump suspension
  455. newTime = new Date(pumphistory[o].timestamp);
  456. break;
  457. }
  458. }
  459. } while (o > 0);
  460. // When latest temp basal is index 0 in pump history
  461. if (n == 0 && pumphistory[0]._type == "TempBasalDuration") {
  462. newTime = new Date();
  463. oldBasalDuration = pumphistory[n]['duration (min)'] / 60;
  464. }
  465. let tempBasalTimeDifference = (newTime - oldTime) / 36e5;
  466. let timeOfbasal = tempBasalTimeDifference - oldBasalDuration;
  467. // if duration of scheduled basal is more than 0
  468. if (timeOfbasal > 0) {
  469. // Timestamp after completed temp basal
  470. let timeOfScheduledBasal = addTimeToDate(oldTime, oldBasalDuration);
  471. scheduledBasalInsulin += calcScheduledBasalInsulin(newTime, timeOfScheduledBasal);
  472. }
  473. }
  474. }
  475. tdd = bolusInsulin + tempInsulin + scheduledBasalInsulin;
  476. var insulin_ = {
  477. TDD: round(tdd, 5),
  478. bolus: round(bolusInsulin, 5),
  479. temp_basal: round(tempInsulin, 5),
  480. scheduled_basal: round(scheduledBasalInsulin, 5)
  481. }
  482. if (pumpData > 21) {
  483. logBolus = ". Bolus insulin: " + bolusInsulin.toPrecision(5) + " U";
  484. logTempBasal = ". Temporary basal insulin: " + tempInsulin.toPrecision(5) + " U";
  485. logBasal = ". Insulin with scheduled basal rate: " + scheduledBasalInsulin.toPrecision(5) + " U";
  486. logtdd = " TDD past 24h is: " + tdd.toPrecision(5) + " U";
  487. logOutPut = dataLog + logtdd + logBolus + logTempBasal + logBasal;
  488. tddReason = ", TDD: " + round(tdd,2) + " U, " + round(bolusInsulin/tdd*100,0) + "% Bolus " + round((tempInsulin+scheduledBasalInsulin)/tdd*100,0) + "% Basal";
  489. } else { tddReason = ", TDD: Not enough pumpData (< 21h)"; }
  490. var tdd_before = tdd;
  491. // -------------------- END OF TDD ----------------------------------------------------
  492. // Dynamic ratios
  493. const BG = glucose_status.glucose;
  494. const useDynamicCR = preferences.enableDynamicCR;
  495. const adjustmentFactor = preferences.adjustmentFactor;
  496. const adjustmentFactorSigmoid = preferences.adjustmentFactorSigmoid;
  497. const enable_sigmoid = preferences.sigmoid;
  498. const currentMinTarget = profileTarget;
  499. var exerciseSetting = false;
  500. var log = "";
  501. var tdd24h_14d_Ratio = 1;
  502. var basal_ratio_log = "";
  503. if (average_total_data > 0) {
  504. tdd24h_14d_Ratio = weightedAverage / average_total_data;
  505. }
  506. // respect autosens_max/min for tdd24h_14d_Ratio, used to adjust basal similarly as autosens
  507. if (tdd24h_14d_Ratio > 1) {
  508. tdd24h_14d_Ratio = Math.min(tdd24h_14d_Ratio, profile.autosens_max);
  509. tdd24h_14d_Ratio = round(tdd24h_14d_Ratio,2);
  510. 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 + ")";
  511. }
  512. else if (tdd24h_14d_Ratio < 1) {
  513. tdd24h_14d_Ratio = Math.max(tdd24h_14d_Ratio, profile.autosens_min);
  514. tdd24h_14d_Ratio = round(tdd24h_14d_Ratio,2);
  515. 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 + ")";
  516. }
  517. else {
  518. basal_ratio_log = "Basal adjusted with a 24 hour to total average (up to 14 days of data) TDD ratio: " + tdd24h_14d_Ratio;
  519. }
  520. basal_ratio_log = ", Basal ratio: " + tdd24h_14d_Ratio;
  521. // One of two exercise settings (they share the same purpose)
  522. if (profile.high_temptarget_raises_sensitivity || profile.exercise_mode) {
  523. exerciseSetting = true;
  524. }
  525. // Turn off Chris' formula when using a temp target >= 118 (6.5 mol/l) and if an exercise setting is enabled.
  526. if (currentMinTarget >= 118 && exerciseSetting) {
  527. dynISFenabled = false;
  528. log = "Dynamic ISF temporarily off due to a high temp target/exercising. Current min target: " + currentMinTarget;
  529. }
  530. var startLog = ", Dynamic ratios log: ";
  531. var afLog = ", AF: " + (enable_sigmoid ? adjustmentFactorSigmoid : adjustmentFactor);
  532. var bgLog = "BG: " + BG + " mg/dl (" + (BG * 0.0555).toPrecision(2) + " mmol/l)";
  533. var formula = "";
  534. var weightLog = "";
  535. // Insulin curve
  536. const curve = preferences.curve;
  537. const ipt = profile.insulinPeakTime;
  538. const ucpk = preferences.useCustomPeakTime;
  539. var insulinFactor = 55; // deafult (120-65)
  540. var insulinPA = 65; // default (Novorapid/Novolog)
  541. switch (curve) {
  542. case "rapid-acting":
  543. insulinPA = 65;
  544. break;
  545. case "ultra-rapid":
  546. insulinPA = 50;
  547. break;
  548. }
  549. if (ucpk) {
  550. insulinFactor = 120 - ipt;
  551. console.log("Custom insulinpeakTime set to :" + ipt + ", insulinFactor: " + insulinFactor);
  552. } else {
  553. insulinFactor = 120 - insulinPA;
  554. console.log("insulinFactor set to : " + insulinFactor);
  555. }
  556. // Use weighted TDD average
  557. tdd_before = tdd;
  558. if (weightPercentage < 1 && weightedAverage > 1) {
  559. tdd = weightedAverage;
  560. console.log("Using weighted TDD average: " + round(tdd,2) + " U, instead of past 24 h (" + round(tdd_before,2) + " U), weight: " + weightPercentage);
  561. weightLog = ", Weighted TDD: " + round(tdd,2) + " U";
  562. }
  563. // Modified Chris Wilson's' formula with added adjustmentFactor for tuning and use of the autosens.ratio:
  564. // var newRatio = profile.sens * adjustmentFactor * tdd * BG / 277700;
  565. //
  566. // New logarithmic formula : var newRatio = profile.sens * adjustmentFactor * tdd * ln(( BG/insulinFactor) + 1 )) / 1800
  567. //
  568. var sigmoidLog = ""
  569. if (dynISFenabled) {
  570. // Logarithmic
  571. if (!enable_sigmoid) {
  572. var newRatio = sensitivity * adjustmentFactor * tdd * Math.log(BG/insulinFactor+1) / 1800;
  573. formula = ", Logarithmic formula";
  574. }
  575. // Sigmoid
  576. else {
  577. const as_min = minLimitChris;
  578. const autosens_interval = maxLimitChris - as_min;
  579. //Blood glucose deviation from set target (the lower BG target) converted to mmol/l to fit current formula.
  580. const bg_dev = (BG - profileTarget) * 0.0555;
  581. // Account for TDD of insulin. Compare last 2 hours with total data (up to 14 days)
  582. var tdd_factor = tdd24h_14d_Ratio; // weighted average TDD / total data average TDD
  583. var max_minus_one = maxLimitChris - 1;
  584. // Avoid division by 0
  585. if (maxLimitChris == 1) {
  586. max_minus_one = maxLimitChris + 0.01 - 1;
  587. }
  588. //Makes sigmoid factor(y) = 1 when BG deviation(x) = 0.
  589. const fix_offset = (Math.log10(1/max_minus_one-as_min/max_minus_one) / Math.log10(Math.E));
  590. //Exponent used in sigmoid formula
  591. const exponent = bg_dev * adjustmentFactorSigmoid * tdd_factor + fix_offset;
  592. // The sigmoid function
  593. const sigmoid_factor = autosens_interval / (1 + Math.exp(-exponent)) + as_min;
  594. newRatio = sigmoid_factor;
  595. formula = ", Sigmoid function";
  596. // Dynamic CR will be processed further down
  597. }
  598. }
  599. var cr = carbRatio;
  600. const cr_before = round(carbRatio, 1);
  601. var log_isfCR = "";
  602. var limitLog = "";
  603. if (dynISFenabled && tdd > 0) {
  604. log_isfCR = ", Dynamic ISF/CR: On/";
  605. // Respect autosens.max and autosens.min limitLogs
  606. if (newRatio > maxLimitChris) {
  607. log = ", Dynamic ISF limited by autosens_max setting: " + maxLimitChris + " (" + round(newRatio,2) + "), ";
  608. limitLog = ", Autosens/Dynamic Limit: " + maxLimitChris + " (" + round(newRatio,2) + ")";
  609. newRatio = maxLimitChris;
  610. } else if (newRatio < minLimitChris) {
  611. log = ", Dynamic ISF limited by autosens_min setting: " + minLimitChris + " (" + round(newRatio,2) + "). ";
  612. limitLog = ", Autosens/Dynamic Limit: " + minLimitChris + " (" + round(newRatio,2) + ")";
  613. newRatio = minLimitChris;
  614. }
  615. // Dynamic CR (Test)
  616. if (useDynamicCR) {
  617. log_isfCR += "On";
  618. var dynCR = newRatio;
  619. /*
  620. // Lessen the ratio used by half, if newRatio > 1.
  621. if (newRatio > 1) {
  622. dynCR = (newRatio - 1) / 2 + 1;
  623. }
  624. cr = round(cr/dynCR, 2);
  625. var logCR = " CR: " + cr + " g/U";
  626. carbRatio = cr;
  627. */
  628. carbRatio /= dynCR;
  629. var logCR = ". New Dynamic CR: " + round(carbRatio, 1) + " g/U";
  630. } else {
  631. logCR = " CR: " + cr + " g/U";
  632. log_isfCR += "Off";
  633. }
  634. const isf = sensitivity / newRatio;
  635. // Set the new ratio
  636. autosens_data.ratio = newRatio;
  637. 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);
  638. if (!enable_sigmoid) {
  639. 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)";
  640. } else { log += sigmoidLog }
  641. logOutPut += startLog + bgLog + afLog + formula + log + log_isfCR + logCR + weightLog;
  642. } else { logOutPut += startLog + "Dynamic Settings disabled"; }
  643. console.log(logOutPut);
  644. if (!dynISFenabled && !useDynamicCR) {
  645. tddReason += "";
  646. } else if (dynISFenabled && profile.tddAdjBasal) {
  647. tddReason += log_isfCR + formula + limitLog + afLog + basal_ratio_log;
  648. }
  649. else if (dynISFenabled && !profile.tddAdjBasal) { tddReason += log_isfCR + formula + limitLog + afLog; }
  650. if (0.5 !== profile.smb_delivery_ratio) {
  651. tddReason += ", SMB Ratio: " + Math.min(profile.smb_delivery_ratio, 1);
  652. }
  653. // Not confident but something like this in iAPS v3.0.3
  654. if (middleWare !== "" && middleWare !== "Nothing changed"){
  655. tddReason += ", Middleware: " + middleWare;
  656. }
  657. // --------------- END OF DYNAMIC RATIOS CALCULATION ------ A FEW LINES ADDED ALSO AT LINE NR 1136 and 1178 ------------------------------------------------
  658. // Set variables required for evaluating error conditions
  659. var rT = {}; //short for requestedTemp
  660. var deliverAt = new Date(systemTime);
  661. if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') {
  662. rT.error ='Error: could not get current basal rate';
  663. return rT;
  664. }
  665. var profile_current_basal = round_basal(profile.current_basal, profile) * overrideFactor;
  666. var basal = profile_current_basal;
  667. // Print Current Override factor, if any
  668. if (oref2_variables.useOverride) {
  669. if (oref2_variables.duration == 0) {
  670. console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Duration: " + "Enabled indefinitely");
  671. } else
  672. console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Expires in: " + oref2_variables.duration + " min.");
  673. }
  674. var bgTime = new Date(glucose_status.date);
  675. var minAgo = round( (systemTime - bgTime) / 60 / 1000 ,1);
  676. var bg = glucose_status.glucose;
  677. var noise = glucose_status.noise;
  678. // Prep various delta variables.
  679. var tick;
  680. if (glucose_status.delta > -0.5) {
  681. tick = "+" + round(glucose_status.delta,0);
  682. } else {
  683. tick = round(glucose_status.delta,0);
  684. }
  685. //var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta);
  686. var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta);
  687. var minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta);
  688. var maxDelta = Math.max(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta);
  689. // Cancel high temps (and replace with neutral) or shorten long zero temps for various error conditions
  690. // 38 is an xDrip error state that usually indicates sensor failure
  691. // all other BG values between 11 and 37 mg/dL reflect non-error-code BG values, so we should zero temp for those
  692. // First, print out different explanations for each different error condition
  693. if (bg <= 10 || bg === 38 || noise >= 3) { //Dexcom is in ??? mode or calibrating, or xDrip reports high noise
  694. rT.reason = "CGM is calibrating, in ??? state, or noise is high";
  695. }
  696. var tooflat=false;
  697. 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) {
  698. if (glucose_status.device == "fakecgm") {
  699. 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");
  700. console.error("Simulator mode detected (" + glucose_status.device + "): continuing anyway");
  701. } else if (bg != 400) {
  702. tooflat=true;
  703. }
  704. }
  705. if (minAgo > 12 || minAgo < -5) { // Dexcom data is too old, or way in the future
  706. 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;
  707. // if BG is too old/noisy, or is completely unchanging, cancel any high temps and shorten any long zero temps
  708. } else if ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 && bg != 400 ) {
  709. if ( glucose_status.last_cal && glucose_status.last_cal < 3 ) {
  710. rT.reason = "CGM was just calibrated";
  711. } else {
  712. 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";
  713. }
  714. }
  715. if (bg != 400) {
  716. if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 ) ) {
  717. if (currenttemp.rate >= basal) { // high temp is running
  718. rT.reason += ". Canceling high temp basal of " + currenttemp.rate;
  719. rT.deliverAt = deliverAt;
  720. rT.temp = 'absolute';
  721. rT.duration = 0;
  722. rT.rate = 0;
  723. return rT;
  724. // don't use setTempBasal(), as it has logic that allows <120% high temps to continue running
  725. //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
  726. } else if ( currenttemp.rate === 0 && currenttemp.duration > 30 ) { //shorten long zero temps to 30m
  727. rT.reason += ". Shortening " + currenttemp.duration + "m long zero temp to 30m. ";
  728. rT.deliverAt = deliverAt;
  729. rT.temp = 'absolute';
  730. rT.duration = 30;
  731. rT.rate = 0;
  732. return rT;
  733. // don't use setTempBasal(), as it has logic that allows long zero temps to continue running
  734. //return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp);
  735. } else { //do nothing.
  736. rT.reason += ". Temp " + currenttemp.rate + " <= current basal " + basal + "U/hr; doing nothing. ";
  737. return rT;
  738. }
  739. }
  740. }
  741. // Get configured target, and return if unable to do so.
  742. // This should occur after checking that we're not in one of the CGM-data-related error conditions handled above,
  743. // and before using target_bg to adjust sensitivityRatio below.
  744. var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver
  745. // if min and max are set, then set target to their average
  746. var target_bg;
  747. var min_bg;
  748. var max_bg;
  749. var high_bg;
  750. if (typeof profileTarget !== 'undefined') {
  751. min_bg = profileTarget;
  752. }
  753. if (typeof profile.max_bg !== 'undefined') {
  754. max_bg = profileTarget;
  755. }
  756. if (typeof profile.enableSMB_high_bg_target !== 'undefined') {
  757. high_bg = profile.enableSMB_high_bg_target;
  758. }
  759. if (typeof profileTarget !== 'undefined') {
  760. target_bg = profileTarget;
  761. } else {
  762. rT.error ='Error: could not determine target_bg. ';
  763. return rT;
  764. }
  765. // Calculate sensitivityRatio based on temp targets, if applicable, or using the value calculated by autosens
  766. // var sensitivityRatio;
  767. var high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity;
  768. var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled target (which might change)
  769. var halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%)
  770. // 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limitLoged to autosens_max (1.2x by default)
  771. //if ( profile.half_basal_exercise_target ) {
  772. halfBasalTarget = profile.half_basal_exercise_target;
  773. //}
  774. if ( high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget ||
  775. profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget ) {
  776. // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44
  777. // 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
  778. //sensitivityRatio = 2/(2+(target_bg-normalTarget)/40);
  779. var c = halfBasalTarget - normalTarget;
  780. // getting multiplication less or equal to 0 means that we have a really low target with a really low halfBasalTarget
  781. // with low TT and lowTTlowersSensitivity we need autosens_max as a value
  782. // we use multiplication instead of the division to avoid "division by zero error"
  783. if (c * (c + target_bg-normalTarget) <= 0.0) {
  784. sensitivityRatio = profile.autosens_max;
  785. }
  786. else {
  787. sensitivityRatio = c/(c+target_bg-normalTarget);
  788. }
  789. // limit sensitivityRatio to profile.autosens_max (1.2x by default)
  790. sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max);
  791. sensitivityRatio = round(sensitivityRatio,2);
  792. process.stderr.write("Sensitivity ratio set to "+sensitivityRatio+" based on temp target of "+target_bg+"; ");
  793. }
  794. else if (typeof autosens_data !== 'undefined' && autosens_data) {
  795. sensitivityRatio = autosens_data.ratio;
  796. // Override Profile.Target
  797. if (overrideTarget !== 0 && overrideTarget !== 6 && overrideTarget !== profile.min_bg && !profile.temptargetSet) {
  798. target_bg = overrideTarget;
  799. console.log("Current Override Profile Target: " + convert_bg(overrideTarget, profile) + " " + profile.out_units);
  800. }
  801. process.stderr.write("Autosens ratio: "+sensitivityRatio+"; ");
  802. }
  803. // Increase the dynamic ratio when using a low temp target
  804. if (profile.temptargetSet && target_bg < normalTarget && dynISFenabled && BG >= target_bg) {
  805. if (sensitivityRatio < newRatio) {
  806. autosens_data.ratio = newRatio * (normalTarget/target_bg);
  807. //Use autosesns.max limit
  808. autosens_data.ratio = Math.min(autosens_data.ratio, profile.autosens_max);
  809. sensitivityRatio = round(autosens_data.ratio, 2);
  810. console.log("Dynamic ratio increased from " + round(newRatio, 2) + " to " + round(autosens_data.ratio,2) + " due to a low temp target (" + target_bg + ").");
  811. }
  812. }
  813. if (sensitivityRatio && !dynISFenabled) { // Only enable adjustment of basal by sensitivityRatio when not using dISF
  814. basal = profile.current_basal * overrideFactor * sensitivityRatio;
  815. basal = round_basal(basal, profile);
  816. if (basal !== profile_current_basal) {
  817. process.stderr.write("Adjusting basal from "+profile_current_basal+" to "+basal+"; ");
  818. } else {
  819. process.stderr.write("Basal unchanged: "+basal+"; ");
  820. }
  821. }
  822. else if (dynISFenabled && profile.tddAdjBasal) {
  823. basal = profile.current_basal * tdd24h_14d_Ratio * overrideFactor;
  824. basal = round_basal(basal, profile);
  825. if (average_total_data > 0) {
  826. 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. " );
  827. if (basal !== profile_current_basal * overrideFactor) {
  828. process.stderr.write("Adjusting basal from " + profile_current_basal * overrideFactor + " U/h to " + basal + " U/h; ");
  829. } else { process.stderr.write("Basal unchanged: " + basal + " U/h; "); }
  830. }
  831. }
  832. // Conversely, adjust BG target based on autosens ratio if no temp target is running
  833. // adjust min, max, and target BG for sensitivity, such that 50% increase in ISF raises target from 100 to 120
  834. if (profile.temptargetSet) {
  835. //process.stderr.write("Temp Target set, not adjusting with autosens; ");
  836. } else if (typeof autosens_data !== 'undefined' && autosens_data) {
  837. if ( profile.sensitivity_raises_target && autosens_data.ratio < 1 || profile.resistance_lowers_target && autosens_data.ratio > 1 ) {
  838. // with a target of 100, default 0.7-1.2 autosens min/max range would allow a 93-117 target range
  839. min_bg = round((min_bg - 60) / autosens_data.ratio) + 60;
  840. max_bg = round((max_bg - 60) / autosens_data.ratio) + 60;
  841. var new_target_bg = round((target_bg - 60) / autosens_data.ratio) + 60;
  842. // don't allow target_bg below 80
  843. new_target_bg = Math.max(80, new_target_bg);
  844. if (target_bg === new_target_bg) {
  845. process.stderr.write("target_bg unchanged: " + convert_bg(new_target_bg, profile) + "; ");
  846. } else {
  847. process.stderr.write("target_bg from "+ convert_bg(new_target_bg, profile) + " to " + convert_bg(new_target_bg, profile) + "; ");
  848. }
  849. target_bg = new_target_bg;
  850. }
  851. }
  852. // Display if differing in enacted box
  853. var targetLog = convert_bg(target_bg, profile);
  854. if (target_bg != profileTarget) {
  855. if (overrideTarget !== 0 && overrideTarget !== 6 && overrideTarget !== target_bg) {
  856. targetLog = convert_bg(profileTarget, profile) + "\u2192" + convert_bg(overrideTarget, profile) + "\u2192" + convert_bg(target_bg, profile);
  857. } else {
  858. targetLog = convert_bg(profileTarget, profile) + "\u2192" + convert_bg(target_bg, profile);
  859. }
  860. }
  861. // Raise target for noisy / raw CGM data.
  862. var adjustedMinBG = 200;
  863. var adjustedTargetBG = 200;
  864. var adjustedMaxBG = 200;
  865. if (glucose_status.noise >= 2) {
  866. // increase target at least 10% (default 30%) for raw / noisy data
  867. var noisyCGMTargetMultiplier = Math.max( 1.1, profile.noisyCGMTargetMultiplier );
  868. // don't allow maxRaw above 250
  869. var maxRaw = Math.min( 250, profile.maxRaw );
  870. adjustedMinBG = round(Math.min(200, min_bg * noisyCGMTargetMultiplier ));
  871. adjustedTargetBG = round(Math.min(200, target_bg * noisyCGMTargetMultiplier ));
  872. adjustedMaxBG = round(Math.min(200, max_bg * noisyCGMTargetMultiplier ));
  873. process.stderr.write("Raising target_bg for noisy / raw CGM data, from " + convert_bg(new_target_bg, profile) + " to " + convert_bg(adjustedTargetBG, profile) + "; ");
  874. min_bg = adjustedMinBG;
  875. target_bg = adjustedTargetBG;
  876. max_bg = adjustedMaxBG;
  877. }
  878. // min_bg thresholds: 80->60, 90->65, 100->70, 110->75, 120->80
  879. threshold = min_bg - 0.5*(min_bg-40);
  880. // Set threshold to the user's setting, as long as it's between 60-120 and above the default calculated threshold
  881. threshold = Math.min(Math.max(profile.threshold_setting, threshold, 60), 120);
  882. console.error(`Threshold set to ${convert_bg(threshold, profile)}`);
  883. // If iob_data or its required properties are missing, return.
  884. // This has to be checked after checking that we're not in one of the CGM-data-related error conditions handled above,
  885. // and before attempting to use iob_data below.
  886. // Adjust ISF based on sensitivityRatio
  887. var isfreason = ""
  888. var profile_sens = round(sensitivity,1);
  889. var sens = sensitivity;
  890. if (typeof autosens_data !== 'undefined' && autosens_data) {
  891. sens = sensitivity / sensitivityRatio;
  892. sens = round(sens, 1);
  893. if (sens !== sensitivity) {
  894. process.stderr.write("ISF from "+ convert_bg(sensitivity,profile) +" to " + convert_bg(sens,profile));
  895. } else {
  896. process.stderr.write("ISF unchanged: "+ convert_bg(sens,profile));
  897. }
  898. //process.stderr.write(" (autosens ratio "+sensitivityRatio+")");
  899. isfreason += "Autosens ratio: " + round(sensitivityRatio, 2) + ", ISF: " + convert_bg(sensitivity,profile) + "\u2192" + convert_bg(sens,profile);
  900. }
  901. console.error("CR:" + carbRatio);
  902. if (typeof iob_data === 'undefined' ) {
  903. rT.error ='Error: iob_data undefined. ';
  904. return rT;
  905. }
  906. var iobArray = iob_data;
  907. if (typeof(iob_data.length) && iob_data.length > 1) {
  908. iob_data = iobArray[0];
  909. }
  910. if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' ) {
  911. rT.error ='Error: iob_data missing some property. ';
  912. return rT;
  913. }
  914. // Compare currenttemp to iob_data.lastTemp and cancel temp if they don't match, as a safety check
  915. // This should occur after checking that we're not in one of the CGM-data-related error conditions handled above,
  916. // and before returning (doing nothing) below if eventualBG is undefined.
  917. var lastTempAge;
  918. if (typeof iob_data.lastTemp !== 'undefined' ) {
  919. lastTempAge = round(( new Date(systemTime).getTime() - iob_data.lastTemp.date ) / 60000); // in minutes
  920. } else {
  921. lastTempAge = 0;
  922. }
  923. //console.error("currenttemp:",currenttemp,"lastTemp:",JSON.stringify(iob_data.lastTemp),"lastTempAge:",lastTempAge,"m");
  924. var tempModulus = (lastTempAge + currenttemp.duration) % 30;
  925. console.error("currenttemp:" + currenttemp.rate + " lastTempAge:" + lastTempAge + "m, tempModulus:" + tempModulus + "m");
  926. rT.temp = 'absolute';
  927. rT.deliverAt = deliverAt;
  928. if ( microBolusAllowed && currenttemp && iob_data.lastTemp && currenttemp.rate !== iob_data.lastTemp.rate && lastTempAge > 10 && currenttemp.duration ) {
  929. rT.reason = "Warning: currenttemp rate " + currenttemp.rate + " != lastTemp rate " + iob_data.lastTemp.rate + " from pumphistory; canceling temp"; // reason.conclusion started
  930. return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp);
  931. }
  932. if ( currenttemp && iob_data.lastTemp && currenttemp.duration > 0 ) {
  933. //console.error(lastTempAge, round(iob_data.lastTemp.duration,1), round(lastTempAge - iob_data.lastTemp.duration,1));
  934. var lastTempEnded = lastTempAge - iob_data.lastTemp.duration;
  935. if ( lastTempEnded > 5 && lastTempAge > 10 ) {
  936. rT.reason = "Warning: currenttemp running but lastTemp from pumphistory ended " + lastTempEnded + "m ago; canceling temp"; // reason.conclusion started
  937. //console.error(currenttemp, round(iob_data.lastTemp,1), round(lastTempAge,1));
  938. return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp);
  939. }
  940. }
  941. // Calculate BGI, deviation, and eventualBG.
  942. // This has to happen after we obtain iob_data
  943. //calculate BG impact: the amount BG "should" be rising or falling based on insulin activity alone
  944. var bgi = round(( -iob_data.activity * sens * 5 ), 2);
  945. // project deviations for 30 minutes
  946. var deviation = round( 30 / 5 * ( minDelta - bgi ) );
  947. // don't overreact to a big negative delta: use minAvgDelta if deviation is negative
  948. if (deviation < 0) {
  949. deviation = round( (30 / 5) * ( minAvgDelta - bgi ) );
  950. // and if deviation is still negative, use long_avgdelta
  951. if (deviation < 0) {
  952. deviation = round( (30 / 5) * ( glucose_status.long_avgdelta - bgi ) );
  953. }
  954. }
  955. // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity
  956. var naive_eventualBG = bg;
  957. if (iob_data.iob > 0) {
  958. naive_eventualBG = round( bg - (iob_data.iob * sens) );
  959. } else { // if IOB is negative, be more conservative and use the lower of sens, profile.sens
  960. naive_eventualBG = round( bg - (iob_data.iob * Math.min(sens, sensitivity) ) );
  961. }
  962. // and adjust it for the deviation above
  963. var eventualBG = naive_eventualBG + deviation;
  964. if (typeof eventualBG === 'undefined' || isNaN(eventualBG)) {
  965. rT.error ='Error: could not calculate eventualBG. Sensitivity: ' + sens + ' Deviation: ' + deviation;
  966. return rT;
  967. }
  968. var expectedDelta = calculate_expected_delta(target_bg, eventualBG, bgi);
  969. var minPredBG;
  970. var minGuardBG;
  971. //console.error(reservoir_data);
  972. // Initialize rT (requestedTemp) object. Has to be done after eventualBG is calculated.
  973. rT = {
  974. 'temp': 'absolute'
  975. , 'bg': bg
  976. , 'tick': tick
  977. , 'eventualBG': eventualBG
  978. , 'insulinReq': 0
  979. , 'reservoir' : reservoir_data // The expected reservoir volume at which to deliver the microbolus (the reservoir volume from right before the last pumphistory run)
  980. , 'deliverAt' : deliverAt // The time at which the microbolus should be delivered
  981. , 'sensitivityRatio' : sensitivityRatio
  982. , 'CR' : round(carbRatio, 1)
  983. , 'TDD': tdd_before
  984. , 'insulin': insulin_
  985. , 'current_target': target_bg
  986. , 'insulinForManualBolus': insulinForManualBolus
  987. , 'manualBolusErrorString': manualBolusErrorString
  988. , 'minDelta': minDelta
  989. , 'expectedDelta': expectedDelta
  990. , 'minGuardBG': minGuardBG
  991. , 'minPredBG': minPredBG
  992. , 'threshold': convert_bg(threshold, profile)
  993. };
  994. // Generate predicted future BGs based on IOB, COB, and current absorption rate
  995. // Initialize and calculate variables used for predicting BGs
  996. var COBpredBGs = [];
  997. var IOBpredBGs = [];
  998. var UAMpredBGs = [];
  999. var ZTpredBGs = [];
  1000. COBpredBGs.push(bg);
  1001. IOBpredBGs.push(bg);
  1002. ZTpredBGs.push(bg);
  1003. UAMpredBGs.push(bg);
  1004. let enableSMB = false;
  1005. // If SMB is always off, don't bother checking if we should enable SMBs
  1006. if (smbIsOff) {
  1007. console.error("SMBs are always off.");
  1008. enableSMB = false;
  1009. } else {
  1010. enableSMB = enable_smb(
  1011. profile,
  1012. microBolusAllowed,
  1013. meal_data,
  1014. bg,
  1015. target_bg,
  1016. high_bg,
  1017. oref2_variables,
  1018. systemTime
  1019. );
  1020. }
  1021. var enableUAM = (profile.enableUAM);
  1022. //console.error(meal_data);
  1023. // carb impact and duration are 0 unless changed below
  1024. var ci = 0;
  1025. var cid = 0;
  1026. // calculate current carb absorption rate, and how long to absorb all carbs
  1027. // CI = current carb impact on BG in mg/dL/5m
  1028. ci = round((minDelta - bgi),1);
  1029. var uci = round((minDelta - bgi),1);
  1030. // ISF (mg/dL/U) / CR (g/U) = CSF (mg/dL/g)
  1031. // use autosens-adjusted sens to counteract autosens meal insulin dosing adjustments so that
  1032. // autotuned CR is still in effect even when basals and ISF are being adjusted by TT or autosens
  1033. // this avoids overdosing insulin for large meals when low temp targets are active
  1034. csf = sens / carbRatio;
  1035. console.error("profile.sens:" + convert_bg(sensitivity,profile) + ", sens:" + convert_bg(sens,profile) + ", CSF:" + round(csf,1));
  1036. var maxCarbAbsorptionRate = 30; // g/h; maximum rate to assume carbs will absorb if no CI observed
  1037. // limitLog Carb Impact to maxCarbAbsorptionRate * csf in mg/dL per 5m
  1038. var maxCI = round(maxCarbAbsorptionRate*csf*5/60,1);
  1039. if (ci > maxCI) {
  1040. console.error("Limiting carb impact from " + ci + " to " + maxCI + "mg/dL/5m (" + maxCarbAbsorptionRate + "g/h)");
  1041. ci = maxCI;
  1042. }
  1043. var remainingCATimeMin = 3; // h; minimum duration of expected not-yet-observed carb absorption
  1044. // adjust remainingCATime (instead of CR) for autosens if sensitivityRatio defined
  1045. if (sensitivityRatio) {
  1046. remainingCATimeMin = remainingCATimeMin / sensitivityRatio;
  1047. }
  1048. // 20 g/h means that anything <= 60g will get a remainingCATimeMin, 80g will get 4h, and 120g 6h
  1049. // when actual absorption ramps up it will take over from remainingCATime
  1050. var assumedCarbAbsorptionRate = 20; // g/h; maximum rate to assume carbs will absorb if no CI observed
  1051. var remainingCATime = remainingCATimeMin;
  1052. if (meal_data.carbs) {
  1053. // if carbs * assumedCarbAbsorptionRate > remainingCATimeMin, raise it
  1054. // so <= 90g is assumed to take 3h, and 120g=4h
  1055. remainingCATimeMin = Math.max(remainingCATimeMin, meal_data.mealCOB/assumedCarbAbsorptionRate);
  1056. var lastCarbAge = round(( new Date(systemTime).getTime() - meal_data.lastCarbTime ) / 60000);
  1057. //console.error(meal_data.lastCarbTime, lastCarbAge);
  1058. var fractionCOBAbsorbed = ( meal_data.carbs - meal_data.mealCOB ) / meal_data.carbs;
  1059. // if the lastCarbTime was 1h ago, increase remainingCATime by 1.5 hours
  1060. remainingCATime = remainingCATimeMin + 1.5 * lastCarbAge/60;
  1061. remainingCATime = round(remainingCATime,1);
  1062. //console.error(fractionCOBAbsorbed, remainingCATimeAdjustment, remainingCATime)
  1063. console.error("Last carbs " + lastCarbAge + " minutes ago; remainingCATime:" + remainingCATime + "hours; " + round(fractionCOBAbsorbed*100, 1) + "% carbs absorbed");
  1064. }
  1065. // calculate the number of carbs absorbed over remainingCATime hours at current CI
  1066. // CI (mg/dL/5m) * (5m)/5 (m) * 60 (min/hr) * 4 (h) / 2 (linear decay factor) = total carb impact (mg/dL)
  1067. var totalCI = Math.max(0, ci / 5 * 60 * remainingCATime / 2);
  1068. // totalCI (mg/dL) / CSF (mg/dL/g) = total carbs absorbed (g)
  1069. var totalCA = totalCI / csf;
  1070. var remainingCarbsCap = 90; // default to 90
  1071. var remainingCarbsFraction = 1;
  1072. if (profile.remainingCarbsCap) { remainingCarbsCap = Math.min(90,profile.remainingCarbsCap); }
  1073. if (profile.remainingCarbsFraction) { remainingCarbsFraction = Math.min(1,profile.remainingCarbsFraction); }
  1074. var remainingCarbsIgnore = 1 - remainingCarbsFraction;
  1075. var remainingCarbs = Math.max(0, meal_data.mealCOB - totalCA - meal_data.carbs*remainingCarbsIgnore);
  1076. remainingCarbs = Math.min(remainingCarbsCap,remainingCarbs);
  1077. // assume remainingCarbs will absorb in a /\ shaped bilinear curve
  1078. // peaking at remainingCATime / 2 and ending at remainingCATime hours
  1079. // area of the /\ triangle is the same as a remainingCIpeak-height rectangle out to remainingCATime/2
  1080. // remainingCIpeak (mg/dL/5m) = remainingCarbs (g) * CSF (mg/dL/g) * 5 (m/5m) * 1h/60m / (remainingCATime/2) (h)
  1081. var remainingCIpeak = remainingCarbs * csf * 5 / 60 / (remainingCATime/2);
  1082. //console.error(profile.min_5m_carbimpact,ci,totalCI,totalCA,remainingCarbs,remainingCI,remainingCATime);
  1083. // calculate peak deviation in last hour, and slope from that to current deviation
  1084. var slopeFromMaxDeviation = round(meal_data.slopeFromMaxDeviation,2);
  1085. // calculate lowest deviation in last hour, and slope from that to current deviation
  1086. var slopeFromMinDeviation = round(meal_data.slopeFromMinDeviation,2);
  1087. // assume deviations will drop back down at least at 1/3 the rate they ramped up
  1088. var slopeFromDeviations = Math.min(slopeFromMaxDeviation,-slopeFromMinDeviation/3);
  1089. //console.error(slopeFromMaxDeviation);
  1090. //5m data points = g * (1U/10g) * (40mg/dL/1U) / (mg/dL/5m)
  1091. // duration (in 5m data points) = COB (g) * CSF (mg/dL/g) / ci (mg/dL/5m)
  1092. // limitLog cid to remainingCATime hours: the reset goes to remainingCI
  1093. var nfcid = 0;
  1094. if (ci === 0) {
  1095. // avoid divide by zero
  1096. cid = 0;
  1097. } else { cid = Math.min(remainingCATime*60/5/2,Math.max(0, meal_data.mealCOB * csf / ci )); }
  1098. // duration (hours) = duration (5m) * 5 / 60 * 2 (to account for linear decay)
  1099. console.error("Carb Impact:" + ci + "mg/dL per 5m; CI Duration:" + round(cid*5/60*2,1) + "hours; remaining CI (" + remainingCATime/2 + "h peak):" + round(remainingCIpeak,1) + "mg/dL per 5m");
  1100. var minIOBPredBG = 999;
  1101. var minCOBPredBG = 999;
  1102. var minUAMPredBG = 999;
  1103. //minGuardBG = bg;
  1104. var minCOBGuardBG = 999;
  1105. var minUAMGuardBG = 999;
  1106. var minIOBGuardBG = 999;
  1107. var minZTGuardBG = 999;
  1108. //var minPredBG;
  1109. var avgPredBG;
  1110. var IOBpredBG = eventualBG;
  1111. var maxIOBPredBG = bg;
  1112. var maxCOBPredBG = bg;
  1113. var maxUAMPredBG = bg;
  1114. var eventualPredBG = bg;
  1115. var lastIOBpredBG;
  1116. var lastCOBpredBG;
  1117. var lastUAMpredBG;
  1118. var lastZTpredBG;
  1119. var UAMduration = 0;
  1120. var remainingCItotal = 0;
  1121. var remainingCIs = [];
  1122. var predCIs = [];
  1123. try {
  1124. iobArray.forEach(function(iobTick) {
  1125. //console.error(iobTick);
  1126. var predBGI = round(( -iobTick.activity * sens * 5 ), 2);
  1127. var predZTBGI = round(( -iobTick.iobWithZeroTemp.activity * sens * 5 ), 2);
  1128. var ZTpredBG = naive_eventualBG;
  1129. // for IOBpredBGs, predicted deviation impact drops linearly from current deviation down to zero
  1130. // over 60 minutes (data points every 5m)
  1131. var predDev = ci * ( 1 - Math.min(1,IOBpredBGs.length/(60/5)) );
  1132. // Adding dynamic ISF in predictions for ZT and IOB. Modification from Tim Street's AAPS but with default as off:
  1133. switch(true) {
  1134. case dynISFenabled && !enable_sigmoid:
  1135. //IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + predBGI + predDev; // Adding dynamic ISF in predictions for UAM, ZT and IOB:
  1136. IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + (round(( -iobTick.activity * (1800 / ( tdd * adjustmentFactor * (Math.log((Math.max( IOBpredBGs[IOBpredBGs.length-1],39) / insulinFactor ) + 1 ) ) )) * 5 ),2)) + predDev;
  1137. //var ZTpredBG = ZTpredBGs[ZTpredBGs.length-1] + predZTBGI; // Adding dynamic ISF in predictions for UAM, ZT and IOB:
  1138. ZTpredBG = ZTpredBGs[ZTpredBGs.length-1] + (round(( -iobTick.iobWithZeroTemp.activity * (1800 / ( tdd * adjustmentFactor * (Math.log(( Math.max(ZTpredBGs[ZTpredBGs.length-1],39) / insulinFactor ) + 1 ) ) )) * 5 ), 2));
  1139. console.log("Dynamic ISF (Logarithmic Formula) )adjusted predictions for IOB and ZT: IOBpredBG: " + round(IOBpredBG,2) + " , ZTpredBG: " + round(ZTpredBG,2));
  1140. break;
  1141. default:
  1142. IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + predBGI + predDev;
  1143. // calculate predBGs with long zero temp without deviations
  1144. ZTpredBG = ZTpredBGs[ZTpredBGs.length-1] + predZTBGI;
  1145. }
  1146. // for COBpredBGs, predicted carb impact drops linearly from current carb impact down to zero
  1147. // eventually accounting for all carbs (if they can be absorbed over DIA)
  1148. var predCI = Math.max(0, Math.max(0,ci) * ( 1 - COBpredBGs.length/Math.max(cid*2,1) ) );
  1149. // if any carbs aren't absorbed after remainingCATime hours, assume they'll absorb in a /\ shaped
  1150. // bilinear curve peaking at remainingCIpeak at remainingCATime/2 hours (remainingCATime/2*12 * 5m)
  1151. // and ending at remainingCATime h (remainingCATime*12 * 5m intervals)
  1152. var intervals = Math.min( COBpredBGs.length, (remainingCATime*12)-COBpredBGs.length );
  1153. var remainingCI = Math.max(0, intervals / (remainingCATime/2*12) * remainingCIpeak );
  1154. remainingCItotal += predCI+remainingCI;
  1155. remainingCIs.push(round(remainingCI,0));
  1156. predCIs.push(round(predCI,0));
  1157. //process.stderr.write(round(predCI,1)+"+"+round(remainingCI,1)+" ");
  1158. COBpredBG = COBpredBGs[COBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predCI + remainingCI;
  1159. // for UAMpredBGs, predicted carb impact drops at slopeFromDeviations
  1160. // calculate predicted CI from UAM based on slopeFromDeviations
  1161. var predUCIslope = Math.max(0, uci + ( UAMpredBGs.length*slopeFromDeviations ) );
  1162. // if slopeFromDeviations is too flat, predicted deviation impact drops linearly from
  1163. // current deviation down to zero over 3h (data points every 5m)
  1164. var predUCImax = Math.max(0, uci * ( 1 - UAMpredBGs.length/Math.max(3*60/5,1) ) );
  1165. //console.error(predUCIslope, predUCImax);
  1166. // predicted CI from UAM is the lesser of CI based on deviationSlope or DIA
  1167. var predUCI = Math.min(predUCIslope, predUCImax);
  1168. if(predUCI>0) {
  1169. //console.error(UAMpredBGs.length,slopeFromDeviations, predUCI);
  1170. UAMduration=round((UAMpredBGs.length+1)*5/60,1);
  1171. }
  1172. // Adding dynamic ISF in predictions for UAM. Modification from Tim Street's AAPS but with default as off:
  1173. switch(true) {
  1174. case dynISFenabled && !enable_sigmoid:
  1175. //UAMpredBG = UAMpredBGs[UAMpredBGs.length-1] + predBGI + Math.min(0, predDev) + predUCI; // Adding dynamic ISF in predictions for UAM:
  1176. UAMpredBG = UAMpredBGs[UAMpredBGs.length-1] + (round(( -iobTick.activity * (1800 / ( tdd * adjustmentFactor * (Math.log(( Math.max(UAMpredBGs[UAMpredBGs.length-1],39) / insulinFactor ) + 1 ) ) )) * 5 ),2)) + Math.min(0, predDev) + predUCI;
  1177. console.log("Dynamic ISF (Logarithmic Formula) adjusted prediction for UAM: UAMpredBG: " + round(UAMpredBG,2));
  1178. break;
  1179. default:
  1180. UAMpredBG = UAMpredBGs[UAMpredBGs.length-1] + predBGI + Math.min(0, predDev) + predUCI;
  1181. }
  1182. //console.error(predBGI, predCI, predUCI);
  1183. // truncate all BG predictions at 4 hours
  1184. if ( IOBpredBGs.length < 48 ) { IOBpredBGs.push(IOBpredBG); }
  1185. if ( COBpredBGs.length < 48 ) { COBpredBGs.push(COBpredBG); }
  1186. if ( UAMpredBGs.length < 48 ) { UAMpredBGs.push(UAMpredBG); }
  1187. if ( ZTpredBGs.length < 48 ) { ZTpredBGs.push(ZTpredBG); }
  1188. // calculate minGuardBGs without a wait from COB, UAM, IOB predBGs
  1189. if ( COBpredBG < minCOBGuardBG ) { minCOBGuardBG = round(COBpredBG); }
  1190. if ( UAMpredBG < minUAMGuardBG ) { minUAMGuardBG = round(UAMpredBG); }
  1191. if ( IOBpredBG < minIOBGuardBG ) { minIOBGuardBG = round(IOBpredBG); }
  1192. if ( ZTpredBG < minZTGuardBG ) { minZTGuardBG = round(ZTpredBG); }
  1193. // set minPredBGs starting when currently-dosed insulin activity will peak
  1194. // look ahead 60m (regardless of insulin type) so as to be less aggressive on slower insulins
  1195. var insulinPeakTime = 60;
  1196. // add 30m to allow for insulin delivery (SMBs or temps)
  1197. insulinPeakTime = 90;
  1198. var insulinPeak5m = (insulinPeakTime/60)*12;
  1199. //console.error(insulinPeakTime, insulinPeak5m, profile.insulinPeakTime, profile.curve);
  1200. // wait 90m before setting minIOBPredBG
  1201. if ( IOBpredBGs.length > insulinPeak5m && (IOBpredBG < minIOBPredBG) ) { minIOBPredBG = round(IOBpredBG); }
  1202. if ( IOBpredBG > maxIOBPredBG ) { maxIOBPredBG = IOBpredBG; }
  1203. // wait 85-105m before setting COB and 60m for UAM minPredBGs
  1204. if ( (cid || remainingCIpeak > 0) && COBpredBGs.length > insulinPeak5m && (COBpredBG < minCOBPredBG) ) { minCOBPredBG = round(COBpredBG); }
  1205. if ( (cid || remainingCIpeak > 0) && COBpredBG > maxIOBPredBG ) { maxCOBPredBG = COBpredBG; }
  1206. if ( enableUAM && UAMpredBGs.length > 12 && (UAMpredBG < minUAMPredBG) ) { minUAMPredBG = round(UAMpredBG); }
  1207. if ( enableUAM && UAMpredBG > maxIOBPredBG ) { maxUAMPredBG = UAMpredBG; }
  1208. });
  1209. // set eventualBG to include effect of carbs
  1210. //console.error("PredBGs:",JSON.stringify(predBGs));
  1211. } catch (e) {
  1212. console.error("Problem with iobArray. Optional feature Advanced Meal Assist disabled");
  1213. }
  1214. if (meal_data.mealCOB) {
  1215. console.error("predCIs (mg/dL/5m):" + predCIs.join(" "));
  1216. console.error("remainingCIs: " + remainingCIs.join(" "));
  1217. }
  1218. rT.predBGs = {};
  1219. IOBpredBGs.forEach(function(p, i, theArray) {
  1220. theArray[i] = round(Math.min(401,Math.max(39,p)));
  1221. });
  1222. for (var i=IOBpredBGs.length-1; i > 12; i--) {
  1223. if (IOBpredBGs[i-1] !== IOBpredBGs[i]) { break; }
  1224. else { IOBpredBGs.pop(); }
  1225. }
  1226. rT.predBGs.IOB = IOBpredBGs;
  1227. lastIOBpredBG=round(IOBpredBGs[IOBpredBGs.length-1]);
  1228. ZTpredBGs.forEach(function(p, i, theArray) {
  1229. theArray[i] = round(Math.min(401,Math.max(39,p)));
  1230. });
  1231. for (i=ZTpredBGs.length-1; i > 6; i--) {
  1232. // stop displaying ZTpredBGs once they're rising and above target
  1233. if (ZTpredBGs[i-1] >= ZTpredBGs[i] || ZTpredBGs[i] <= target_bg) { break; }
  1234. else { ZTpredBGs.pop(); }
  1235. }
  1236. rT.predBGs.ZT = ZTpredBGs;
  1237. lastZTpredBG=round(ZTpredBGs[ZTpredBGs.length-1]);
  1238. if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) {
  1239. COBpredBGs.forEach(function(p, i, theArray) {
  1240. theArray[i] = round(Math.min(1500,Math.max(39,p)));
  1241. });
  1242. for (i=COBpredBGs.length-1; i > 12; i--) {
  1243. if (COBpredBGs[i-1] !== COBpredBGs[i]) { break; }
  1244. else { COBpredBGs.pop(); }
  1245. }
  1246. rT.predBGs.COB = COBpredBGs;
  1247. lastCOBpredBG=round(COBpredBGs[COBpredBGs.length-1]);
  1248. eventualBG = Math.max(eventualBG, round(COBpredBGs[COBpredBGs.length-1]));
  1249. console.error("COBpredBG: " + round(COBpredBGs[COBpredBGs.length-1]) );
  1250. }
  1251. if (ci > 0 || remainingCIpeak > 0) {
  1252. if (enableUAM) {
  1253. UAMpredBGs.forEach(function(p, i, theArray) {
  1254. theArray[i] = round(Math.min(401,Math.max(39,p)));
  1255. });
  1256. for (i=UAMpredBGs.length-1; i > 12; i--) {
  1257. if (UAMpredBGs[i-1] !== UAMpredBGs[i]) { break; }
  1258. else { UAMpredBGs.pop(); }
  1259. }
  1260. rT.predBGs.UAM = UAMpredBGs;
  1261. lastUAMpredBG=round(UAMpredBGs[UAMpredBGs.length-1]);
  1262. if (UAMpredBGs[UAMpredBGs.length-1]) {
  1263. eventualBG = Math.max(eventualBG, round(UAMpredBGs[UAMpredBGs.length-1]) );
  1264. }
  1265. }
  1266. // set eventualBG based on COB or UAM predBGs
  1267. rT.eventualBG = eventualBG;
  1268. }
  1269. console.error("UAM Impact:" + uci + "mg/dL per 5m; UAM Duration:" + UAMduration + "hours");
  1270. minIOBPredBG = Math.max(39,minIOBPredBG);
  1271. minCOBPredBG = Math.max(39,minCOBPredBG);
  1272. minUAMPredBG = Math.max(39,minUAMPredBG);
  1273. minPredBG = round(minIOBPredBG);
  1274. var fractionCarbsLeft = meal_data.mealCOB/meal_data.carbs;
  1275. // if we have COB and UAM is enabled, average both
  1276. if ( minUAMPredBG < 999 && minCOBPredBG < 999 ) {
  1277. // weight COBpredBG vs. UAMpredBG based on how many carbs remain as COB
  1278. avgPredBG = round( (1-fractionCarbsLeft)*UAMpredBG + fractionCarbsLeft*COBpredBG );
  1279. // if UAM is disabled, average IOB and COB
  1280. } else if ( minCOBPredBG < 999 ) {
  1281. avgPredBG = round( (IOBpredBG + COBpredBG)/2 );
  1282. // if we have UAM but no COB, average IOB and UAM
  1283. } else if ( minUAMPredBG < 999 ) {
  1284. avgPredBG = round( (IOBpredBG + UAMpredBG)/2 );
  1285. } else {
  1286. avgPredBG = round( IOBpredBG );
  1287. }
  1288. // if avgPredBG is below minZTGuardBG, bring it up to that level
  1289. if ( minZTGuardBG > avgPredBG ) {
  1290. avgPredBG = minZTGuardBG;
  1291. }
  1292. // if we have both minCOBGuardBG and minUAMGuardBG, blend according to fractionCarbsLeft
  1293. if ( (cid || remainingCIpeak > 0) ) {
  1294. if ( enableUAM ) {
  1295. minGuardBG = fractionCarbsLeft*minCOBGuardBG + (1-fractionCarbsLeft)*minUAMGuardBG;
  1296. } else {
  1297. minGuardBG = minCOBGuardBG;
  1298. }
  1299. } else if ( enableUAM ) {
  1300. minGuardBG = minUAMGuardBG;
  1301. } else {
  1302. minGuardBG = minIOBGuardBG;
  1303. }
  1304. minGuardBG = round(minGuardBG);
  1305. //console.error(minCOBGuardBG, minUAMGuardBG, minIOBGuardBG, minGuardBG);
  1306. var minZTUAMPredBG = minUAMPredBG;
  1307. // if minZTGuardBG is below threshold, bring down any super-high minUAMPredBG by averaging
  1308. // this helps prevent UAM from giving too much insulin in case absorption falls off suddenly
  1309. if ( minZTGuardBG < threshold ) {
  1310. minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2;
  1311. // if minZTGuardBG is between threshold and target, blend in the averaging
  1312. } else if ( minZTGuardBG < target_bg ) {
  1313. // target 100, threshold 70, minZTGuardBG 85 gives 50%: (85-70) / (100-70)
  1314. var blendPct = (minZTGuardBG-threshold) / (target_bg-threshold);
  1315. var blendedMinZTGuardBG = minUAMPredBG*blendPct + minZTGuardBG*(1-blendPct);
  1316. minZTUAMPredBG = (minUAMPredBG + blendedMinZTGuardBG) / 2;
  1317. //minZTUAMPredBG = minUAMPredBG - target_bg + minZTGuardBG;
  1318. // if minUAMPredBG is below minZTGuardBG, bring minUAMPredBG up by averaging
  1319. // this allows more insulin if lastUAMPredBG is below target, but minZTGuardBG is still high
  1320. } else if ( minZTGuardBG > minUAMPredBG ) {
  1321. minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2;
  1322. }
  1323. minZTUAMPredBG = round(minZTUAMPredBG);
  1324. //console.error("minUAMPredBG:",minUAMPredBG,"minZTGuardBG:",minZTGuardBG,"minZTUAMPredBG:",minZTUAMPredBG);
  1325. // if any carbs have been entered recently
  1326. if (meal_data.carbs) {
  1327. // if UAM is disabled, use max of minIOBPredBG, minCOBPredBG
  1328. if ( ! enableUAM && minCOBPredBG < 999 ) {
  1329. minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG));
  1330. // if we have COB, use minCOBPredBG, or blendedMinPredBG if it's higher
  1331. } else if ( minCOBPredBG < 999 ) {
  1332. // calculate blendedMinPredBG based on how many carbs remain as COB
  1333. var blendedMinPredBG = fractionCarbsLeft*minCOBPredBG + (1-fractionCarbsLeft)*minZTUAMPredBG;
  1334. // if blendedMinPredBG > minCOBPredBG, use that instead
  1335. minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG, blendedMinPredBG));
  1336. // if carbs have been entered, but have expired, use minUAMPredBG
  1337. } else if ( enableUAM ) {
  1338. minPredBG = minZTUAMPredBG;
  1339. } else {
  1340. minPredBG = minGuardBG;
  1341. }
  1342. // in pure UAM mode, use the higher of minIOBPredBG,minUAMPredBG
  1343. } else if ( enableUAM ) {
  1344. minPredBG = round(Math.max(minIOBPredBG,minZTUAMPredBG));
  1345. }
  1346. // make sure minPredBG isn't higher than avgPredBG
  1347. minPredBG = Math.min( minPredBG, avgPredBG );
  1348. // Print summary variables based on predBGs etc.
  1349. process.stderr.write("minPredBG: "+minPredBG+" minIOBPredBG: "+minIOBPredBG+" minZTGuardBG: "+minZTGuardBG);
  1350. if (minCOBPredBG < 999) {
  1351. process.stderr.write(" minCOBPredBG: "+minCOBPredBG);
  1352. }
  1353. if (minUAMPredBG < 999) {
  1354. process.stderr.write(" minUAMPredBG: "+minUAMPredBG);
  1355. }
  1356. console.error(" avgPredBG:" + avgPredBG + " COB/Carbs:" + meal_data.mealCOB + "/" + meal_data.carbs);
  1357. // But if the COB line falls off a cliff, don't trust UAM too much:
  1358. // use maxCOBPredBG if it's been set and lower than minPredBG
  1359. if ( maxCOBPredBG > bg ) {
  1360. minPredBG = Math.min(minPredBG, maxCOBPredBG);
  1361. }
  1362. rT.COB=meal_data.mealCOB;
  1363. rT.IOB=iob_data.iob;
  1364. rT.BGI=convert_bg(bgi,profile);
  1365. rT.deviation=convert_bg(deviation, profile);
  1366. rT.ISF=convert_bg(sens, profile);
  1367. rT.CR=round(carbRatio, 1);
  1368. rT.target_bg=convert_bg(target_bg, profile);
  1369. rT.TDD=round(tdd_before, 2);
  1370. rT.current_target=round(target_bg, 0);
  1371. var cr_log = rT.CR;
  1372. if (cr_before != rT.CR) {
  1373. cr_log = cr_before + "\u2192" + rT.CR;
  1374. }
  1375. rT.reason = isfreason + ", COB: " + rT.COB + ", Dev: " + rT.deviation + ", BGI: " + rT.BGI + ", CR: " + cr_log + ", Target: " + targetLog + ", minPredBG " + convert_bg(minPredBG, profile) + ", minGuardBG " + convert_bg(minGuardBG, profile) + ", IOBpredBG " + convert_bg(lastIOBpredBG, profile);
  1376. if (lastCOBpredBG > 0) {
  1377. rT.reason += ", COBpredBG " + convert_bg(lastCOBpredBG, profile);
  1378. }
  1379. if (lastUAMpredBG > 0) {
  1380. rT.reason += ", UAMpredBG " + convert_bg(lastUAMpredBG, profile);
  1381. }
  1382. rT.reason += tddReason;
  1383. rT.reason += "; "; // reason.conclusion started
  1384. // Use minGuardBG to prevent overdosing in hypo-risk situations
  1385. // use naive_eventualBG if above 40, but switch to minGuardBG if both eventualBGs hit floor of 39
  1386. var carbsReqBG = naive_eventualBG;
  1387. if ( carbsReqBG < 40 ) {
  1388. carbsReqBG = Math.min( minGuardBG, carbsReqBG );
  1389. }
  1390. var bgUndershoot = threshold - carbsReqBG;
  1391. // calculate how long until COB (or IOB) predBGs drop below min_bg
  1392. var minutesAboveMinBG = 240;
  1393. var minutesAboveThreshold = 240;
  1394. if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) {
  1395. for (i=0; i<COBpredBGs.length; i++) {
  1396. if ( COBpredBGs[i] < min_bg ) {
  1397. minutesAboveMinBG = 5*i;
  1398. break;
  1399. }
  1400. }
  1401. for (i=0; i<COBpredBGs.length; i++) {
  1402. if ( COBpredBGs[i] < threshold ) {
  1403. minutesAboveThreshold = 5*i;
  1404. break;
  1405. }
  1406. }
  1407. }
  1408. else {
  1409. for (i=0; i<IOBpredBGs.length; i++) {
  1410. //console.error(IOBpredBGs[i], min_bg);
  1411. if ( IOBpredBGs[i] < min_bg ) {
  1412. minutesAboveMinBG = 5*i;
  1413. break;
  1414. }
  1415. }
  1416. for (i=0; i<IOBpredBGs.length; i++) {
  1417. //console.error(IOBpredBGs[i], threshold);
  1418. if ( IOBpredBGs[i] < threshold ) {
  1419. minutesAboveThreshold = 5*i;
  1420. break;
  1421. }
  1422. }
  1423. }
  1424. if (enableSMB && minGuardBG < threshold) {
  1425. console.error("minGuardBG " + convert_bg(minGuardBG, profile) + " projected below " + convert_bg(threshold, profile) + " - disabling SMB");
  1426. rT.manualBolusErrorString = 1;
  1427. rT.minGuardBG = minGuardBG;
  1428. rT.insulinForManualBolus = round((rT.eventualBG - rT.target_bg) / sens, 2);
  1429. //rT.reason += "minGuardBG "+minGuardBG+"<"+threshold+": SMB disabled; ";
  1430. enableSMB = false;
  1431. }
  1432. // Disable SMB for sudden rises (often caused by calibrations or activation/deactivation of Dexcom's noise-filtering algorithm)
  1433. // Added maxDelta_bg_threshold as a hidden preference and included a cap at 0.4 as a safety limitLog
  1434. var maxDelta_bg_threshold;
  1435. if (typeof profile.maxDelta_bg_threshold === 'undefined') {
  1436. maxDelta_bg_threshold = 0.2;
  1437. }
  1438. if (typeof profile.maxDelta_bg_threshold !== 'undefined') {
  1439. maxDelta_bg_threshold = Math.min(profile.maxDelta_bg_threshold, 0.4);
  1440. }
  1441. if ( maxDelta > maxDelta_bg_threshold * bg ) {
  1442. console.error("maxDelta " + convert_bg(maxDelta, profile)+ " > " + 100 * maxDelta_bg_threshold + "% of BG " + convert_bg(bg, profile) + " - disabling SMB");
  1443. rT.reason += "maxDelta " + convert_bg(maxDelta, profile) + " > " + 100 * maxDelta_bg_threshold + "% of BG " + convert_bg(bg, profile) + " - SMB disabled!, ";
  1444. enableSMB = false;
  1445. }
  1446. // Calculate carbsReq (carbs required to avoid a hypo)
  1447. console.error("BG projected to remain above " + convert_bg(min_bg, profile) + " for " + minutesAboveMinBG + "minutes");
  1448. if ( minutesAboveThreshold < 240 || minutesAboveMinBG < 60 ) {
  1449. console.error("BG projected to remain above " + convert_bg(threshold,profile) + " for " + minutesAboveThreshold + "minutes");
  1450. }
  1451. // include at least minutesAboveThreshold worth of zero temps in calculating carbsReq
  1452. // always include at least 30m worth of zero temp (carbs to 80, low temp up to target)
  1453. var zeroTempDuration = minutesAboveThreshold;
  1454. // BG undershoot, minus effect of zero temps until hitting min_bg, converted to grams, minus COB
  1455. var zeroTempEffect = profile.current_basal*overrideFactor*sens*zeroTempDuration/60;
  1456. // don't count the last 25% of COB against carbsReq
  1457. var COBforCarbsReq = Math.max(0, meal_data.mealCOB - 0.25*meal_data.carbs);
  1458. var carbsReq = (bgUndershoot - zeroTempEffect) / csf - COBforCarbsReq;
  1459. zeroTempEffect = round(zeroTempEffect);
  1460. carbsReq = round(carbsReq);
  1461. console.error("naive_eventualBG:",naive_eventualBG,"bgUndershoot:",bgUndershoot,"zeroTempDuration:",zeroTempDuration,"zeroTempEffect:",zeroTempEffect,"carbsReq:",carbsReq);
  1462. if ( meal_data.reason == "Could not parse clock data" ) {
  1463. console.error("carbsReq unknown: Could not parse clock data");
  1464. } else if ( carbsReq >= profile.carbsReqThreshold && minutesAboveThreshold <= 45 ) {
  1465. rT.carbsReq = carbsReq;
  1466. rT.reason += carbsReq + " add'l carbs req w/in " + minutesAboveThreshold + "m; ";
  1467. }
  1468. // Begin core dosing logic: check for situations requiring low or high temps, and return appropriate temp after first match
  1469. // don't low glucose suspend if IOB is already super negative and BG is rising faster than predicted
  1470. var worstCaseInsulinReq = 0;
  1471. var durationReq = 0;
  1472. if (bg < threshold && iob_data.iob < -profile.current_basal*overrideFactor*20/60 && minDelta > 0 && minDelta > expectedDelta) {
  1473. rT.reason += "IOB "+iob_data.iob+" < " + round(-profile.current_basal*overrideFactor*20/60,2);
  1474. rT.reason += " and minDelta " + convert_bg(minDelta, profile) + " > " + "expectedDelta " + convert_bg(expectedDelta, profile) + "; ";
  1475. // predictive low glucose suspend mode: BG is / is projected to be < threshold
  1476. } else if ( bg < threshold || minGuardBG < threshold ) {
  1477. rT.reason += "minGuardBG " + convert_bg(minGuardBG, profile) + "<" + convert_bg(threshold, profile);
  1478. bgUndershoot = target_bg - minGuardBG;
  1479. if (minGuardBG < threshold) {
  1480. rT.manualBolusErrorString = 2;
  1481. rT.minGuardBG = minGuardBG;
  1482. }
  1483. rT.insulinForManualBolus = round((eventualBG - target_bg) / sens, 2);
  1484. worstCaseInsulinReq = bgUndershoot / sens;
  1485. durationReq = round(60*worstCaseInsulinReq / profile.current_basal*overrideFactor);
  1486. durationReq = round(durationReq/30)*30;
  1487. // always set a 30-120m zero temp (oref0-pump-loop will let any longer SMB zero temp run)
  1488. durationReq = Math.min(120,Math.max(30,durationReq));
  1489. return tempBasalFunctions.setTempBasal(0, durationReq, profile, rT, currenttemp);
  1490. }
  1491. // if not in LGS mode, cancel temps before the top of the hour to reduce beeping/vibration
  1492. // console.error(profile.skip_neutral_temps, rT.deliverAt.getMinutes());
  1493. if ( profile.skip_neutral_temps && rT.deliverAt.getMinutes() >= 55 ) {
  1494. if (!enableSMB) {
  1495. rT.reason += "; Canceling temp at " + (60 - rT.deliverAt.getMinutes()) + "min before turn of the hour to avoid beeping of MDT. SMB are disabled anyways.";
  1496. return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp);
  1497. } else {
  1498. console.error((60 - rT.deliverAt.getMinutes()) + "min before turn of the hour, but SMB's are enabled - not skipping neutral temps.")
  1499. }
  1500. }
  1501. var insulinReq = 0;
  1502. var rate = basal;
  1503. var insulinScheduled = 0;
  1504. if (eventualBG < min_bg) { // if eventual BG is below target:
  1505. rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " < " + convert_bg(min_bg, profile);
  1506. // if 5m or 30m avg BG is rising faster than expected delta
  1507. if ( minDelta > expectedDelta && minDelta > 0 && !carbsReq ) {
  1508. // if naive_eventualBG < 40, set a 30m zero temp (oref0-pump-loop will let any longer SMB zero temp run)
  1509. if (naive_eventualBG < 40) {
  1510. rT.reason += ", naive_eventualBG < 40. ";
  1511. return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp);
  1512. }
  1513. if (glucose_status.delta > minDelta) {
  1514. rT.reason += ", but Delta " + convert_bg(tick, profile) + " > expectedDelta " + convert_bg(expectedDelta, profile);
  1515. } else {
  1516. rT.reason += ", but Min. Delta " + minDelta.toFixed(2) + " > Exp. Delta " + convert_bg(expectedDelta, profile);
  1517. }
  1518. if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) {
  1519. rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. ";
  1520. return rT;
  1521. } else {
  1522. rT.reason += "; setting current basal of " + basal + " as temp. ";
  1523. return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
  1524. }
  1525. }
  1526. // calculate 30m low-temp required to get projected BG up to target
  1527. // multiply by 2 to low-temp faster for increased hypo safety
  1528. insulinReq = 2 * Math.min(0, (eventualBG - target_bg) / sens);
  1529. insulinReq = round( insulinReq , 2);
  1530. // calculate naiveInsulinReq based on naive_eventualBG
  1531. var naiveInsulinReq = Math.min(0, (naive_eventualBG - target_bg) / sens);
  1532. naiveInsulinReq = round( naiveInsulinReq , 2);
  1533. if (minDelta < 0 && minDelta > expectedDelta) {
  1534. // if we're barely falling, newinsulinReq should be barely negative
  1535. var newinsulinReq = round(( insulinReq * (minDelta / expectedDelta) ), 2);
  1536. //console.error("Increasing insulinReq from " + insulinReq + " to " + newinsulinReq);
  1537. insulinReq = newinsulinReq;
  1538. }
  1539. // rate required to deliver insulinReq less insulin over 30m:
  1540. rate = basal + (2 * insulinReq);
  1541. rate = round_basal(rate, profile);
  1542. // if required temp < existing temp basal
  1543. insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60;
  1544. // if current temp would deliver a lot (30% of basal) less than the required insulin,
  1545. // by both normal and naive calculations, then raise the rate
  1546. var minInsulinReq = Math.min(insulinReq,naiveInsulinReq);
  1547. console.log("naiveInsulinReq:" + naiveInsulinReq);
  1548. if (insulinScheduled < minInsulinReq - basal*0.3) {
  1549. rT.reason += ", " + currenttemp.duration + "m@" + (currenttemp.rate).toFixed(2) + " is a lot less than needed. ";
  1550. return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp);
  1551. }
  1552. if (typeof currenttemp.rate !== 'undefined' && (currenttemp.duration > 5 && rate >= currenttemp.rate * 0.8)) {
  1553. rT.reason += ", temp " + currenttemp.rate + " ~< req " + rate + "U/hr. ";
  1554. return rT;
  1555. }
  1556. else {
  1557. // calculate a long enough zero temp to eventually correct back up to target
  1558. if ( rate <=0 ) {
  1559. bgUndershoot = target_bg - naive_eventualBG;
  1560. worstCaseInsulinReq = bgUndershoot / sens;
  1561. durationReq = round(60*worstCaseInsulinReq / profile.current_basal * overrideFactor);
  1562. if (durationReq < 0) {
  1563. durationReq = 0;
  1564. // don't set a temp longer than 120 minutes
  1565. } else {
  1566. durationReq = round(durationReq/30)*30;
  1567. durationReq = Math.min(120,Math.max(0,durationReq));
  1568. }
  1569. //console.error(durationReq);
  1570. if (durationReq > 0) {
  1571. rT.reason += ", setting " + durationReq + "m zero temp. ";
  1572. return tempBasalFunctions.setTempBasal(rate, durationReq, profile, rT, currenttemp);
  1573. }
  1574. }
  1575. else {
  1576. rT.reason += ", setting " + rate + "U/hr. ";
  1577. }
  1578. return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp);
  1579. }
  1580. }
  1581. // if eventual BG is above min_bg but BG is falling faster than expected Delta
  1582. if (minDelta < expectedDelta) {
  1583. rT.minDelta = minDelta;
  1584. rT.expectedDelta = expectedDelta;
  1585. //Describe how the glucose is changing
  1586. if (expectedDelta - minDelta >= 2 || (expectedDelta + (-1 * minDelta) >= 2)) {
  1587. if (minDelta >= 0 && expectedDelta > 0) {
  1588. rT.manualBolusErrorString = 3;
  1589. }
  1590. else if ((minDelta < 0 && expectedDelta <= 0) || (minDelta < 0 && expectedDelta >= 0)) {
  1591. rT.manualBolusErrorString = 4;
  1592. }
  1593. else {
  1594. rT.manualBolusErrorString = 5;
  1595. }
  1596. }
  1597. rT.insulinForManualBolus = round((rT.eventualBG - rT.target_bg) / sens, 2);
  1598. // if in SMB mode, don't cancel SMB zero temp
  1599. if (! (microBolusAllowed && enableSMB)) {
  1600. if (glucose_status.delta < minDelta) {
  1601. rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Delta " + convert_bg(tick, profile) + " < Exp. Delta " + convert_bg(expectedDelta, profile);
  1602. } else {
  1603. rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Min. Delta " + minDelta.toFixed(2) + " < Exp. Delta " + convert_bg(expectedDelta, profile);
  1604. }
  1605. if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) {
  1606. rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. ";
  1607. return rT;
  1608. } else {
  1609. rT.reason += "; setting current basal of " + basal + " as temp. ";
  1610. return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
  1611. }
  1612. }
  1613. }
  1614. // eventualBG or minPredBG is below max_bg
  1615. if (Math.min(eventualBG,minPredBG) < max_bg) {
  1616. if (minPredBG < min_bg && eventualBG > min_bg) {
  1617. rT.manualBolusErrorString = 6;
  1618. rT.insulinForManualBolus = round((rT.eventualBG - rT.target_bg) / sens, 2);
  1619. rT.minPredBG = minPredBG;
  1620. }
  1621. // if in SMB mode, don't cancel SMB zero temp
  1622. if (! (microBolusAllowed && enableSMB )) {
  1623. rT.reason += convert_bg(eventualBG, profile)+ "-" + convert_bg(minPredBG, profile) + " in range: no temp required";
  1624. if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) {
  1625. rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. ";
  1626. return rT;
  1627. } else {
  1628. rT.reason += "; setting current basal of " + basal + " as temp. ";
  1629. return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
  1630. }
  1631. }
  1632. }
  1633. // eventual BG is at/above target
  1634. // if iob is over max, just cancel any temps
  1635. if ( eventualBG >= max_bg ) {
  1636. rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " >= " + convert_bg(max_bg, profile) + ", ";
  1637. if (eventualBG > max_bg) {
  1638. rT.insulinForManualBolus = round((eventualBG - target_bg) / sens, 2);
  1639. }
  1640. }
  1641. if (iob_data.iob > max_iob) {
  1642. rT.reason += "IOB " + round(iob_data.iob,2) + " > max_iob " + max_iob;
  1643. if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) {
  1644. rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. ";
  1645. return rT;
  1646. } else {
  1647. rT.reason += "; setting current basal of " + basal + " as temp. ";
  1648. return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
  1649. }
  1650. }
  1651. else { // otherwise, calculate 30m high-temp required to get projected BG down to target
  1652. // insulinReq is the additional insulin required to get minPredBG down to target_bg
  1653. //console.error(minPredBG,eventualBG);
  1654. insulinReq = round( (Math.min(minPredBG,eventualBG) - target_bg) / sens, 2);
  1655. insulinForManualBolus = round((eventualBG - target_bg) / sens, 2);
  1656. // if that would put us over max_iob, then reduce accordingly
  1657. if (insulinReq > max_iob-iob_data.iob) {
  1658. console.error("SMB limited by maxIOB: " + max_iob-iob_data.iob + " (. insulinReq: " + insulinReq + " U)");
  1659. rT.reason += "max_iob " + max_iob + ", ";
  1660. insulinReq = max_iob-iob_data.iob;
  1661. } else { console.error("SMB not limited by maxIOB ( insulinReq: " + insulinReq + " U).");}
  1662. if (insulinForManualBolus > max_iob-iob_data.iob) {
  1663. console.error("Ev. Bolus limited by maxIOB: " + max_iob-iob_data.iob + " (. insulinForManualBolus: " + insulinForManualBolus + " U)");
  1664. rT.reason += "max_iob " + max_iob + ", ";
  1665. } else { console.error("Ev. Bolus would not be limited by maxIOB ( insulinForManualBolus: " + insulinForManualBolus + " U).");}
  1666. // rate required to deliver insulinReq more insulin over 30m:
  1667. rate = basal + (2 * insulinReq);
  1668. rate = round_basal(rate, profile);
  1669. insulinReq = round(insulinReq,3);
  1670. rT.insulinReq = insulinReq;
  1671. //console.error(iob_data.lastBolusTime);
  1672. // minutes since last bolus
  1673. var lastBolusAge = round(( new Date(systemTime).getTime() - iob_data.lastBolusTime ) / 60000,1);
  1674. //console.error(lastBolusAge);
  1675. //console.error(profile.temptargetSet, target_bg, rT.COB);
  1676. // only allow microboluses with COB or low temp targets, or within DIA hours of a bolus
  1677. if (microBolusAllowed && enableSMB && bg > threshold) {
  1678. // never bolus more than maxSMBBasalMinutes worth of basal
  1679. var smbMinutesSetting = 30;
  1680. if (typeof profile.maxSMBBasalMinutes !== 'undefined') {
  1681. smbMinutesSetting = profile.maxSMBBasalMinutes;
  1682. }
  1683. var uamMinutesSetting = 30;
  1684. if (typeof profile.maxUAMSMBBasalMinutes !== 'undefined') {
  1685. uamMinutesSetting = profile.maxUAMSMBBasalMinutes;
  1686. }
  1687. if (oref2_variables.useOverride && advancedSettings && smbMinutes !== smbMinutesSetting) {
  1688. console.error("SMB Max Minutes - setting overriden from " + smbMinutesSetting + " to " + smbMinutes);
  1689. smbMinutesSetting = smbMinutes;
  1690. }
  1691. if (oref2_variables.useOverride && advancedSettings && uamMinutes !== uamMinutesSetting) {
  1692. console.error("UAM Max Minutes - setting overriden from " + uamMinutesSetting + " to " + uamMinutes);
  1693. uamMinutesSetting = uamMinutes;
  1694. }
  1695. var mealInsulinReq = round( meal_data.mealCOB / carbRatio ,3);
  1696. var maxBolus = 0;
  1697. if (typeof smbMinutesSetting === 'undefined' ) {
  1698. maxBolus = round(profile.current_basal *overrideFactor * 30 / 60 ,1);
  1699. console.error("smbMinutesSetting undefined: defaulting to 30m");
  1700. if( insulinReq > maxBolus ) {
  1701. console.error("SMB limited by maxBolus: " + maxBolus + " ( " + insulinReq + " U)");
  1702. }
  1703. } else if ( iob_data.iob > mealInsulinReq && iob_data.iob > 0 ) {
  1704. console.error("IOB" + iob_data.iob + "> COB" + meal_data.mealCOB + "; mealInsulinReq =" + mealInsulinReq);
  1705. if (uamMinutesSetting) {
  1706. console.error("maxUAMSMBBasalMinutes: " + uamMinutesSetting + ", profile.current_basal: " + profile.current_basal * overrideFactor);
  1707. maxBolus = round(profile.current_basal * overrideFactor * uamMinutesSetting / 60 ,1);
  1708. } else {
  1709. console.error("maxUAMSMBBasalMinutes undefined: defaulting to 30m");
  1710. maxBolus = round( profile.current_basal * overrideFactor * 30 / 60 ,1);
  1711. }
  1712. if( insulinReq > maxBolus ) {
  1713. console.error("SMB limited by maxUAMSMBBasalMinutes [ " + uamMinutesSetting + "m ]: " + maxBolus + "U ( " + insulinReq + "U )");
  1714. } else { console.error("SMB is not limited by maxUAMSMBBasalMinutes. ( insulinReq: " + insulinReq + "U )"); }
  1715. } else {
  1716. console.error(".maxSMBBasalMinutes: " + smbMinutesSetting + ", profile.current_basal: " + profile.current_basal * overrideFactor);
  1717. maxBolus = round(profile.current_basal * overrideFactor * smbMinutesSetting / 60 ,1);
  1718. if( insulinReq > maxBolus ) {
  1719. console.error("SMB limited by maxSMBBasalMinutes: " + smbMinutesSetting + "m ]: " + maxBolus + "U ( insulinReq: " + insulinReq + "U )");
  1720. } else { console.error("SMB is not limited by maxSMBBasalMinutes. ( insulinReq: " + insulinReq + "U )"); }
  1721. }
  1722. // bolus 1/2 the insulinReq, up to maxBolus, rounding down to nearest bolus increment
  1723. var bolusIncrement = profile.bolus_increment;
  1724. //if (profile.bolus_increment) { bolusIncrement=profile.bolus_increment };
  1725. var roundSMBTo = 1 / bolusIncrement;
  1726. var smb_ratio = Math.min(profile.smb_delivery_ratio, 1);
  1727. if (smb_ratio != 0.5) {
  1728. console.error("SMB Delivery Ratio changed from default 0.5 to " + round(smb_ratio,2))
  1729. }
  1730. var microBolus = Math.min(insulinReq*smb_ratio, maxBolus);
  1731. microBolus = Math.floor(microBolus*roundSMBTo)/roundSMBTo;
  1732. // calculate a long enough zero temp to eventually correct back up to target
  1733. var smbTarget = target_bg;
  1734. worstCaseInsulinReq = (smbTarget - (naive_eventualBG + minIOBPredBG)/2 ) / sens;
  1735. durationReq = round(60*worstCaseInsulinReq / profile.current_basal * overrideFactor);
  1736. // if insulinReq > 0 but not enough for a microBolus, don't set an SMB zero temp
  1737. if (insulinReq > 0 && microBolus < bolusIncrement) {
  1738. durationReq = 0;
  1739. }
  1740. var smbLowTempReq = 0;
  1741. if (durationReq <= 0) {
  1742. durationReq = 0;
  1743. // don't set an SMB zero temp longer than 60 minutes
  1744. } else if (durationReq >= 30) {
  1745. durationReq = round(durationReq/30)*30;
  1746. durationReq = Math.min(60,Math.max(0,durationReq));
  1747. } else {
  1748. // if SMB durationReq is less than 30m, set a nonzero low temp
  1749. smbLowTempReq = round( basal * durationReq/30 ,2);
  1750. durationReq = 30;
  1751. }
  1752. rT.reason += " insulinReq " + insulinReq;
  1753. if (microBolus >= maxBolus) {
  1754. rT.reason += "; maxBolus " + maxBolus;
  1755. }
  1756. if (durationReq > 0) {
  1757. rT.reason += "; setting " + durationReq + "m low temp of " + smbLowTempReq + "U/h";
  1758. }
  1759. rT.reason += ". ";
  1760. //allow SMBs every 3 minutes by default
  1761. var SMBInterval = 3;
  1762. if (profile.SMBInterval) {
  1763. // allow SMBIntervals between 1 and 10 minutes
  1764. SMBInterval = Math.min(10,Math.max(1,profile.SMBInterval));
  1765. }
  1766. var nextBolusMins = round(SMBInterval-lastBolusAge,0);
  1767. var nextBolusSeconds = round((SMBInterval - lastBolusAge) * 60, 0) % 60;
  1768. //console.error(naive_eventualBG, insulinReq, worstCaseInsulinReq, durationReq);
  1769. console.error("naive_eventualBG " + naive_eventualBG + "," + durationReq + "m " + smbLowTempReq + "U/h temp needed; last bolus " + lastBolusAge +"m ago; maxBolus: " + maxBolus);
  1770. if (lastBolusAge > SMBInterval) {
  1771. if (microBolus > 0) {
  1772. rT.units = microBolus;
  1773. rT.reason += "Microbolusing " + microBolus + "U. ";
  1774. }
  1775. } else {
  1776. rT.reason += "Waiting " + nextBolusMins + "m " + nextBolusSeconds + "s to microbolus again. ";
  1777. }
  1778. //rT.reason += ". ";
  1779. // if no zero temp is required, don't return yet; allow later code to set a high temp
  1780. if (durationReq > 0) {
  1781. rT.rate = smbLowTempReq;
  1782. rT.duration = durationReq;
  1783. return rT;
  1784. }
  1785. }
  1786. var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile);
  1787. if (bg == 400) {
  1788. return tempBasalFunctions.setTempBasal(profile.current_basal, 30, profile, rT, currenttemp);
  1789. }
  1790. if (rate > maxSafeBasal) {
  1791. rT.reason += "adj. req. rate: " + rate + " to maxSafeBasal: " + round(maxSafeBasal,2) + ", ";
  1792. rate = round_basal(maxSafeBasal, profile);
  1793. }
  1794. insulinScheduled = currenttemp.duration * (currenttemp.rate - basal) / 60;
  1795. if (insulinScheduled >= insulinReq * 2) { // if current temp would deliver >2x more than the required insulin, lower the rate
  1796. rT.reason += currenttemp.duration + "m@" + (currenttemp.rate).toFixed(2) + " > 2 * insulinReq. Setting temp basal of " + rate + "U/hr. ";
  1797. return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp);
  1798. }
  1799. if (typeof currenttemp.duration === 'undefined' || currenttemp.duration === 0) { // no temp is set
  1800. rT.reason += "no temp, setting " + rate + "U/hr. ";
  1801. return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp);
  1802. }
  1803. if (currenttemp.duration > 5 && (round_basal(rate, profile) <= round_basal(currenttemp.rate, profile))) { // if required temp <~ existing temp basal
  1804. rT.reason += "temp " + currenttemp.rate + " >~ req " + rate + "U/hr. ";
  1805. return rT;
  1806. }
  1807. // required temp > existing temp basal
  1808. rT.reason += "temp " + currenttemp.rate + "<" + rate + "U/hr. ";
  1809. return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp);
  1810. }
  1811. };
  1812. module.exports = determine_basal;