ProfileGenerator.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. // Bool properties
  36. highTemptargetRaisesSensitivity = preferences.highTemptargetRaisesSensitivity
  37. lowTemptargetLowersSensitivity = preferences.lowTemptargetLowersSensitivity
  38. sensitivityRaisesTarget = preferences.sensitivityRaisesTarget
  39. resistanceLowersTarget = preferences.resistanceLowersTarget
  40. exerciseMode = preferences.exerciseMode
  41. skipNeutralTemps = preferences.skipNeutralTemps
  42. enableUAM = preferences.enableUAM
  43. a52RiskEnable = preferences.a52RiskEnable
  44. enableSMBWithCOB = preferences.enableSMBWithCOB
  45. enableSMBWithTemptarget = preferences.enableSMBWithTemptarget
  46. allowSMBWithHighTemptarget = preferences.allowSMBWithHighTemptarget
  47. enableSMBAlways = preferences.enableSMBAlways
  48. enableSMBAfterCarbs = preferences.enableSMBAfterCarbs
  49. rewindResetsAutosens = preferences.rewindResetsAutosens
  50. unsuspendIfNoTemp = preferences.unsuspendIfNoTemp
  51. enableSMBHighBg = preferences.enableSMB_high_bg
  52. useCustomPeakTime = preferences.useCustomPeakTime
  53. suspendZerosIob = preferences.suspendZerosIOB
  54. useNewFormula = preferences.useNewFormula
  55. enableDynamicCR = preferences.enableDynamicCR
  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. autotune _: Autotune?,
  74. freeaps _: FreeAPSSettings
  75. ) throws -> Profile {
  76. let bgTargets = bgTargets.inMgDl()
  77. let isf = isf.inMgDl()
  78. let model = model.replacingOccurrences(of: "\"", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
  79. guard !carbRatios.schedule.isEmpty else {
  80. throw ProfileError.invalidCarbRatio
  81. }
  82. var preferences = preferences
  83. switch (preferences.curve, preferences.useCustomPeakTime) {
  84. case (.rapidActing, true):
  85. preferences.insulinPeakTime = max(50, min(preferences.insulinPeakTime, 120))
  86. case (.rapidActing, false):
  87. preferences.insulinPeakTime = 75
  88. case (.ultraRapid, true):
  89. preferences.insulinPeakTime = max(35, min(preferences.insulinPeakTime, 100))
  90. case (.ultraRapid, false):
  91. preferences.insulinPeakTime = 55
  92. default:
  93. // don't do anything
  94. print("don't modify insulin peak time")
  95. }
  96. /* From Javascript
  97. for (var pref in preferences) {
  98. if (preferences.hasOwnProperty(pref)) {
  99. inputs[pref] = preferences[pref];
  100. }
  101. }
  102. inputs.max_iob = inputs.max_iob || 0;
  103. */
  104. // we don't need the JS logic above because it is handled by
  105. // our update function
  106. // in Trio it looks like autotune is always null
  107. /*
  108. var basalProfile = basalProfile
  109. var carbRatios = carbRatios
  110. if let autotune = autotune {
  111. if let basal = autotune.basalProfile {
  112. basalProfile = basal
  113. }
  114. // onlyAutotuneBasals is not defined in Swift
  115. if let isfProfile = autotune.isfProfile {
  116. // TODO: should we convert this to mg/dL as well?
  117. isf = isfProfile
  118. }
  119. if let carbRatio = autotune.carbRatio {
  120. carbRatios.schedule[0].ratio = carbRatio
  121. }
  122. }
  123. */
  124. return try generate(
  125. pumpSettings: pumpSettings,
  126. bgTargets: bgTargets,
  127. basalProfile: basalProfile,
  128. isf: isf,
  129. preferences: preferences,
  130. carbRatios: carbRatios,
  131. tempTargets: tempTargets,
  132. model: model
  133. )
  134. }
  135. /// Direct port of the OpenASP profile generate function
  136. static func generate(
  137. pumpSettings: PumpSettings,
  138. bgTargets: BGTargets,
  139. basalProfile: [BasalProfileEntry],
  140. isf: InsulinSensitivities,
  141. preferences: Preferences,
  142. carbRatios: CarbRatios,
  143. tempTargets: [TempTarget],
  144. model: String
  145. ) throws -> Profile {
  146. // var profile = opts && opts.type ? opts : defaults( );
  147. var profile = Profile() // uses defaults
  148. // check if inputs has overrides for any of the default prefs
  149. // and apply if applicable. Note, this comes from the generate/profile.js
  150. // where preferences get copied to the input then in the generate function
  151. // where it checks the input for properties that match the defaults
  152. profile.update(from: preferences)
  153. if pumpSettings.insulinActionCurve > 1 {
  154. profile.dia = pumpSettings.insulinActionCurve
  155. } else {
  156. throw ProfileError.invalidDIA(value: pumpSettings.insulinActionCurve)
  157. }
  158. profile.model = model
  159. profile.skipNeutralTemps = preferences.skipNeutralTemps
  160. profile.currentBasal = try Basal.basalLookup(basalProfile)
  161. profile.basalprofile = basalProfile
  162. let basalProfile = basalProfile
  163. .map { BasalProfileEntry(start: $0.start, minutes: $0.minutes, rate: $0.rate.rounded(scale: 3)) }
  164. profile.maxDailyBasal = Basal.maxDailyBasal(basalProfile)
  165. profile.maxBasal = pumpSettings.maxBasal
  166. // this check is an error check profile.currentBasal === 0 in Javascript
  167. guard let currentBasal = profile.currentBasal, abs(currentBasal) > 0 else {
  168. throw ProfileError.invalidCurrentBasal(value: profile.currentBasal)
  169. }
  170. guard let maxDailyBasal = profile.maxDailyBasal, abs(maxDailyBasal) > 0 else {
  171. throw ProfileError.invalidMaxDailyBasal(value: profile.maxDailyBasal)
  172. }
  173. guard let maxBasal = profile.maxBasal, maxBasal >= 0.1 else {
  174. throw ProfileError.invalidMaxBasal(value: profile.maxBasal)
  175. }
  176. // var range = targets.bgTargetsLookup(inputs, profile);
  177. profile.outUnits = bgTargets.userPreferredUnits.rawValue
  178. let (updatedTargets, range) = try Targets.bgTargetsLookup(targets: bgTargets, tempTargets: tempTargets, profile: profile)
  179. // profile.min_bg = Math.round(range.min_bg);
  180. // profile.max_bg = Math.round(range.max_bg);
  181. profile.minBg = range.minBg?.rounded()
  182. profile.maxBg = range.maxBg?.rounded()
  183. // Note: we're using updatedTargets here because in Javascript the bgTargetsLookup
  184. // function mutates the input, so we want the mutated version in the
  185. // profile and we need to round the properties
  186. let roundedTargets = updatedTargets.targets.map { target -> ComputedBGTargetEntry in
  187. ComputedBGTargetEntry(
  188. low: target.low.rounded(),
  189. high: target.high.rounded(),
  190. start: target.start,
  191. offset: target.offset,
  192. maxBg: target.maxBg?.rounded(),
  193. minBg: target.minBg?.rounded(),
  194. temptargetSet: target.temptargetSet
  195. )
  196. }
  197. // Set the rounded targets on the profile
  198. profile.bgTargets = ComputedBGTargets(
  199. units: updatedTargets.units,
  200. userPreferredUnits: updatedTargets.userPreferredUnits,
  201. targets: roundedTargets
  202. )
  203. // delete profile.bg_targets.raw;
  204. // Note: we don't need this in Swift as we don't have the raw property
  205. profile.temptargetSet = range.temptargetSet
  206. let (sens, isfUpdated) = try Isf.isfLookup(isfDataInput: isf)
  207. profile.sens = sens
  208. profile.isfProfile = isfUpdated
  209. guard let sens = profile.sens, sens >= 5 else {
  210. print("ISF of \(String(describing: profile.sens)) is not supported")
  211. throw ProfileError.invalidISF(value: profile.sens)
  212. }
  213. // Handle carb ratio data
  214. guard let currentCarbRatio = Carbs.carbRatioLookup(carbRatio: carbRatios) else {
  215. throw ProfileError.invalidCarbRatio
  216. }
  217. profile.carbRatio = currentCarbRatio
  218. profile.carbRatios = carbRatios
  219. return profile
  220. }
  221. }