ProfileGenerator.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import Foundation
  2. extension Profile {
  3. /// Updates profile properties from preferences where CodingKeys match
  4. /// This function ended up being pretty ugly, but I couldn't think of a cleaner
  5. /// way. I considered converting to JSON or using Mirror, but these weren't
  6. /// great so in the end I think that this approach is simpliest.
  7. ///
  8. /// Also, this implementation does _not_ copy any of the optional properties
  9. /// since these should get set in the `generate` method.
  10. mutating func update(from preferences: Preferences) {
  11. // Decimal properties
  12. maxIob = preferences.maxIOB
  13. min5mCarbImpact = preferences.min5mCarbimpact
  14. maxCOB = preferences.maxCOB
  15. maxDailySafetyMultiplier = preferences.maxDailySafetyMultiplier
  16. currentBasalSafetyMultiplier = preferences.currentBasalSafetyMultiplier
  17. autosensMax = preferences.autosensMax
  18. autosensMin = preferences.autosensMin
  19. halfBasalExerciseTarget = preferences.halfBasalExerciseTarget
  20. remainingCarbsCap = preferences.remainingCarbsCap
  21. smbInterval = preferences.smbInterval
  22. maxSMBBasalMinutes = preferences.maxSMBBasalMinutes
  23. maxUAMSMBBasalMinutes = preferences.maxUAMSMBBasalMinutes
  24. bolusIncrement = preferences.bolusIncrement
  25. carbsReqThreshold = preferences.carbsReqThreshold
  26. remainingCarbsFraction = preferences.remainingCarbsFraction
  27. enableSMBHighBgTarget = preferences.enableSMB_high_bg_target
  28. maxDeltaBgThreshold = preferences.maxDeltaBGthreshold
  29. insulinPeakTime = preferences.insulinPeakTime
  30. noisyCGMTargetMultiplier = preferences.noisyCGMTargetMultiplier
  31. adjustmentFactor = preferences.adjustmentFactor
  32. adjustmentFactorSigmoid = preferences.adjustmentFactorSigmoid
  33. weightPercentage = preferences.weightPercentage
  34. thresholdSetting = preferences.threshold_setting
  35. maxMealAbsorptionTime = preferences.maxMealAbsorptionTime
  36. smbDeliveryRatio = preferences.smbDeliveryRatio
  37. // Bool properties
  38. highTemptargetRaisesSensitivity = preferences.highTemptargetRaisesSensitivity
  39. lowTemptargetLowersSensitivity = preferences.lowTemptargetLowersSensitivity
  40. sensitivityRaisesTarget = preferences.sensitivityRaisesTarget
  41. resistanceLowersTarget = preferences.resistanceLowersTarget
  42. skipNeutralTemps = preferences.skipNeutralTemps
  43. enableUAM = preferences.enableUAM
  44. a52RiskEnable = preferences.a52RiskEnable
  45. enableSMBWithCOB = preferences.enableSMBWithCOB
  46. enableSMBWithTemptarget = preferences.enableSMBWithTemptarget
  47. allowSMBWithHighTemptarget = preferences.allowSMBWithHighTemptarget
  48. enableSMBAlways = preferences.enableSMBAlways
  49. enableSMBAfterCarbs = preferences.enableSMBAfterCarbs
  50. rewindResetsAutosens = preferences.rewindResetsAutosens
  51. unsuspendIfNoTemp = preferences.unsuspendIfNoTemp
  52. enableSMBHighBg = preferences.enableSMB_high_bg
  53. useCustomPeakTime = preferences.useCustomPeakTime
  54. suspendZerosIob = preferences.suspendZerosIOB
  55. useNewFormula = preferences.useNewFormula
  56. sigmoid = preferences.sigmoid
  57. tddAdjBasal = preferences.tddAdjBasal
  58. // Enum properties
  59. curve = preferences.curve
  60. }
  61. }
  62. enum ProfileGenerator {
  63. /// This function is a port of the prepare/profile.js function from Trio, and it calls the core OpenAPS function
  64. static func generate(
  65. pumpSettings: PumpSettings,
  66. bgTargets: BGTargets,
  67. basalProfile: [BasalProfileEntry],
  68. isf: InsulinSensitivities,
  69. preferences: Preferences,
  70. carbRatios: CarbRatios,
  71. tempTargets: [TempTarget],
  72. model: String,
  73. clock: Date
  74. ) throws -> Profile {
  75. let model = model.replacingOccurrences(of: "\"", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
  76. guard !carbRatios.schedule.isEmpty else {
  77. throw ProfileError.invalidCarbRatio
  78. }
  79. var preferences = preferences
  80. switch (preferences.curve, preferences.useCustomPeakTime) {
  81. case (.rapidActing, true):
  82. preferences.insulinPeakTime = max(50, min(preferences.insulinPeakTime, 120))
  83. case (.rapidActing, false):
  84. preferences.insulinPeakTime = 75
  85. case (.ultraRapid, true):
  86. preferences.insulinPeakTime = max(35, min(preferences.insulinPeakTime, 100))
  87. case (.ultraRapid, false):
  88. preferences.insulinPeakTime = 55
  89. default:
  90. // don't do anything
  91. debug(.openAPS, "don't modify insulin peak time")
  92. }
  93. return try generateProfile(
  94. pumpSettings: pumpSettings,
  95. bgTargets: bgTargets,
  96. basalProfile: basalProfile,
  97. isf: isf,
  98. preferences: preferences,
  99. carbRatios: carbRatios,
  100. tempTargets: tempTargets,
  101. model: model,
  102. clock: clock
  103. )
  104. }
  105. /// Direct port of the OpenAPS profile generate function
  106. private static func generateProfile(
  107. pumpSettings: PumpSettings,
  108. bgTargets: BGTargets,
  109. basalProfile: [BasalProfileEntry],
  110. isf: InsulinSensitivities,
  111. preferences: Preferences,
  112. carbRatios: CarbRatios,
  113. tempTargets: [TempTarget],
  114. model: String,
  115. clock: Date
  116. ) throws -> Profile {
  117. var profile = Profile() // start with the defaults
  118. // check if inputs has overrides for any of the default prefs
  119. // and apply if applicable. Note, this comes from the generate/profile.js
  120. // where preferences get copied to the input then in the generate function
  121. // where it checks the input for properties that match the defaults
  122. profile.update(from: preferences)
  123. // in the Javascript version this check is for 1, but in Trio
  124. // the minimum dia you can set with the UI is 5
  125. guard pumpSettings.insulinActionCurve >= 5 else {
  126. throw ProfileError.invalidDIA(value: pumpSettings.insulinActionCurve)
  127. }
  128. profile.dia = pumpSettings.insulinActionCurve
  129. profile.model = model
  130. profile.skipNeutralTemps = preferences.skipNeutralTemps
  131. profile.currentBasal = try Basal.basalLookup(basalProfile, now: clock)
  132. profile.basalprofile = basalProfile
  133. let basalProfile = basalProfile
  134. .map { BasalProfileEntry(start: $0.start, minutes: $0.minutes, rate: $0.rate.rounded(scale: 3)) }
  135. profile.maxDailyBasal = Basal.maxDailyBasal(basalProfile)
  136. profile.maxBasal = pumpSettings.maxBasal
  137. // Error check: profile.currentBasal === 0 in Javascript
  138. if let currentBasal = profile.currentBasal {
  139. guard currentBasal != 0 else {
  140. throw ProfileError.invalidCurrentBasal(value: profile.currentBasal)
  141. }
  142. }
  143. // Error check: profile.max_daily_basal === 0 in Javascript
  144. if let maxDailyBasal = profile.maxDailyBasal {
  145. guard maxDailyBasal != 0 else {
  146. throw ProfileError.invalidMaxDailyBasal(value: profile.maxDailyBasal)
  147. }
  148. }
  149. // Error check: profile.max_basal < 0.1 in Javascript
  150. if let maxBasal = profile.maxBasal {
  151. guard maxBasal >= 0.1 else {
  152. throw ProfileError.invalidMaxBasal(value: profile.maxBasal)
  153. }
  154. }
  155. profile.outUnits = bgTargets.userPreferredUnits
  156. let (updatedTargets, range) = try Targets
  157. .bgTargetsLookup(targets: bgTargets, tempTargets: tempTargets, profile: profile, now: clock)
  158. profile.minBg = range.minBg?.rounded()
  159. profile.maxBg = range.maxBg?.rounded()
  160. // Note: we're using updatedTargets here because in Javascript the bgTargetsLookup
  161. // function mutates the input, so we want the mutated version in the
  162. // profile and we need to round the properties
  163. let roundedTargets = updatedTargets.targets.map { target -> ComputedBGTargetEntry in
  164. ComputedBGTargetEntry(
  165. low: target.low.rounded(),
  166. high: target.high.rounded(),
  167. start: target.start,
  168. offset: target.offset,
  169. maxBg: target.maxBg?.rounded(),
  170. minBg: target.minBg?.rounded(),
  171. temptargetSet: target.temptargetSet
  172. )
  173. }
  174. // Set the rounded targets on the profile
  175. profile.bgTargets = ComputedBGTargets(
  176. units: updatedTargets.units,
  177. userPreferredUnits: updatedTargets.userPreferredUnits,
  178. targets: roundedTargets
  179. )
  180. profile.temptargetSet = range.temptargetSet
  181. let (sens, isfUpdated) = try Isf.isfLookup(isfDataInput: isf, timestamp: clock)
  182. profile.sens = sens
  183. profile.isfProfile = isfUpdated
  184. // Error check: profile.sens < 5 in Javascript
  185. if let sens = profile.sens {
  186. guard sens >= 5 else {
  187. debug(.openAPS, "ISF of \(String(describing: profile.sens)) is not supported")
  188. throw ProfileError.invalidISF(value: profile.sens)
  189. }
  190. }
  191. // Handle carb ratio data
  192. guard let currentCarbRatio = Carbs.carbRatioLookup(carbRatio: carbRatios, now: clock) else {
  193. throw ProfileError.invalidCarbRatio
  194. }
  195. profile.carbRatio = currentCarbRatio
  196. profile.carbRatios = carbRatios
  197. return profile
  198. }
  199. }