def find_partial_effect(i): insulin_sensitivity = find_ratio_at_time(sensitivity_starts, sensitivity_ends, sensitivity_values, carb_starts[i]) carb_ratio = find_ratio_at_time(carb_ratio_starts, [], carb_ratios, carb_starts[i]) return carb_glucose_effect(carb_starts[i], carb_quantities[i], date, carb_ratio, insulin_sensitivity, default_absorption_time, delay, carb_absorptions[i])
def get_pending_insulin( at_date, basal_starts, basal_rates, basal_minutes, last_temp_basal, pending_bolus_amount=None ): """ Get the pending insulin for the purposes of calculating a recommended bolus Arguments: at_date -- the "now" time (roughly equivalent to datetime.now) basal_starts -- list of times the basal rates start at basal_rates -- list of basal rates (U/hr) basal_minutes -- list of basal lengths (in mins) last_temp_basal -- information about the last temporary basal in the form [type, start time, end time, basal rate] pending_bolus_amount -- amount of unconfirmed bolus insulin (U) Output: Amount of insulin that is "pending" """ assert len(basal_starts) == len(basal_rates),\ "expected input shapes to match" if (not basal_starts or not last_temp_basal or last_temp_basal[1] > last_temp_basal[2] ): return 0 # if the end date for the temp basal is greater than current date, # find the pending insulin if (last_temp_basal[2] > at_date and last_temp_basal[0] in [DoseType.tempbasal, DoseType.basal]): normal_basal_rate = find_ratio_at_time( basal_starts, [], basal_rates, at_date ) remaining_time = time_interval_since( last_temp_basal[2], at_date ) / 60 / 60 remaining_units = ( last_temp_basal[3] - normal_basal_rate ) * remaining_time pending_basal_insulin = max(0, remaining_units) else: pending_basal_insulin = 0 if pending_bolus_amount: pending_bolus = pending_bolus_amount else: pending_bolus = 0 return pending_basal_insulin + pending_bolus
def find_partial_effect(i): insulin_sensitivity = find_ratio_at_time(sensitivity_starts, sensitivity_ends, sensitivity_values, carb_starts[i]) carb_ratio = find_ratio_at_time(carb_ratio_starts, [], carb_ratios, carb_starts[i]) csf = insulin_sensitivity / carb_ratio partial_carbs_absorbed = carb_status.dynamic_absorbed_carbs( carb_starts[i], carb_quantities[i], absorptions[i], timelines[i], date, carb_absorptions[i] or default_absorption_time, delay, delta, ) return csf * partial_carbs_absorbed
def map_(carb_entry_starts, carb_entry_quantities, carb_entry_absorptions, effect_starts, effect_ends, effect_values, carb_ratio_starts, carb_ratios, sensitivity_starts, sensitivity_ends, sensitivity_values, absorption_time_overrun, default_absorption_time, delay, delta=5): """ Maps a sorted timeline of carb entries to the observed absorbed carbohydrates for each, from a timeline of glucose effect velocities. This makes some important assumptions: - insulin effects, used with glucose to calculate counteraction, are "correct" - carbs are absorbed completely in the order they were eaten without mixing or overlapping effects Arguments: carb_entry_starts -- list of times of carb entry (datetime objects) carb_entry_quantities -- list of grams of carbs eaten carb_entry_absorptions -- list of lengths of absorption times (mins) effect_starts -- list of start times of carb effect (datetime objects) effect_ends -- list of end times of carb effect (datetime objects) effect_values -- list of carb effects (mg/dL) carb_ratio_starts -- list of start times of carb ratios (time objects) carb_ratios -- list of carb ratios (g/U) sensitivity_starts -- list of time objects of start times of given insulin sensitivity values sensitivity_ends -- list of time objects of start times of given insulin sensitivity values sensitivity_values -- list of sensitivities (mg/dL/U) absorption_time_overrun -- multiplier to determine absorption time from the specified absorption time default_absorption_time -- absorption time to use for unspecified carb entries delay -- the time to delay the carb effect delta -- time interval between glucose values Output: 3 lists in format (absorption_results, absorption_timelines, carb_entries) - lists are matched by index - one index represents one carb entry and its corresponding data - absorption_results: each index is a list of absorption information - structure: [(0) observed grams absorbed, (1) clamped grams, (2) total carbs in entry, (3) remaining carbs, (4) observed absorption start, (5) observed absorption end, (6) estimated time remaining] - absorption_timelines: each index is a list that contains lists of timeline values - structure: [(0) timeline start time, (1) timeline end time, (2) absorbed value during timeline interval (g)] - if a timeline is a list with only "None", less than minimum absorption was observed - carb_entries: each index is a list of carb entry values - these lists are values that were calculated during map_ runtime - structure: [(0) carb sensitivities (mg/dL/G of carbohydrate), (1) maximum carb absorption times (min), (2) maximum absorption end times (datetime), (3) last date effects were observed (datetime) (4) total glucose effect expected for entry (mg/dL)] """ assert len(carb_entry_starts) == len(carb_entry_quantities)\ == len(carb_entry_absorptions), "expected input shapes to match" assert len(effect_starts) == len(effect_ends) == len(effect_values), \ "expected input shapes to match" assert len(carb_ratio_starts) == len(carb_ratios),\ "expected input shapes to match" assert len(sensitivity_starts) == len(sensitivity_ends)\ == len(sensitivity_values), "expected input shapes to match" if (not carb_entry_starts or not carb_ratios or not sensitivity_starts): return ([], [], []) builder_entry_indexes = list(range(0, len(carb_entry_starts))) # CSF is in mg/dL/g builder_carb_sensitivities = [ find_ratio_at_time(sensitivity_starts, sensitivity_ends, sensitivity_values, carb_entry_starts[i]) / find_ratio_at_time(carb_ratio_starts, [], carb_ratios, carb_entry_starts[i]) for i in builder_entry_indexes ] # unit: g/s builder_max_absorb_times = [ (carb_entry_absorptions[i] or default_absorption_time) * absorption_time_overrun for i in builder_entry_indexes ] builder_max_end_dates = [ carb_entry_starts[i] + timedelta(minutes=builder_max_absorb_times[i] + delay) for i in builder_entry_indexes ] last_effect_dates = [ min(builder_max_end_dates[i], max(effect_ends[len(effect_ends) - 1], carb_entry_starts[i])) for i in builder_entry_indexes ] entry_effects = [ carb_entry_quantities[i] * builder_carb_sensitivities[i] for i in builder_entry_indexes ] observed_effects = [0 for i in builder_entry_indexes] observed_completion_dates = [None for i in builder_entry_indexes] observed_timeline_starts = [[] for i in builder_entry_indexes] observed_timeline_ends = [[] for i in builder_entry_indexes] observed_timeline_carb_values = [[] for i in builder_entry_indexes] assert len(builder_entry_indexes) == len(builder_carb_sensitivities)\ == len(builder_max_absorb_times) == len(builder_max_end_dates)\ == len(last_effect_dates), "expected shapes to match" def add_next_effect(entry_index, effect, start, end): if start < carb_entry_starts[entry_index]: return observed_effects[entry_index] += effect if not observed_completion_dates[entry_index]: # Continue recording the timeline until # 100% of the carbs have been observed observed_timeline_starts[entry_index].append(start) observed_timeline_ends[entry_index].append(end) observed_timeline_carb_values[entry_index].append( effect / builder_carb_sensitivities[entry_index]) # Once 100% of the carbs are observed, track the endDate if (observed_effects[entry_index] + sys.float_info.epsilon >= entry_effects[entry_index]): observed_completion_dates[entry_index] = end for index in range(0, len(effect_starts)): if effect_starts[index] >= effect_ends[index]: continue # Select only the entries whose dates overlap the current date interval # These are not always contiguous, as maxEndDate varies between entries active_builders = [] for j in builder_entry_indexes: if (effect_starts[index] < builder_max_end_dates[j] and effect_starts[index] >= carb_entry_starts[j]): active_builders.append(j) # Ignore velocities < 0 when estimating carb absorption. # These are most likely the result of insulin absorption increases # such as during activity effect_value = max(0, effect_values[index]) * delta def rate_increase(index_): return (carb_entry_quantities[index_] / builder_max_absorb_times[index_]) # Sum the minimum absorption rates of each active entry to # determine how to split the active effects total_rate = 0 for i in active_builders: total_rate += rate_increase(i) for b_index in active_builders: entry_effect = (carb_entry_quantities[b_index] * builder_carb_sensitivities[b_index]) remaining_effect = max(entry_effect - observed_effects[b_index], 0) # Apply a portion of the effect to this entry partial_effect_value = min( remaining_effect, (carb_entry_quantities[b_index] / builder_max_absorb_times[b_index]) / total_rate * effect_value if total_rate != 0 and effect_value != 0 else 0) total_rate -= (carb_entry_quantities[b_index] / builder_max_absorb_times[b_index]) effect_value -= partial_effect_value add_next_effect(b_index, partial_effect_value, effect_starts[index], effect_ends[index]) # If there's still remainder effects with no additional entries # to account them to, count them as overrun on the final entry if (effect_value > sys.float_info.epsilon and b_index == (len(active_builders) - 1)): add_next_effect( b_index, effect_value, effect_starts[index], effect_ends[index], ) def absorption_result(builder_index): # absorption list structure: [observed grams absorbed, clamped grams, # total carbs in entry, remaining carbs, observed absorption start, # observed absorption end, estimated time remaining] observed_grams = (observed_effects[builder_index] / builder_carb_sensitivities[builder_index]) entry_grams = carb_entry_quantities[builder_index] time = (time_interval_since(last_effect_dates[builder_index], carb_entry_starts[builder_index]) / 60 - delay) min_predicted_grams = linearly_absorbed_carbs( entry_grams, time, builder_max_absorb_times[builder_index]) clamped_grams = min(entry_grams, max(min_predicted_grams, observed_grams)) min_absorption_rate = (carb_entry_quantities[builder_index] / builder_max_absorb_times[builder_index]) estimated_time_remaining = ((entry_grams - clamped_grams) / min_absorption_rate if min_absorption_rate > 0 else 0) absorption = [ observed_grams, clamped_grams, entry_grams, entry_grams - clamped_grams, carb_entry_starts[builder_index], observed_completion_dates[builder_index] or last_effect_dates[builder_index], estimated_time_remaining ] return absorption # The timeline of observed absorption, # if greater than the minimum required absorption. def clamped_timeline(builder_index): entry_grams = carb_entry_quantities[builder_index] time = (time_interval_since(last_effect_dates[builder_index], carb_entry_starts[builder_index]) / 60 - delay) min_predicted_grams = linearly_absorbed_carbs( entry_grams, time, builder_max_absorb_times[builder_index]) observed_grams = (observed_effects[builder_index] / builder_carb_sensitivities[builder_index]) output = [] for i in range(0, len(observed_timeline_starts[builder_index])): output.append([ observed_timeline_starts[builder_index][i], observed_timeline_ends[builder_index][i], observed_timeline_carb_values[builder_index][i] ] if ( observed_grams >= min_predicted_grams) else [None, None, None]) return output def entry_properties(i): return [ builder_carb_sensitivities[i], builder_max_absorb_times[i], builder_max_end_dates[i], last_effect_dates[i], entry_effects[i] ] entries = [] absorptions = [] timelines = [] for i in builder_entry_indexes: absorptions.append(absorption_result(i)) timelines.append(clamped_timeline(i)) entries.append(entry_properties(i)) assert len(absorptions) == len(timelines) == len(entries),\ "expect output shapes to match" return (absorptions, timelines, entries)
def recommended_bolus( glucose_dates, glucose_values, target_starts, target_ends, target_mins, target_maxes, at_date, suspend_threshold, sensitivity_starts, sensitivity_ends, sensitivity_values, model, pending_insulin, max_bolus, volume_rounder=None ): """ Recommends a temporary basal rate to conform a glucose prediction timeline to a correction range Returns None if normal scheduled basal or active temporary basal is sufficient Arguments: glucose_dates -- dates of glucose values (datetime) glucose_values -- glucose values (in mg/dL) target_starts -- start times for given target ranges (datetime) target_ends -- stop times for given target ranges (datetime) target_mins -- the lower bounds of target ranges (mg/dL) target_maxes -- the upper bounds of target ranges (mg/dL) at_date -- date to calculate the temp basal at suspend_threshold -- value to suspend all insulin delivery at (mg/dL) sensitivity_starts -- list of time objects of start times of given insulin sensitivity values sensitivity_ends -- list of time objects of start times of given insulin sensitivity values sensitivity_values -- list of sensitivities (mg/dL/U) model -- list of insulin model parameters in format [DIA, peak_time] if exponential model, or [DIA] if Walsh model pending_insulin -- number of units expected to be delivered, but not yet reflected in the correction max_bolus -- the maximum allowable bolus value in Units volume_rounder -- the smallest fraction of a unit supported in insulin delivery; if None, no rounding is performed Output: A bolus recommendation """ assert len(glucose_dates) == len(glucose_values),\ "expected input shapes to match" assert len(target_starts) == len(target_ends) == len(target_mins)\ == len(target_maxes), "expected input shapes to match" assert len(sensitivity_starts) == len(sensitivity_ends)\ == len(sensitivity_values), "expected input shapes to match" if (not glucose_dates or not target_starts or not sensitivity_starts ): return [0, 0, None] sensitivity_value = find_ratio_at_time( sensitivity_starts, sensitivity_ends, sensitivity_values, at_date ) correction = insulin_correction( glucose_dates, glucose_values, target_starts, target_ends, target_mins, target_maxes, at_date, suspend_threshold, sensitivity_value, model ) bolus = as_bolus( correction, pending_insulin, max_bolus, volume_rounder ) if bolus[0] < 0: bolus = 0 return bolus
def recommended_temp_basal( glucose_dates, glucose_values, target_starts, target_ends, target_mins, target_maxes, at_date, suspend_threshold, sensitivity_starts, sensitivity_ends, sensitivity_values, model, basal_starts, basal_rates, basal_minutes, max_basal_rate, last_temp_basal, duration=30, continuation_interval=11, rate_rounder=None ): """ Recommends a temporary basal rate to conform a glucose prediction timeline to a correction range Returns None if normal scheduled basal or active temporary basal is sufficient Arguments: glucose_dates -- dates of glucose values (datetime) glucose_values -- glucose values (in mg/dL) target_starts -- start times for given target ranges (datetime) target_ends -- stop times for given target ranges (datetime) target_mins -- the lower bounds of target ranges (mg/dL) target_maxes -- the upper bounds of target ranges (mg/dL) at_date -- date to calculate the temp basal at suspend_threshold -- value to suspend all insulin delivery at (mg/dL) sensitivity_starts -- list of time objects of start times of given insulin sensitivity values sensitivity_ends -- list of time objects of start times of given insulin sensitivity values sensitivity_values -- list of sensitivities (mg/dL/U) model -- list of insulin model parameters in format [DIA, peak_time] if exponential model, or [DIA] if Walsh model basal_starts -- list of times the basal rates start at basal_rates -- list of basal rates(U/hr) basal_minutes -- list of basal lengths (in mins) max_basal_rate -- max basal rate that Loop can give (U/hr) last_temp_basal -- list of last temporary basal information in format [type, start time, end time, basal rate] duration -- length of the temp basal (mins) continuation_interval -- length of time before an ongoing temp basal should be continued with a new command (mins) rate_rounder -- the smallest fraction of a unit supported in basal delivery; if None, no rounding is performed Output: The recommended temporary basal in the format [rate, duration] """ assert len(glucose_dates) == len(glucose_values),\ "expected input shapes to match" assert len(target_starts) == len(target_ends) == len(target_mins)\ == len(target_maxes), "expected input shapes to match" assert len(sensitivity_starts) == len(sensitivity_ends)\ == len(sensitivity_values), "expected input shapes to match" assert len(basal_starts) == len(basal_rates) == len(basal_minutes),\ "expected input shapes to match" if (not glucose_dates or not target_starts or not sensitivity_starts or not basal_starts ): return None sensitivity_value = find_ratio_at_time( sensitivity_starts, sensitivity_ends, sensitivity_values, at_date ) correction = insulin_correction( glucose_dates, glucose_values, target_starts, target_ends, target_mins, target_maxes, at_date, suspend_threshold, sensitivity_value, model ) scheduled_basal_rate = find_ratio_at_time( basal_starts, [], basal_rates, at_date ) if (correction[0] == Correction.above_range and correction[1] < correction[3]): max_basal_rate = scheduled_basal_rate temp_basal = as_temp_basal( correction, scheduled_basal_rate, max_basal_rate, duration, rate_rounder ) recommendation = if_necessary( temp_basal, at_date, scheduled_basal_rate, last_temp_basal, continuation_interval ) # convert a "cancel" into zero-temp, zero-duration basal if recommendation == Correction.cancel: return [0, 0] return recommendation
def insulin_correction( prediction_dates, prediction_values, target_starts, target_ends, target_mins, target_maxes, at_date, suspend_threshold_value, sensitivity_value, model ): """ Computes a total insulin amount necessary to correct a glucose differential at a given sensitivity prediction_dates -- dates glucose values were predicted (datetime) prediction_values -- predicted glucose values (mg/dL) target_starts -- start times for given target ranges (datetime) target_ends -- stop times for given target ranges (datetime) target_mins -- the lower bounds of target ranges (mg/dL) target_maxes -- the upper bounds of target ranges (mg/dL) at_date -- date to calculate correction suspend_threshold -- value to suspend all insulin delivery at (mg/dL) sensitivity_value -- the sensitivity (mg/dL/U) model -- list of insulin model parameters in format [DIA, peak_time] if exponential model, or [DIA] if Walsh model Output: A list of insulin correction information. All lists have the type as the first index, and may include additional information based on the type. Types: - entirely_below_range Structure: [type, glucose value to be corrected, minimum target, units of correction insulin] - suspend Structure: [type, min glucose value] - in_range Structure: [type] - above_range Structure: [type, minimum predicted glucose value, glucose value to be corrected, minimum target, units of correction insulin] """ assert len(prediction_dates) == len(prediction_values),\ "expected input shapes to match" assert len(target_starts) == len(target_ends) == len(target_mins)\ == len(target_maxes), "expected input shapes to match" (min_glucose, eventual_glucose, correcting_glucose, min_correction_units ) = ([], None, None, None) # only calculate a correction if the prediction is between # "now" and now + DIA if len(model) == 1: # if Walsh model date_range = [at_date, at_date + timedelta(hours=model[0]) ] else: date_range = [at_date, at_date + timedelta(minutes=model[0]) ] # if we don't know the suspend threshold, it defaults to the lower # bound of the correction range at the time the "loop" is being run at if not suspend_threshold_value: suspend_threshold_value = find_ratio_at_time( target_starts, target_ends, target_mins, at_date ) # For each prediction above target, determine the amount of insulin # necessary to correct glucose based on the modeled effectiveness of # the insulin at that time for i in range(0, len(prediction_dates)): if not is_time_between( date_range[0], date_range[1], prediction_dates[i] ): continue # If any predicted value is below the suspend threshold, # return immediately if prediction_values[i] < suspend_threshold_value: return [Correction.suspend, prediction_values[i]] # Update range statistics if not min_glucose or prediction_values[i] < min_glucose[1]: min_glucose = [prediction_dates[i], prediction_values[i]] eventual_glucose = [prediction_dates[i], prediction_values[i]] predicted_glucose_value = prediction_values[i] time = time_interval_since( prediction_dates[i], at_date ) / 60 average_target = ( find_ratio_at_time( target_starts, target_ends, target_maxes, prediction_dates[i] ) + find_ratio_at_time( target_starts, target_ends, target_mins, prediction_dates[i] ) ) / 2 # Compute the target value as a function of time since the dose started target_value = target_glucose_value( (time / ( (60 * model[0]) if len(model) == 1 else model[0] ) ), suspend_threshold_value, average_target ) # Compute the dose required to bring this prediction to target: # dose = (Glucose delta) / (% effect × sensitivity) if len(model) == 1: # if Walsh model percent_effected = 1 - walsh_percent_effect_remaining( time, model[0] ) else: percent_effected = 1 - percent_effect_remaining( time, model[0], model[1] ) effected_sensitivity = percent_effected * sensitivity_value # calculate the Units needed to correct that predicted glucose value correction_units = insulin_correction_units( predicted_glucose_value, target_value, effected_sensitivity ) if not correction_units or correction_units <= 0: continue # Update the correction only if we've found a new minimum if min_correction_units: if correction_units >= min_correction_units: continue correcting_glucose = [prediction_dates[i], prediction_values[i]] min_correction_units = correction_units if not eventual_glucose or not min_glucose: return None # Choose either the minimum glucose or eventual glucose as correction delta min_glucose_targets = [ find_ratio_at_time( target_starts, target_ends, target_mins, min_glucose[0] ), find_ratio_at_time( target_starts, target_ends, target_maxes, min_glucose[0] ) ] eventual_glucose_targets = [ find_ratio_at_time( target_starts, target_ends, target_mins, eventual_glucose[0] ), find_ratio_at_time( target_starts, target_ends, target_maxes, eventual_glucose[0] ) ] # Treat the mininum glucose when both are below range if (min_glucose[1] < min_glucose_targets[0] and eventual_glucose[1] < min_glucose_targets[0] ): time = time_interval_since(min_glucose[0], at_date) / 60 # For time = 0, assume a small amount effected. # This will result in large (negative) unit recommendation # rather than no recommendation at all. if len(model) == 1: percent_effected = max( sys.float_info.epsilon, 1 - walsh_percent_effect_remaining(time, model[0]) ) else: percent_effected = max( sys.float_info.epsilon, 1 - percent_effect_remaining(time, model[0], model[1]) ) units = insulin_correction_units( min_glucose[1], sum(min_glucose_targets) / len(min_glucose_targets), sensitivity_value * percent_effected ) if not units: return None # we're way below target return [ Correction.entirely_below_range, min_glucose[1], min_glucose_targets[0], units ] # we're above target elif (eventual_glucose[1] > eventual_glucose_targets[1] and min_correction_units and correcting_glucose ): return [ Correction.above_range, min_glucose[1], correcting_glucose[1], eventual_glucose_targets[0], min_correction_units ] # we're in range else: return [Correction.in_range]