Generates smoothed helix primitives and extrapolates lost ends.

    Notes
    -----
    From an input list of CA coordinates, the running average is
    calculated to form a primitive. The smoothing_level dictates how
    many times to calculate the running average. A higher
    smoothing_level generates a 'smoother' primitive - i.e. the
    points on the primitive more closely fit a smooth curve in R^3.
    Each time the smoothing level is increased by 1, a point is lost
    from either end of the primitive. To correct for this, the primitive
    is extrapolated at the ends to approximate the lost values. There
    is a trade-off then between the smoothness of the primitive and
    its accuracy at the ends.


    Parameters
    ----------
    cas_coords : list(numpy.array or float or tuple)
        Each element of the list must have length 3.
    smoothing_level : int
        Number of times to run the averaging.

    Returns
    -------
    final_primitive : list(numpy.array)
        Each array has length 3.

def make_primitive_extrapolate_ends(cas_coords, smoothing_level=2):
    """Generates smoothed helix primitives and extrapolates lost ends.

    Notes
    -----
    From an input list of CA coordinates, the running average is
    calculated to form a primitive. The smoothing_level dictates how
    many times to calculate the running average. A higher
    smoothing_level generates a 'smoother' primitive - i.e. the
    points on the primitive more closely fit a smooth curve in R^3.
    Each time the smoothing level is increased by 1, a point is lost
    from either end of the primitive. To correct for this, the primitive
    is extrapolated at the ends to approximate the lost values. There
    is a trade-off then between the smoothness of the primitive and
    its accuracy at the ends.


    Parameters
    ----------
    cas_coords : list(numpy.array or float or tuple)
        Each element of the list must have length 3.
    smoothing_level : int
        Number of times to run the averaging.

    Returns
    -------
    final_primitive : list(numpy.array)
        Each array has length 3.
    """
    try:
        smoothed_primitive = make_primitive_smoothed(
            cas_coords, smoothing_level=smoothing_level)
    except ValueError:
        smoothed_primitive = make_primitive_smoothed(
            cas_coords, smoothing_level=smoothing_level - 1)
    # if returned smoothed primitive is too short, lower the smoothing
    # level and try again.
    if len(smoothed_primitive) < 3:
        smoothed_primitive = make_primitive_smoothed(
            cas_coords, smoothing_level=smoothing_level - 1)
    final_primitive = []
    for ca in cas_coords:
        prim_dists = [distance(ca, p) for p in smoothed_primitive]
        closest_indices = sorted([x[0] for x in sorted(
            enumerate(prim_dists), key=lambda k: k[1])[:3]])
        a, b, c = [smoothed_primitive[x] for x in closest_indices]
        ab_foot = find_foot(a, b, ca)
        bc_foot = find_foot(b, c, ca)
        ca_foot = (ab_foot + bc_foot) / 2
        final_primitive.append(ca_foot)
    return final_primitive