| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- import Foundation
- extension Profile {
- /// Updates profile properties from preferences where CodingKeys match
- /// This function ended up being pretty ugly, but I couldn't think of a cleaner
- /// way. I considered converting to JSON or using Mirror, but these weren't
- /// great so in the end I think that this approach is simpliest.
- ///
- /// Also, this implementation does _not_ copy any of the optional properties
- /// since these should get set in the `generate` method.
- mutating func update(from preferences: Preferences) {
- // Decimal properties
- maxIob = preferences.maxIOB
- min5mCarbImpact = preferences.min5mCarbimpact
- maxCOB = preferences.maxCOB
- maxDailySafetyMultiplier = preferences.maxDailySafetyMultiplier
- currentBasalSafetyMultiplier = preferences.currentBasalSafetyMultiplier
- autosensMax = preferences.autosensMax
- autosensMin = preferences.autosensMin
- halfBasalExerciseTarget = preferences.halfBasalExerciseTarget
- remainingCarbsCap = preferences.remainingCarbsCap
- smbInterval = preferences.smbInterval
- maxSMBBasalMinutes = preferences.maxSMBBasalMinutes
- maxUAMSMBBasalMinutes = preferences.maxUAMSMBBasalMinutes
- bolusIncrement = preferences.bolusIncrement
- carbsReqThreshold = preferences.carbsReqThreshold
- remainingCarbsFraction = preferences.remainingCarbsFraction
- enableSMBHighBgTarget = preferences.enableSMB_high_bg_target
- maxDeltaBgThreshold = preferences.maxDeltaBGthreshold
- insulinPeakTime = preferences.insulinPeakTime
- noisyCGMTargetMultiplier = preferences.noisyCGMTargetMultiplier
- adjustmentFactor = preferences.adjustmentFactor
- adjustmentFactorSigmoid = preferences.adjustmentFactorSigmoid
- weightPercentage = preferences.weightPercentage
- thresholdSetting = preferences.threshold_setting
- maxMealAbsorptionTime = preferences.maxMealAbsorptionTime
- smbDeliveryRatio = preferences.smbDeliveryRatio
- // Bool properties
- highTemptargetRaisesSensitivity = preferences.highTemptargetRaisesSensitivity
- lowTemptargetLowersSensitivity = preferences.lowTemptargetLowersSensitivity
- sensitivityRaisesTarget = preferences.sensitivityRaisesTarget
- resistanceLowersTarget = preferences.resistanceLowersTarget
- skipNeutralTemps = preferences.skipNeutralTemps
- enableUAM = preferences.enableUAM
- a52RiskEnable = preferences.a52RiskEnable
- enableSMBWithCOB = preferences.enableSMBWithCOB
- enableSMBWithTemptarget = preferences.enableSMBWithTemptarget
- allowSMBWithHighTemptarget = preferences.allowSMBWithHighTemptarget
- enableSMBAlways = preferences.enableSMBAlways
- enableSMBAfterCarbs = preferences.enableSMBAfterCarbs
- rewindResetsAutosens = preferences.rewindResetsAutosens
- unsuspendIfNoTemp = preferences.unsuspendIfNoTemp
- enableSMBHighBg = preferences.enableSMB_high_bg
- useCustomPeakTime = preferences.useCustomPeakTime
- suspendZerosIob = preferences.suspendZerosIOB
- useNewFormula = preferences.useNewFormula
- sigmoid = preferences.sigmoid
- tddAdjBasal = preferences.tddAdjBasal
- // Enum properties
- curve = preferences.curve
- }
- }
- enum ProfileGenerator {
- /// This function is a port of the prepare/profile.js function from Trio, and it calls the core OpenAPS function
- static func generate(
- pumpSettings: PumpSettings,
- bgTargets: BGTargets,
- basalProfile: [BasalProfileEntry],
- isf: InsulinSensitivities,
- preferences: Preferences,
- carbRatios: CarbRatios,
- tempTargets: [TempTarget],
- model: String,
- clock: Date
- ) throws -> Profile {
- let model = model.replacingOccurrences(of: "\"", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
- guard !carbRatios.schedule.isEmpty else {
- throw ProfileError.invalidCarbRatio
- }
- var preferences = preferences
- switch (preferences.curve, preferences.useCustomPeakTime) {
- case (.rapidActing, true):
- preferences.insulinPeakTime = max(50, min(preferences.insulinPeakTime, 120))
- case (.rapidActing, false):
- preferences.insulinPeakTime = 75
- case (.ultraRapid, true):
- preferences.insulinPeakTime = max(35, min(preferences.insulinPeakTime, 100))
- case (.ultraRapid, false):
- preferences.insulinPeakTime = 55
- default:
- // don't do anything
- debug(.openAPS, "don't modify insulin peak time")
- }
- return try generateProfile(
- pumpSettings: pumpSettings,
- bgTargets: bgTargets,
- basalProfile: basalProfile,
- isf: isf,
- preferences: preferences,
- carbRatios: carbRatios,
- tempTargets: tempTargets,
- model: model,
- clock: clock
- )
- }
- /// Direct port of the OpenAPS profile generate function
- private static func generateProfile(
- pumpSettings: PumpSettings,
- bgTargets: BGTargets,
- basalProfile: [BasalProfileEntry],
- isf: InsulinSensitivities,
- preferences: Preferences,
- carbRatios: CarbRatios,
- tempTargets: [TempTarget],
- model: String,
- clock: Date
- ) throws -> Profile {
- var profile = Profile() // start with the defaults
- // check if inputs has overrides for any of the default prefs
- // and apply if applicable. Note, this comes from the generate/profile.js
- // where preferences get copied to the input then in the generate function
- // where it checks the input for properties that match the defaults
- profile.update(from: preferences)
- // in the Javascript version this check is for 1, but in Trio
- // the minimum dia you can set with the UI is 5
- guard pumpSettings.insulinActionCurve >= 5 else {
- throw ProfileError.invalidDIA(value: pumpSettings.insulinActionCurve)
- }
- profile.dia = pumpSettings.insulinActionCurve
- profile.model = model
- profile.skipNeutralTemps = preferences.skipNeutralTemps
- profile.currentBasal = try Basal.basalLookup(basalProfile, now: clock)
- profile.basalprofile = basalProfile
- let basalProfile = basalProfile
- .map { BasalProfileEntry(start: $0.start, minutes: $0.minutes, rate: $0.rate.rounded(scale: 3)) }
- profile.maxDailyBasal = Basal.maxDailyBasal(basalProfile)
- profile.maxBasal = pumpSettings.maxBasal
- // Error check: profile.currentBasal === 0 in Javascript
- if let currentBasal = profile.currentBasal {
- guard currentBasal != 0 else {
- throw ProfileError.invalidCurrentBasal(value: profile.currentBasal)
- }
- }
- // Error check: profile.max_daily_basal === 0 in Javascript
- if let maxDailyBasal = profile.maxDailyBasal {
- guard maxDailyBasal != 0 else {
- throw ProfileError.invalidMaxDailyBasal(value: profile.maxDailyBasal)
- }
- }
- // Error check: profile.max_basal < 0.1 in Javascript
- if let maxBasal = profile.maxBasal {
- guard maxBasal >= 0.1 else {
- throw ProfileError.invalidMaxBasal(value: profile.maxBasal)
- }
- }
- profile.outUnits = bgTargets.userPreferredUnits
- let (updatedTargets, range) = try Targets
- .bgTargetsLookup(targets: bgTargets, tempTargets: tempTargets, profile: profile, now: clock)
- profile.minBg = range.minBg?.rounded()
- profile.maxBg = range.maxBg?.rounded()
- // Note: we're using updatedTargets here because in Javascript the bgTargetsLookup
- // function mutates the input, so we want the mutated version in the
- // profile and we need to round the properties
- let roundedTargets = updatedTargets.targets.map { target -> ComputedBGTargetEntry in
- ComputedBGTargetEntry(
- low: target.low.rounded(),
- high: target.high.rounded(),
- start: target.start,
- offset: target.offset,
- maxBg: target.maxBg?.rounded(),
- minBg: target.minBg?.rounded(),
- temptargetSet: target.temptargetSet
- )
- }
- // Set the rounded targets on the profile
- profile.bgTargets = ComputedBGTargets(
- units: updatedTargets.units,
- userPreferredUnits: updatedTargets.userPreferredUnits,
- targets: roundedTargets
- )
- profile.temptargetSet = range.temptargetSet
- let (sens, isfUpdated) = try Isf.isfLookup(isfDataInput: isf, timestamp: clock)
- profile.sens = sens
- profile.isfProfile = isfUpdated
- // Error check: profile.sens < 5 in Javascript
- if let sens = profile.sens {
- guard sens >= 5 else {
- debug(.openAPS, "ISF of \(String(describing: profile.sens)) is not supported")
- throw ProfileError.invalidISF(value: profile.sens)
- }
- }
- // Handle carb ratio data
- guard let currentCarbRatio = Carbs.carbRatioLookup(carbRatio: carbRatios, now: clock) else {
- throw ProfileError.invalidCarbRatio
- }
- profile.carbRatio = currentCarbRatio
- profile.carbRatios = carbRatios
- return profile
- }
- }
|