Skip to content

Take plain numbers in fixed units at the public API - #17

Merged
ps2 merged 3 commits into
mainfrom
plain-numbers-api
Nov 1, 2026
Merged

ps2 merged 3 commits into
mainfrom
plain-numbers-api

Conversation

@ps2

@ps2 ps2 commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

LoopKit depends on LoopAlgorithm today only for LoopQuantity and LoopUnit, so an app that wants LoopKit without the algorithm (Trio) still has to build LoopAlgorithm. This changes LoopAlgorithm's public API to plain Doubles in fixed units. LoopKit can then own its unit types, and neither package depends on the other, with no new shared package.

This is the first of a set of changes. LoopKit and Loop PRs follow, and Loop becomes the only place that converts between LoopKit's types and LoopAlgorithm's (see the plan below).

API changes

Before After
Glucose quantity: LoopQuantity mgdl: Double (GlucoseValue, GlucoseEffect, PredictedGlucoseValue, GlucoseChange)
Carbs quantity grams: Double (CarbEntry, CarbStatus, AbsorbedCarbValue.observedGrams etc.)
Velocity quantity mgdlPerSecond: Double
Sensitivity sensitivity: [AbsoluteScheduleValue<LoopQuantity>] sensitivityMgdlPerUnit: [AbsoluteScheduleValue<Double>]
Target target: [AbsoluteScheduleValue<ClosedRange<LoopQuantity>>] targetMgdl: GlucoseRangeTimeline (ClosedRange<Double>)
Suspend threshold suspendThreshold: LoopQuantity? suspendThresholdMgdl: Double?
Momentum limit momentumVelocityMaximum: LoopQuantity? momentumVelocityMaximumMgdlPerMinute: Double?

The plain-number inputs carry their unit in the name, both on AlgorithmInput and in the public overloads (generatePrediction, PrecomputedInsulinInput.withEffects/build, the glucoseEffects and carb-effect overloads). That way an mmol/L value passed by mistake stands out at the call site instead of compiling unnoticed. The Loop PR adds an end-to-end mmol/L test as well.

  • LoopQuantity and LoopUnit are now internal. Internally the math still uses them, so results are unchanged.
  • The LoopAlgorithm struct is renamed Algorithm, so LoopAlgorithm.X always names the module. Call sites become Algorithm.run(input:) and Algorithm.generatePrediction(...).
  • GlucoseSampleValue drops trendRate and condition; the algorithm never read them. FixtureGlucoseSample.condition and the internal GlucoseCondition go too.
  • run converts the input schedules to quantities once at the start.
  • Helpers apps don't call (retrospective correction types, insulinCorrection, recommendTempBasal, etc.) are internal. The fixture JSON format is unchanged.
  • LoopKit's tests of LoopMath (prediction, momentum blending, ICE − carb effect, decay), date rounding, dose annotation and IOB move here with their fixtures, since they test this package's code. The dose tests read LoopKit's pump event dose format through a small test-only loader, with LoopKit's Walsh curve and daily basal schedule expansion ported for the tests.

Plan

LoopWorkspace next-dev pins LoopKit/LoopAlgorithm e3f5e3c, so merging this to main doesn't affect it. The workspace bump that adopts this change needs these PRs too:

Testing

ps2 added 3 commits October 31, 2026 23:54
LoopKit currently depends on LoopAlgorithm for LoopQuantity and LoopUnit, so
apps that want LoopKit without the algorithm (Trio) still have to build it.
With this change LoopAlgorithm's public API uses Doubles in fixed units, so
LoopKit can own its unit types and the two packages no longer need each other.

- Glucose values are mg/dL (GlucoseValue.mgdl), carbs are grams
  (CarbEntry.grams), sensitivity is mg/dL per unit, targets and the suspend
  threshold are mg/dL, velocities are mg/dL per second.
- LoopQuantity and LoopUnit are internal; the math is unchanged.
- The LoopAlgorithm struct is renamed Algorithm, so LoopAlgorithm.X always
  names the module.
- GlucoseSampleValue drops trendRate and condition, which the algorithm
  never read.
- Helpers that apps don't call are internal.
- LoopKit's tests of LoopMath and date rounding move here.
- AlgorithmInput's sensitivity, target and suspendThreshold become
  sensitivityMgdlPerUnit, targetMgdl and suspendThresholdMgdl, and the public
  overloads that take plain numbers label them the same way
  (momentumVelocityMaximumMgdlPerMinute, insulinSensitivityHistoryMgdlPerUnit,
  ...). A value in the wrong unit no longer passes unnoticed at the call site.
  Fixture JSON keys are unchanged.
- run converts the schedules to quantities once instead of on each access.
- Remove FixtureGlucoseSample.condition and the now unused GlucoseCondition.
- Units on every AlgorithmInput property, Algorithm.run(input:) and both
  generatePrediction overloads, including the parameters that had none.
- README gains a usage section with the units table.
- Fix doc comments that no longer matched the code: generatePrediction never
  returns nil, annotated(with:) takes basalHistory, filterDateInterval takes
  an interval, useLegacyIntegration is described, and the sensitivity window
  note no longer refers to mid-absorption ISF as future work.
@ps2
ps2 merged commit 41baa8f into main Nov 1, 2026
ps2 added a commit to LoopKit/LoopKit that referenced this pull request Nov 1, 2026
The dose annotation and IOB tests moved to LoopAlgorithm (LoopKit/LoopAlgorithm#17).
The reconcile step of testIOBFromSuspend tests LoopKit's own reconciled(), so
it stays here as testReconcileSuspend with its two fixtures.
ps2 added a commit to LoopKit/Loop that referenced this pull request Nov 1, 2026
They test LoopAlgorithm's dose annotation and insulin on board, so they now
live there with their fixtures (LoopKit/LoopAlgorithm#17). The override tests
stay here because they cross from LoopKit's override schedules into the
algorithm.
ps2 added a commit to LoopKit/LoopKit that referenced this pull request Nov 1, 2026
* Own the shared value types instead of depending on LoopAlgorithm

LoopKit depended on LoopAlgorithm only for LoopQuantity, LoopUnit and a set
of small value types, so apps that use LoopKit without the algorithm (Trio)
still had to build it. LoopAlgorithm now takes plain numbers in fixed units
(LoopKit/LoopAlgorithm#17), so LoopKit can own these types itself.

- LoopKit has its own LoopQuantity, LoopUnit, AbsoluteScheduleValue, glucose,
  carb and insulin value types, insulin models and dose recommendations,
  under the same names, so code that used them through LoopAlgorithm only
  changes its import.
- No LoopAlgorithm package dependency in the project, Package.swift or CI;
  LoopKit builds and tests on its own.
- LegacyDoseChart takes a ChartedDose instead of the algorithm's dose type.
- Tests of algorithm code move to LoopAlgorithm (LoopMath, date rounding)
  and Loop (dose annotation, IOB, an override run).

* Keep the suspend reconcile check in LoopKit

The dose annotation and IOB tests moved to LoopAlgorithm (LoopKit/LoopAlgorithm#17).
The reconcile step of testIOBFromSuspend tests LoopKit's own reconciled(), so
it stays here as testReconcileSuspend with its two fixtures.
ps2 added a commit to LoopKit/Loop that referenced this pull request Nov 1, 2026
* Use LoopKit's value types and bridge to LoopAlgorithm

LoopKit no longer depends on LoopAlgorithm, and LoopAlgorithm takes plain
numbers in fixed units (LoopKit/LoopAlgorithm#17). Loop is now the one place
that converts between the two.

- AlgorithmBridge.swift: StoredGlucoseSample and StoredCarbEntry conform to
  the algorithm's protocols; LoopKit insulin models are wrapped for the
  algorithm; schedules convert to mg/dL; algorithm results convert back to
  LoopKit values for storage, display and the watch.
- LoopKitTypeAliases.swift makes the unqualified value-type names mean
  LoopKit's in each target; algorithm types are written LoopAlgorithm.X.
- LoopAlgorithm is linked once, in LoopCore; the widget, watch app and
  LoopCore's own code no longer import it.
- LoopCore gets the two constants the UI used from LoopAlgorithm.
- Name the new slow retrospective correction experiment in Current Setup.
- Tests: an end-to-end test that mmol/L settings dose the same as mg/dL, and
  the dose annotation, IOB and override tests moved from LoopKit.

* Map the dose direction exhaustively and test mmol/L manual boluses

- The automatic dose direction maps by switch instead of rawValue, so a case
  added on either side fails to compile instead of crashing the loop.
- Test that mmol/L settings give the same manual bolus recommendation and
  bolus preview as mg/dL with a pending carb entry.
- Test that LoopCore's copies of the input recency interval and maximum carb
  absorption time match LoopAlgorithm's.
- Fix the indentation of a predictGlucose call.

* Move the dose history tests to LoopAlgorithm

They test LoopAlgorithm's dose annotation and insulin on board, so they now
live there with their fixtures (LoopKit/LoopAlgorithm#17). The override tests
stay here because they cross from LoopKit's override schedules into the
algorithm.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant