Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions LoopKit/JSONStreamEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,19 @@ public class JSONStreamEncoder {
}

for value in values {
try stream.write(encoded ? ",\n" : "[\n")
try stream.write(try Self.encoder.encode(value))
encoded = true
do {
let encodedData = try Self.encoder.encode(value)
try stream.write(encoded ? ",\n" : "[\n")
try stream.write(encodedData)
encoded = true
} catch {
// Log the error and insert a placeholder or error message in the JSON
print("Failed to encode value: \(value) with error: \(error.localizedDescription)")
let errorInfo = "{\"error\": \"Failed to encode value: \(value) due to: \(error.localizedDescription)\"}"
try stream.write(encoded ? ",\n" : "[\n")
try stream.write(errorInfo.data(using: .utf8)!)
encoded = true
}
}
}

Expand Down
66 changes: 65 additions & 1 deletion LoopKit/LoopAlgorithm/LoopAlgorithm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum AlgorithmError: Error {

public struct LoopAlgorithmEffects {
public var insulin: [GlucoseEffect]
public var negativeInsulinDamper: [GlucoseEffect]
public var carbs: [GlucoseEffect]
public var retrospectiveCorrection: [GlucoseEffect]
public var momentum: [GlucoseEffect]
Expand All @@ -29,8 +30,9 @@ public struct AlgorithmEffectsOptions: OptionSet {
public static let insulin = AlgorithmEffectsOptions(rawValue: 1 << 1)
public static let momentum = AlgorithmEffectsOptions(rawValue: 1 << 2)
public static let retrospection = AlgorithmEffectsOptions(rawValue: 1 << 3)
public static let damper = AlgorithmEffectsOptions(rawValue: 1 << 4)

public static let all: AlgorithmEffectsOptions = [.carbs, .insulin, .momentum, .retrospection]
public static let all: AlgorithmEffectsOptions = [.carbs, .insulin, .momentum, .retrospection, .damper]

public init(rawValue: UInt8) {
self.rawValue = rawValue
Expand Down Expand Up @@ -166,6 +168,67 @@ public actor LoopAlgorithm {
}

var prediction = LoopMath.predictGlucose(startingAt: latestGlucose, momentum: momentumEffects, effects: effects)

var damperEffects = [GlucoseEffect]()

if settings.algorithmEffectsOptions.contains(.damper) {
var posDeltaSum = 0.0
insulinEffects.enumerated().forEach{
let delta : Double
if $0.offset == 0 {
delta = 0
} else {
delta = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter) - insulinEffects[$0.offset - 1].quantity.doubleValue(for: .milligramsPerDeciliter)
}
posDeltaSum += max(0, delta)
}

// NID will change the final prediction so that positive changes will be multiplied by weight alpha
// the long term slope will be marginalSlope
// in the initial linear scaling region alpha will be anchorAlpha at anchorPoint
let marginalSlope = 0.1
let anchorPoint = 50.0 // FIXME change to basal * ISF
let anchorAlpha = 0.8

let linearScaleSlope = (1.0 - anchorAlpha)/anchorPoint // how alpha scales down in the linear scale region

// the slope in the linear scale region of alpha * posDeltaSum is 1 - 2*linearScaleSlope*posDeltaSum.
// the transitionPoint is where we transition from linear scale region to marginalSlope. The slope is continuous at this point
let transitionPoint = (1 - marginalSlope) / (2 * linearScaleSlope)

let alpha : Double
if posDeltaSum < transitionPoint { // linear scaling region
alpha = 1 - linearScaleSlope * posDeltaSum
} else { // marginal slope region
let transitionValue = (1 - linearScaleSlope * transitionPoint) * transitionPoint
alpha = (transitionValue + marginalSlope * (posDeltaSum - transitionPoint)) / posDeltaSum
}

var dampedPrediction = [PredictedGlucoseValue]()
var value = 0.0
prediction.enumerated().forEach{

if $0.offset == 0 {
value = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter)
dampedPrediction.append($0.element)
damperEffects.append(GlucoseEffect(startDate: $0.element.startDate, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0)))
return
}
let currValue = $0.element.quantity.doubleValue(for: .milligramsPerDeciliter)
let delta = currValue - prediction[$0.offset - 1].quantity.doubleValue(for: .milligramsPerDeciliter)

if delta > 0 {
value += alpha * delta
} else {
value += delta
}

dampedPrediction.append(PredictedGlucoseValue(startDate: $0.element.startDate, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value)))
damperEffects.append(GlucoseEffect(startDate: $0.element.startDate, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value - currValue)))
}

prediction = dampedPrediction
}

// Dosing requires prediction entries at least as long as the insulin model duration.
// If our prediction is shorter than that, then extend it here.
Expand All @@ -178,6 +241,7 @@ public actor LoopAlgorithm {
glucose: prediction,
effects: LoopAlgorithmEffects(
insulin: insulinEffects,
negativeInsulinDamper: damperEffects,
carbs: carbEffects,
retrospectiveCorrection: rcEffect,
momentum: momentumEffects,
Expand Down
11 changes: 10 additions & 1 deletion LoopKit/LoopAlgorithm/LoopAlgorithmSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public struct LoopAlgorithmSettings {
public var maximumBolus: Double? = nil
public var suspendThreshold: GlucoseThreshold? = nil
public var useIntegralRetrospectiveCorrection: Bool = false
public var useNegativeInsulinDamper: Bool = false

public init(
basal: [AbsoluteScheduleValue<Double>],
Expand All @@ -41,7 +42,8 @@ public struct LoopAlgorithmSettings {
maximumBasalRatePerHour: Double? = nil,
maximumBolus: Double? = nil,
suspendThreshold: GlucoseThreshold? = nil,
useIntegralRetrospectiveCorrection: Bool = false)
useIntegralRetrospectiveCorrection: Bool = false,
useNegativeInsulinDamper: Bool = false)
{
self.basal = basal
self.sensitivity = sensitivity
Expand All @@ -54,6 +56,7 @@ public struct LoopAlgorithmSettings {
self.maximumBolus = maximumBolus
self.suspendThreshold = suspendThreshold
self.useIntegralRetrospectiveCorrection = useIntegralRetrospectiveCorrection
self.useNegativeInsulinDamper = useNegativeInsulinDamper
}
}

Expand All @@ -79,6 +82,8 @@ extension LoopAlgorithmSettings: Codable {
self.maximumBolus = try container.decodeIfPresent(Double.self, forKey: .maximumBolus)
self.suspendThreshold = try container.decodeIfPresent(GlucoseThreshold.self, forKey: .suspendThreshold)
self.useIntegralRetrospectiveCorrection = try container.decodeIfPresent(Bool.self, forKey: .useIntegralRetrospectiveCorrection) ?? false
self.useNegativeInsulinDamper = try container.decodeIfPresent(Bool.self, forKey: .useNegativeInsulinDamper) ?? false

}

public func encode(to encoder: Encoder) throws {
Expand All @@ -100,6 +105,9 @@ extension LoopAlgorithmSettings: Codable {
if useIntegralRetrospectiveCorrection {
try container.encode(useIntegralRetrospectiveCorrection, forKey: .useIntegralRetrospectiveCorrection)
}
if useNegativeInsulinDamper {
try container.encode(useNegativeInsulinDamper, forKey: .useNegativeInsulinDamper)
}
}

private enum CodingKeys: String, CodingKey {
Expand All @@ -114,6 +122,7 @@ extension LoopAlgorithmSettings: Codable {
case maximumBolus
case suspendThreshold
case useIntegralRetrospectiveCorrection
case useNegativeInsulinDamper
}
}

Expand Down
10 changes: 3 additions & 7 deletions LoopKitUI/CarbKit/CarbQuantityRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public struct CarbQuantityRow: View {
private let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumIntegerDigits = 3
formatter.maximumFractionDigits = 1
return formatter
}()
Expand Down Expand Up @@ -76,13 +77,8 @@ public struct CarbQuantityRow: View {

// Update quantity based on text field input
private func updateQuantity(with input: String) {
let filtered = input.filter { "0123456789.".contains($0) }
if filtered != input {
self.carbInput = filtered
}

if let doubleValue = Double(filtered) {
quantity = doubleValue
if let number = formatter.number(from: input) {
quantity = number.doubleValue
} else {
quantity = nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,8 @@ public final class OverrideSelectionViewController: UICollectionViewController,
return originalIndexPath
}

return proposedIndexPath == indexPathOfCustomOverride()
let customPresetRow = self.collectionView(collectionView, numberOfItemsInSection: proposedIndexPath.section) - 2
return proposedIndexPath.row >= customPresetRow
? originalIndexPath
: proposedIndexPath

Expand Down
4 changes: 2 additions & 2 deletions LoopKitUI/Views/CheckmarkListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ public struct CheckmarkListItem: View {
.foregroundColor(.accentColor)
} else {
Circle()
.stroke()
.foregroundColor(Color(.systemGray4))
.stroke(lineWidth: 2)
.foregroundColor(Color(.systemGray))
}
}

Expand Down