Ejemplo n.º 1
0
 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])
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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]