forked from Artificial-Pancreas/iAPS
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add calibration functions allowing to transform a BG to a corrected BG with a linear function. Add interface to manage calibrations points add calibration to calculate a new value of BG add notification to remove calibration when change CGM or sensors This functionality is activated only for Libre CGM even could be use for all CGM. (cherry picked from commit 6bdd3a21729b992ebbdc6709e3f62738659ac8ff)
- Loading branch information
Showing
13 changed files
with
535 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import Foundation | ||
import LibreTransmitter | ||
import Swinject | ||
|
||
struct Calibration: JSON, Hashable, Identifiable { | ||
let x: Double | ||
let y: Double | ||
var date = Date() | ||
|
||
static let zero = Calibration(x: 0, y: 0) | ||
|
||
var id = UUID() | ||
} | ||
|
||
protocol CalibrationService { | ||
var slope: Double { get } | ||
var intercept: Double { get } | ||
var calibrations: [Calibration] { get } | ||
|
||
func addCalibration(_ calibration: Calibration) | ||
func removeCalibration(_ calibration: Calibration) | ||
func removeAllCalibrations() | ||
func removeLast() | ||
|
||
func calibrate(value: Int) -> Double | ||
} | ||
|
||
final class BaseCalibrationService: CalibrationService, Injectable { | ||
private enum Config { | ||
static let minSlope = 0.8 | ||
static let maxSlope = 1.25 | ||
static let minIntercept = -100.0 | ||
static let maxIntercept = 100.0 | ||
static let maxValue = 500.0 | ||
static let minValue = 0.0 | ||
} | ||
|
||
@Injected() var storage: FileStorage! | ||
@Injected() var notificationCenter: NotificationCenter! | ||
private var lifetime = Lifetime() | ||
|
||
private(set) var calibrations: [Calibration] = [] { | ||
didSet { | ||
storage.save(calibrations, as: OpenAPS.FreeAPS.calibrations) | ||
} | ||
} | ||
|
||
init(resolver: Resolver) { | ||
injectServices(resolver) | ||
calibrations = storage.retrieve(OpenAPS.FreeAPS.calibrations, as: [Calibration].self) ?? [] | ||
subscribe() | ||
} | ||
|
||
private func subscribe() { | ||
// notificationCenter.publisher(for: .newSensorDetected) | ||
// .sink { [weak self] _ in | ||
// self?.removeAllCalibrations() | ||
// } | ||
// .store(in: &lifetime) | ||
} | ||
|
||
var slope: Double { | ||
guard calibrations.count >= 2 else { | ||
return 1 | ||
} | ||
|
||
let xs = calibrations.map(\.x) | ||
let ys = calibrations.map(\.y) | ||
let sum1 = average(multiply(xs, ys)) - average(xs) * average(ys) | ||
let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2) | ||
let slope = sum1 / sum2 | ||
|
||
return min(max(slope, Config.minSlope), Config.maxSlope) | ||
} | ||
|
||
var intercept: Double { | ||
guard calibrations.count >= 1 else { | ||
return 0 | ||
} | ||
let xs = calibrations.map(\.x) | ||
let ys = calibrations.map(\.y) | ||
|
||
let intercept = average(ys) - slope * average(xs) | ||
|
||
return min(max(intercept, Config.minIntercept), Config.maxIntercept) | ||
} | ||
|
||
func calibrate(value: Int) -> Double { | ||
linearRegression(value) | ||
} | ||
|
||
func addCalibration(_ calibration: Calibration) { | ||
calibrations.append(calibration) | ||
} | ||
|
||
func removeCalibration(_ calibration: Calibration) { | ||
calibrations.removeAll { $0 == calibration } | ||
} | ||
|
||
func removeAllCalibrations() { | ||
calibrations.removeAll() | ||
} | ||
|
||
func removeLast() { | ||
calibrations.removeLast() | ||
} | ||
|
||
private func average(_ input: [Double]) -> Double { | ||
input.reduce(0, +) / Double(input.count) | ||
} | ||
|
||
private func multiply(_ a: [Double], _ b: [Double]) -> [Double] { | ||
zip(a, b).map(*) | ||
} | ||
|
||
private func linearRegression(_ x: Int) -> Double { | ||
(intercept + slope * Double(x)).clamped(Config.minValue ... Config.maxValue) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.