Skip to content

Commit b05f72b

Browse files
ps2Pete Schwamb
andauthored
Make decayEffect a continuous function of sample timestamp (#33)
* Make decayEffect a continuous function of sample timestamp Reformulates decayEffect using a closed-form quadratic in time-since-sample rather than accumulating step-by-step from the floored simulation boundary. This makes the effect value at any future absolute timestamp independent of which delta-sized simulation bucket the sample's startDate falls into. For samples aligned to delta boundaries the two formulations are mathematically identical. For unaligned samples (the common case with real CGM streams) the new formulation removes a small discontinuity that the old code exhibited at bucket boundaries. Adds LoopMathTests covering continuity across a delta boundary. Existing fixture-calibrated tests are re-pinned to the new values; per-prediction drift is on the order of 0.1 mg/dL. Ports the LoopMath change from LoopKit/LoopKit#556 by Moti Nisenson-Ken to the LoopAlgorithm package, where decayEffect now lives. * Space to kick off tests --------- Co-authored-by: Pete Schwamb <pete@pete-mbp-eth.maplect.net>
1 parent bd1a879 commit b05f72b

6 files changed

Lines changed: 152 additions & 98 deletions

File tree

Sources/LoopAlgorithm/LoopMath.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,19 +192,25 @@ extension GlucoseValue {
192192
let glucoseUnit = LoopUnit.milligramsPerDeciliter
193193
let velocityUnit = GlucoseEffectVelocity.perSecondUnit
194194

195-
// The starting rate, which we will decay to 0 over the specified duration
196-
let intercept = rate.doubleValue(for: velocityUnit) // mg/dL/s
197-
let decayStartDate = startDate.addingTimeInterval(delta)
198-
let slope = -intercept / (duration - delta) // mg/dL/s/s
195+
let firstChange = rate.doubleValue(for: velocityUnit) * delta // mg/dL/s * s = mg/dL
196+
let secondChange = firstChange * (1 - delta / (duration - delta))
197+
198+
// Solve for f(t) = a*t^2 + b*t + c, where t is relative to self.startDate.
199+
// f(0) = c
200+
// f(delta) - c = firstChange = a*delta^2 + b*delta
201+
// f(2*delta) - c = firstChange + secondChange = 4*a*delta^2 + 2*b*delta
202+
// --> firstChange - secondChange = 2*a*delta^2
203+
let c = quantity.doubleValue(for: glucoseUnit)
204+
let a = (secondChange - firstChange) / (2 * delta * delta) // mg/dL/s^2
205+
let b = (firstChange + secondChange - 4 * a * delta * delta) / (2 * delta) // mg/dL/s
199206

200207
var values = [GlucoseEffect(startDate: startDate, quantity: quantity)]
201-
var date = decayStartDate
202-
var lastValue = quantity.doubleValue(for: glucoseUnit)
208+
var date = startDate.addingTimeInterval(delta)
203209

204210
repeat {
205-
let value = lastValue + (intercept + slope * date.timeIntervalSince(decayStartDate)) * delta
211+
let time = min(duration, date.timeIntervalSince(self.startDate))
212+
let value = a * time * time + b * time + c
206213
values.append(GlucoseEffect(startDate: date, quantity: LoopQuantity(unit: glucoseUnit, doubleValue: value)))
207-
lastValue = value
208214
date = date.addingTimeInterval(delta)
209215
} while date < endDate
210216

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"manual" : {
3-
"amount" : 10.546890782709953
3+
"amount" : 10.52269112701204
44
}
5-
}
5+
}

0 commit comments

Comments
 (0)