Welcome to the documentation for PyLoopKit. This document contains instructions to help get you oriented to the project, and includes details on running the Loop Algorithm in Python. The project was started by Anna Quinlan in the summer of 2019; if you have any questions please reach out to Ed Nykaza.
- PyLoopKit was built using Python 3.7.3; there is an environment file in the project that can be used with Anaconda or Miniconda to recreate the virtual environment.
- PyLoopKit does not support glucose values that are in mmol/L; they must be converted to mg/dL before usage (conversion can be done by multiplying by 18.01559)
- Though PyLoopKit is able to work with dates with a consistent time zone, it has not been tested when there are time zone changes within the data itself
- PyLoopKit uses the datetime objects to store dates and times. Dates with timezones must either have the correct timezone (ex: 2019-08-12 01:28:19 -0700), or be in UTC and have a constant offset (ex: 2019-08-12 01:28:19 +0000, with an offset of -25200 seconds)
- In order to prepare for vectorization, PyLoopKit uses index-matched lists to store data, instead of objects like Loop
- Example: if
glucose_dates = [14:00, 14:05, 14:10]
andglucose_values = [105, 102, 104]
, then that would mean a BG measurement of 105 mg/dL at 2:00 PM, 102 mg/dL at 2:05 PM, and 104 mg/dL at 2:10 PM
- Example: if
- There is no support for integral retrospective correction (only retrospective correction); these correction methods, though they have similar names, utilize different methods to compute the correction and as such will return different predictions.
- If (and only if) you are running PyLoopKit using data from a Loop issue report, predicted glucose values (and the recommended basals/bolus) will likely be slightly off. This is because the issue report does not contain the full insulin history.
- To correctly calculate counteraction effects at a point in time, the past duration of insulin action (DIA)-worth of insulin is required; Loop uses 24 hours of counteraction effects to calculate carbohydrate effects and retrospective correction effects.
- However, there are only 24 hours of dose history in a report, meaning that the first DIA-worth of counteraction effects will be incorrect because not all insulin active at that time was accounted for. This will affect three of the four effects that are output by the issue report (insulin, carb and retrospective correction effects). The insulin effects will still have the same momentary effects, but since some initial IOB was unaccounted for, the value of the overall effect will differ.
- If, having read the caution above, you still would like to use an issue report with PyLoopKit, the issue report must first be run through the issue report parser in the Tidepool data analytics repository to convert it from Markdown to json
- As you read through this doc
bold()
is used for functions.
Functions to Run the Loop Algorithm
- If you are passing in data from an issue report, you can use the function
parse_report_and_run()
inpyloop_parser.py
. This function expects the input file to have been generated through the issue report parser in the Tidepool data analytics repository. - If passing data from a previous run, or data that you have prepared to be in the format specified in “Input Data Requirements”, pass it into
update()
inloop_data_manager.py
Tests
- PyLoopKit has tests (using
unittests
) for all major functions - The tests are located in the test folder; fixtures are located in the fixtures folder
- To run tests, navigate to the test folder and run
python3 -m unittest -v
- PyLoopKit has manually been tested with seven issue reports from actual Loopers, and around ten issue reports generated with a simulator pump and CGM
- There are also four automatic tests that utilize issue reports and validate the accuracy of the glucose prediction and recommendations
- This project has been primarily tested using issue reports without overrides, though there is support for overrides in the PyLoopKit issue report parser.
How Effects Used for Glucose Predictions Are Calculated
- Momentum effects:
get_recent_momentum_effects()
inglucose_store.py
- Filters the glucose data so that it is within
momentum_data_interval
minutes - Calls
linear_momentum_effect()
inglucose_math.py
to calculate the glucose momentum effect- Checks that the glucose values are valid, with only one provenance (
has_single_provenance()
), no CGM calibration values (is_calibrated()
), and BG values that are continuous (is_continuous()
) - Does a linear regression on the BG values with
linear_regression()
, then uses the slope to project momentum effect for each value, proportional to the time since the starting date- Momentum effect cannot be negative
- Checks that the glucose values are valid, with only one provenance (
- Filters the glucose data so that it is within
- Insulin effects:
get_glucose_effects()
indose_store.py
- Filters dose data so that the data starts at start time minus DIA
- Reconciles the data, trimming overlapping temporary basal rates (temp basals) and adding resumes for suspends (if necessary) using
reconciled()
ininsulin_math.py
- Sorts the data, since
reconciled()
often makes the doses slightly out of order - Annotates the data with the scheduled basal rate during the dose using
annotated()
ininsulin_math.py
; boluses have a scheduled basal rate of 0 U/hr. - Trims doses to the start of the interval (start time - DIA)
- Gets insulin effects using
glucose_effects()
ininsulin_math.py
- Determines what the start and end times for the effects should be using
simulation_date_range_for_samples()
- Iterates from the start to the end in
delta
-long intervals (where delta is typically set to 5 minutes), finding the partial insulin effect for each dose at a givendate
usingglucose_effect()
- Determines the percentage of the dose that has been used up before
date
if the dose is shorter than 1.05 *delta
(typically a bolus or very short temp basal) with the computation 1 - percent_effect_remaining - Determines the percentage of the dose that has been used up before
date
if the dose is shorter than 1.05 *delta
(typically temp basal) with the computation 1 -continuous_delivery_glucose_effect()
- Calculates the Units of insulin (net of any scheduled basal rates) in the dose with
net_basal_units()
, then multiplies by negative insulinsensitivity
and the percentage of used dose to calculate the partial effect
- Determines the percentage of the dose that has been used up before
- Determines what the start and end times for the effects should be using
- Filters effects so they start at the start time
- Carb effects:
get_carb_glucose_effects()
incarb_store.py
- Filters the carb data so it starts at start time minus
maximum_absorption_time_interval
(the slowest absorption time * 2) - If counteraction effects are provided, calculates the absorption dynamically using
map_()
anddynamic_glucose_effects()
map_()
generates a timeline of absorption and absorption statistics. It calculates the carb absorption using positive counteraction effects, then if there are multiple active carb entries, splits the absorption proportionally based on the minimum expected absorption rates.dynamic_glucose_effects()
determines what the start and end times for the effects should be usingsimulation_date_range()
, then iterates from start to the end indelta
-long intervals, suming the partial carb effects at thatdate
for each entry usingdynamic_absorbed_carbs()
in carb_status.py- If there is no absorption information for an entry, effects are calculated using
absorbed_carbs()
incarb_math.py
, which is a parabolic model - If less than the minimum expected absorption is observed, the absorbed carbs are calculated linearly with
linearly_absorbed_carbs()
incarb_math.py
to ensure they eventually absorb
- If there is no absorption information for an entry, effects are calculated using
- If counteraction effects are not provided (which is very rare), it calculates the absorption using
carb_glucose_effects
(), which uses a parabolic model to generate the timeline.
- Filters the carb data so it starts at start time minus
- Retrospective correction (if enabled):
update_retrospective_glucose_effect()
inloop_data_manager.py
- “Subtracts” the carb effects from the counteraction effects to determine discrepancies over
delta
-minute intervals usingsubtracting()
inloop_math.py
- Sums those discrepancies over time using
combined_sums()
inloop_math.py
- Calculates the average velocity of the retrospective discrepancies, then decays that effect linearly with
decay_effect()
inloop_math.py
, using the most recent glucose measurement as the starting point
- “Subtracts” the carb effects from the counteraction effects to determine discrepancies over
If using an issue report, you can skip this section; this will be handled by the PyLoopKit issue report parser. **All the input data must be contained in one dictionary with the necessary keys. **PyLoopKit uses index-matched lists to store data, so when discussing the data properties and requirements, it is assumed that these will be lists of the values (unless otherwise noted) that are matched index-wise. This information is also contained in the doc-string of update() in loop_data_manager.py
Glucose Data
- Required Lists
- “glucose_dates”
- the time of the BG measurement as a datetime object
- "glucose_values" (BG value)
- must be in mg/dL
- Example: 150
- “glucose_dates”
Insulin Data
- PyLoopKit will automatically trim overlapping doses and add resumes for suspends
- Required Lists
- “dose_types”
- DoseType enums (the class is contained in
dose.py
)- When initializing, string must be either “Bolus”, “TempBasal”, “BasalProfileStart”/ “Basal”, or “PumpSuspend”/”Suspend” (case-insensitive)
- the input validation function will issue a warning if there are types that are not these values
- DoseType enums (the class is contained in
- “dose_start_times”
- time the dose started at as a datetime object
- “dose_end_times”
- time dose ended at as a datetime object
- If dose is type “Bolus”, the end time the time the pump finished delivery of the bolus
- “dose_values”
- Units of insulin in dose (if a bolus) or the basal rate in U/hr (if a basal)
- For basals, this is not a net basal rate; it’s the basal rate that the pump was set to, or that Loop set the pump to
- Bolus example: 1.5
- Basal example: 0.2
- Units of insulin in dose (if a bolus) or the basal rate in U/hr (if a basal)
- “dose_types”
Carbohydrate Data
- Required Lists
- “carb_dates”
- time carbohydrates were consumed at (ISO-formatted date)
- Example: "2015-07-13T12:02:37"
- “carb_values”
- grams of carbohydrates consumed
- Example: 20
- “carb_absorption_times”
- estimated absorption time in minutes
- this is the “lollipop, taco, pizza” option in Loop
- if no absorption time is specified, defaults to medium, which is 180 minutes (3 hours)
- Pass a list with
None
values if not specifying absorption time
- Pass a list with
- Example: 120
- estimated absorption time in minutes
- “carb_dates”
Settings Data
- key: “settings_dictionary”
- dictionary of various settings
- Required Keys
- “insulin_model” (insulin model)
- list containing insulin model information
- model is either Walsh or exponential; this typing can be inferred from the length of the list
- if Walsh:
- structure = [DIA (in hours)]
- Example: [4]
- if exponential:
- structure = [DIA (in minutes), peak (in minutes)]
- Child model has a peak at 65 mins, adult model has peak at 75 mins
- Example for adult: [240, 75]
- Example for child: [240, 65]
- “max_basal_rate”
- the maximum basal rate that Loop will deliver (in Units/hr)
- Example: 4
- “max_bolus”
- the maximum bolus that Loop will recommend (in Units)
- Example: 10
- “suspend_threshold”
- glucose value (mg/dL) on the prediction curve at which Loop will set a zero-temp and not recommend any boluses
- If the suspend_thresold is
None
, PyLoopKit defaults to the lower value of the correction range at the time the “loop” is being run at, which mirrors the behavior of Loop - Example: 70
- “default_absorption_times”
- list of absorption times (minutes) to default to if there is no specified absorption time for a carb entry
- format: [default fast absorption time, default **medium_ _**absorption time, default slow absorption time]
- Loop defaults to [120, 180, 240]
- “insulin_model” (insulin model)
- Optional Keys
- “insulin_delay”
- minutes to delay the insulin absorption
- PyLoopKit and Loop default to 10 (minutes)
- “rate_rounder” (rounding increment)
- the interval to round basals & bolus (this is pump-specific)
- Some Medtronic pumps can dose in 0.025 U increments, versus Omnipod doses in 0.05 U increments
- If not present, PyLoopKit does not round dose values
- Example: 0.05
- This would round a temp basal of 0.266 U/hr to 0.25 U/hr, or a temp basal of 0.271 U/hr to 0.30 U/hr
- the interval to round basals & bolus (this is pump-specific)
- “retrospective_correction_enabled”
- Boolean on whether to enable retrospective correction
- PyLoopKit and Loop default to False
- Example: False (to disable)
- “dynamic_carb_absorption_enabled”
- Boolean on whether to allow carb effects to be calculated dynamically
- PyLoopKit and Loop default to True
- “retrospective_correction_grouping_interval”
- Interval (minutes) over which to aggregate changes in glucose for retrospective correction
- PyLoopKit and Loop default to 30
- "retrospective_correction_integration_interval"
- Interval (minutes) of the time over which to integrate the retrospective correction effects
- PyLoopKit and Loop default to 30
- "recency_interval"
- how recent the glucose measurements must be in order to calculate retrospective correction effects (minutes)
- PyLoopKit and Loop default to 15
- “momentum_data_interval”
- interval (minutes) of recent BG measurements to use to calculate the momentum effect
- PyLoopKit and Loop default to 15 (minutes)
- “insulin_delay”
Insulin Sensitivity Schedule
- Required Lists
- "sensitivity_ratio_start_times"
- time the sensitivity value starts being used (datetime time object)
- Example: time(0, 0, 0)
- "sensitivity_ratio_end_times"
- time the sensitivity value stops being used (datetime time object)
- Example: time(23, 59, 59)
- The end time can be the same as the start time if there is one ratio for the whole day
- "sensitivity_ratio_values"
- insulin sensitivity factor (ISF) in mg/dL per Unit of insulin
- amount one Unit will drop blood glucose levels
- Example: 40
- insulin sensitivity factor (ISF) in mg/dL per Unit of insulin
- "sensitivity_ratio_start_times"
Carb Ratio Schedule
- Required Lists
- "carb_ratio_start_times"
- time the carb ratio starts being used (datetime time object)
- Example: time(0, 0, 0)
- "carb_ratio_values"
- carb ratio in grams of carbohydrates per Unit of insulin
- Example: 10
- "carb_ratio_start_times"
Basal Schedule
- Required Lists
- "basal_rate_start_times"
- time the basal starts being used (datetime time object)
- Example: time(0, 0, 0)
- "basal_rate_values"
- the length of time the basal runs for (in minutes)
- Example: 600
- "basal_rate_minutes"
- the infusion rate in U/hour
- Example: 0.85
- "basal_rate_start_times"
Correction Range Schedule
- Required Lists
- "target_range_start_times"
- time the target range starts being used (datetime time object)
- Example: time(0, 0, 0)
- "target_range_end_times"
- time the target range stops being used (datetime time object)
- Example: time(23, 59, 59)
- The end time can be the same as the start time if there is one range for the whole day
- "target_range_minimum_values"
- minimum value for target range (mg/dL)
- Example: 80
- "target_range_maximum_values"
- maximum value for target range (mg/dL)
- Example: 100
- "target_range_start_times"
Last Temporary Basal Rate
- key: "last_temporary_basal"
- list of information about the last temporary basal
- Form: [type of dose, start time for basal, end time for basal, basal rate in U/hr]
- Type must be DoseType.tempbasal or DoseType.basal
- If not present, PyLoopKit defaults to an empty list
Time to Calculate At
- key: "time_to_calculate_at"
- the time to assume as the “now” time, which is also the time to recommend the temporary basal and bolus at (datetime object)
Installing the Virtual Environment
- The PyLoopKit environment was developed with Anaconda. You'll need to install Miniconda or Anaconda for your platform.
- In a terminal, navigate to the directory where the environment.yml is located (likely the PyLoopKit/pyloopkit folder).
- Run
conda env create
; this will download all of the package dependencies and install them in a virtual environment named py-loop. PLEASE NOTE: this may take up to 30 minutes to complete.
Using the Virtual Environment
- In Terminal run
source activate py-loop
, or in the Anaconda Prompt runconda activate py-loop
to start the environment. - Run
deactivate
to stop the environment.
Using the Examples
- Example input and output files can be found in the example_files folder
- Run
example.py
(located in the main directory) to run an example input file through the algorithm and generate graphs of the calculated data- File options:
example_issue_report_1.json
- Issue report with an exponential adult insulin curve
example_issue_report_2.json
- Issue report with an exponential child insulin curve
example_issue_report_3.json
- Issue report with retrospective correction effects enabled
example_issue_report_4.json
- Issue report with Walsh insulin model
example_from_previous_run.json
- input dictionary that was saved from the output of a previous run of
update()
- input dictionary that was saved from the output of a previous run of
- There is code in
example.py
to run any of these files; uncomment the file you want to use - An output json file will be generated and saved
- File options:
Importing from an Issue Report
- The issue report must be have already been parsed into json format with the parser found in Tidepool’s data-science repository
parse_report_and_run()
inpyloop_parser.py
is the function that can automatically take this json issue report, extract the data into a usable format, then run it through the algorithm and give recommendations- The path and file name are required
-
For Mac, an example would be
path = "/Users/jamesjellyfish/Downloads" file_name = "issue_report.json"
-
For Windows, an example would be
path = "c:\Users\jamesjellyfish\Downloads" file_name = "issue_report.json"
-
Sample call: **
parse_report_and_run(path, file_name)
-
Directly Passing Data
update()
inloop_data_manager.py
can take the input dictionary, run it through the algorithm, and return an output dictionaryupdate()
takes one input dictionary and extracts all the necessary information, provided the keys are the same as are specified in “Input Data Requirements”
Input Validation in PyLoopKit
- In order to flag unreasonable inputs, PyLoopKit uses the functions in
input_validation_tools.py
- Two types of notices: warnings and errors
- Warnings do not stop PyLoopKit from running; errors do stop the run
- Warnings use the Loop guardrail values (see this document for more information)
- Errors are for values that are highly unreasonable
- Example: a negative DIA
- If you believe that these thresholds are not appropriate for your dataset, please change the relevant values.
Interpreting the Output
- PyLoopKit returns a dictionary containing each calculated effect, the glucose prediction, the recommendation for a temp basal and/or bolus, and a dictionary of the input data into the algorithm
- For each effect or glucose prediction, there are two index-matched lists: one for dates, and one for effect values
- Effect Values
- Momentum, insulin, carb, and retrospective correction effects are in mg/dL
- You can calculate the change in mg/dL/min with simple arithmetic;
(value_2 - value_1) / (time_2 - time_1)
- You can calculate the change in mg/dL/min with simple arithmetic;
- Counteraction effects are in mg/dL/minute
- Momentum, insulin, carb, and retrospective correction effects are in mg/dL
- Glucose prediction
- Also in mg/dL
- Includes all the effects that were calculated
- Recommended temporary basal rate
- List in format [temporary basal rate, minutes to run the temp for]
- [0.475, 30] would mean a rate of 0.475 U/hr for 30 minutes
- If there is no recommendation, Loop is opting to continue the current temp basal (or the scheduled basal if no temp is running)
- This occurs a lot with issue reports, because the “last temporary basal” is often the temp basal that was set with the most recent run of the loop
- If the recommendation has a duration of 0 minutes, Loop is opting to cancel the current temp and return to the scheduled basal rate
- [0.5, 0] would be a cancel command because the recommended temp of 0.5 U/hr is the same as the scheduled basal rate
- List in format [temporary basal rate, minutes to run the temp for]
- Recommended bolus
- List in format [units of insulin, pending insulin, recommendation notice]
- [0, 0.3, ["glucoseBelowSuspendThreshold", 56.1]] would mean there are 0.3 U of pending insulin, and Loop is recommending a bolus of 0 U because a point on the predicted glucose graph is 56.1 mg/dL, which is below the suspend threshold
- [0.5, 0, None] would mean there are 0 U of pending insulin, and Loop is recommending a bolus of 0.5 U
- Pending insulin is the insulin that is planned but not yet been delivered
- Composed of pending basal amount + pending bolus amount
- Pending basal is defined as the net units that have yet to be delivered by the currently running temp
- Pending boluses are not reflected in issue reports, thus the recommended bolus may differ if using an issue report and there was a pending bolus
- Composed of pending basal amount + pending bolus amount
- Bolus recommendation notices are the warnings displayed above the bolus screen in Loop if the prediction is either below target or below the suspend threshold
- List in format [units of insulin, pending insulin, recommendation notice]
- Dictionary of input data
- Key: “input_data”
- Can be used to re-run the algorithm in the future if desired