DynamicISF.swift 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import Foundation
  2. /// Represents the successful output of a dynamic ISF calculation.
  3. struct DynamicISFResult {
  4. /// The final sensitivity ratio, after all calculations and clamping.
  5. let ratio: Decimal
  6. /// The ratio of 24h TDD to the 14-day average TDD, clamped by autosens limits.
  7. let tddRatio: Decimal
  8. /// The calculated insulin factor (120 - peak time), used in the logarithmic formula.
  9. let insulinFactor: Decimal
  10. }
  11. enum DynamicISF {
  12. /// Calculates the dynamic ISF ratio and related values.
  13. ///
  14. /// This function ports the core logic from `determine-basal.js` for dynamic ISF.
  15. /// - Parameters:
  16. /// - profile: The user's profile, containing settings like autosens limits and insulin curve type.
  17. /// - preferences: The user's preferences, containing feature flags like `useNewFormula` and `sigmoid`.
  18. /// - currentGlucose: The most recent glucose reading.
  19. /// - tdd: The total daily dose of insulin, used as a key input for the logarithmic formula.
  20. /// - profileTarget: The effective, override-adjusted blood glucose target. Used in the sigmoid formula.
  21. /// - sensitivity: The effective, override-adjusted insulin sensitivity (ISF). Used in the logarithmic formula.
  22. /// - trioCustomOrefVariables: Custom variables containing TDD averages needed for the TDD ratio calculation.
  23. /// - Returns: A `DynamicISFResult` struct on success, or `nil` if the feature is disabled or preconditions are not met.
  24. static func calculate(
  25. profile: Profile,
  26. preferences: Preferences,
  27. currentGlucose: Decimal,
  28. trioCustomOrefVariables: TrioCustomOrefVariables
  29. ) -> DynamicISFResult? {
  30. let tdd: Decimal
  31. if profile.weightPercentage < 1, trioCustomOrefVariables.weightedAverage > 1 {
  32. tdd = trioCustomOrefVariables.weightedAverage
  33. } else {
  34. tdd = trioCustomOrefVariables.currentTDD
  35. }
  36. guard preferences.useNewFormula, tdd > 0, var sensitivity = profile.sens, var profileTarget = profile.minBg else {
  37. return nil
  38. }
  39. if trioCustomOrefVariables.useOverride {
  40. let overrideFactor = trioCustomOrefVariables.overridePercentage / 100
  41. if trioCustomOrefVariables.isfAndCr || trioCustomOrefVariables.isf {
  42. sensitivity = sensitivity / overrideFactor
  43. }
  44. }
  45. let overrideTarget = trioCustomOrefVariables.overrideTarget
  46. if overrideTarget != 0, overrideTarget != 6, trioCustomOrefVariables
  47. .useOverride, !(profile.temptargetSet ?? false)
  48. {
  49. profileTarget = overrideTarget
  50. }
  51. let minLimit = min(profile.autosensMin, profile.autosensMax)
  52. let maxLimit = max(profile.autosensMin, profile.autosensMax)
  53. // If the limits are invalid, disable dynamicISF
  54. guard maxLimit > minLimit, maxLimit >= 1, minLimit <= 1 else {
  55. return nil
  56. }
  57. let bg = currentGlucose
  58. var tdd24h_14d_Ratio: Decimal
  59. if trioCustomOrefVariables.average_total_data > 0 {
  60. tdd24h_14d_Ratio = trioCustomOrefVariables.weightedAverage / trioCustomOrefVariables.average_total_data
  61. } else {
  62. tdd24h_14d_Ratio = 1
  63. }
  64. let clampedTddRatio = tdd24h_14d_Ratio.clamp(lowerBound: minLimit, upperBound: maxLimit).rounded(scale: 2)
  65. let insulinFactor: Decimal
  66. if preferences.useCustomPeakTime {
  67. insulinFactor = 120 - profile.insulinPeakTime
  68. } else {
  69. switch profile.curve {
  70. case .rapidActing: insulinFactor = 120 - 65
  71. case .ultraRapid: insulinFactor = 120 - 50
  72. default: insulinFactor = 120 - 65
  73. }
  74. }
  75. var newRatio: Decimal
  76. if preferences.sigmoid {
  77. let autosensInterval = maxLimit - minLimit
  78. let bgDev = (bg - profileTarget) * 0.0555
  79. let tddFactor = clampedTddRatio
  80. var maxMinusOne = maxLimit - 1
  81. // BUG: Note this fudge factor is to avoid a divide by zero but produces
  82. // unintuitive (and incorrect) results. See the unit tests for an example
  83. if maxLimit == 1 { maxMinusOne = maxLimit + 0.01 - 1 }
  84. let fixOffset = Decimal.log10(1 / maxMinusOne - minLimit / maxMinusOne) / Decimal(Foundation.log10(M_E))
  85. let exponent = bgDev * preferences.adjustmentFactorSigmoid * tddFactor + fixOffset
  86. newRatio = autosensInterval / (1 + Decimal.exp(-exponent)) + minLimit
  87. } else {
  88. newRatio = sensitivity * preferences.adjustmentFactor * tdd * (Decimal.log((bg / insulinFactor) + 1) / 1800)
  89. }
  90. return DynamicISFResult(
  91. ratio: newRatio.clamp(lowerBound: minLimit, upperBound: maxLimit),
  92. tddRatio: clampedTddRatio,
  93. insulinFactor: insulinFactor
  94. )
  95. }
  96. }
  97. extension Decimal {
  98. static func exp(_ x: Decimal) -> Decimal {
  99. Decimal(Foundation.exp(Double(x)))
  100. }
  101. static func log10(_ x: Decimal) -> Decimal {
  102. Decimal(Foundation.log10(Double(x)))
  103. }
  104. static func log(_ x: Decimal) -> Decimal {
  105. Decimal(Foundation.log(Double(x)))
  106. }
  107. }