Esempio n. 1
0
def interval_approximate_nd(f, a, b, deg, return_inf_norm=False):
    """Finds the chebyshev approximation of an n-dimensional function on an
    interval.

    Parameters
    ----------
    f : function from R^n -> R
        The function to interpolate.
    a : numpy array
        The lower bound on the interval.
    b : numpy array
        The upper bound on the interval.
    deg : numpy array
        The degree of the interpolation in each dimension.
    return_inf_norm : bool
        whether to return the inf norm of the function

    Returns
    -------
    coeffs : numpy array
        The coefficient of the chebyshev interpolating polynomial.
    inf_norm : float
        The inf_norm of the function
    """
    dim = len(a)
    if dim != len(b):
        raise ValueError("Interval dimensions must be the same!")

    if hasattr(f, "evaluate_grid"):
        cheb_points = transform(get_cheb_grid(deg, dim, True), a, b)
        values_block = f.evaluate_grid(cheb_points)
    else:
        cheb_points = transform(get_cheb_grid(deg, dim, False), a, b)
        values_block = f(*cheb_points.T).reshape(*([deg + 1] * dim))

    values = chebyshev_block_copy(values_block)

    if return_inf_norm:
        inf_norm = np.max(np.abs(values_block))

    x0_slicer, deg_slicer, slices, rescale = interval_approx_slicers(dim, deg)
    coeffs = fftn(values / rescale).real
    for x0sl, degsl in zip(x0_slicer, deg_slicer):
        # halve the coefficients in each slice
        coeffs[x0sl] /= 2
        coeffs[degsl] /= 2

    if return_inf_norm:
        return coeffs[tuple(slices)], inf_norm
    else:
        return coeffs[tuple(slices)]
Esempio n. 2
0
def interval_approximate_1d(f,
                            a,
                            b,
                            deg,
                            return_bools=False,
                            return_inf_norm=False):
    """Finds the chebyshev approximation of a one-dimensional function on an
    interval.

    Parameters
    ----------
    f : function from R -> R
        The function to interpolate.
    a : float
        The lower bound on the interval.
    b : float
        The upper bound on the interval.
    deg : int
        The degree of the interpolation.
    return_inf_norm : bool
        Whether to return the inf norm of the function
    Returns
    -------
    coeffs : numpy array
        The coefficient of the chebyshev interpolating polynomial.
    inf_norm : float
        The inf_norm of the function
    """
    extrema = transform(np.cos((np.pi * np.arange(2 * deg)) / deg), a, b)
    values = f(extrema)

    if return_inf_norm:
        inf_norm = np.max(np.abs(values))

    coeffs = np.real(np.fft.fft(values / deg))
    coeffs[0] /= 2
    coeffs[deg] /= 2

    if return_bools:
        # Check to see if the sign changes on the interval
        is_positive = values > 0
        sign_change = any(is_positive) and any(~is_positive)
        if return_inf_norm: return coeffs[:deg + 1], sign_change, inf_norm
        else: return coeffs[:deg + 1], sign_change
    else:
        if return_inf_norm: return coeffs[:deg + 1], inf_norm
        else: return coeffs[:deg + 1]
Esempio n. 3
0
def get_abs_approx_tol(func, deg, a, b, dim):
    """ Gets an absolute approximation tolerance based on the assumption that
        on the interval of size linearization_size * 2, the function can be
        perfectly approximated by a low degree Chebyshev polynomial.

        Parameters
        ----------
            func : function
                Function to approximate.
            deg : int
                The degree to use to approximate the function on the interval.
            a : numpy array
                The lower bounds of the interval on which to approximate.
            b : numpy array
                The upper bounds of the interval on which to approximate.

        Returns
        -------
            abs_approx_tol : float
                The calculated absolute approximation tolerance based on the
                noise of the function on the small interval.
    """
    # Half the width of the smaller interval
    linearization_size = 1e-14

    # Get a random small interval from [-1, 1] and transform so it's
    # within [a, b]
    x = transform(random_point(dim), a, b)
    a2 = np.array(x - linearization_size)
    b2 = np.array(x + linearization_size)

    # Approximate with a low degree Chebyshev polynomial
    coeff = interval_approximate_nd(func, a2, b2, 2 * deg)
    coeff[deg_slices(deg, dim)] = 0

    # Sum up coeffieicents that are assumed to be just noise
    abs_approx_tol = np.sum(np.abs(coeff))

    # Divide by the number of spots that were summed up.
    numSpots = (deg * 2)**dim - (deg)**dim

    # Multiply by 10 to give a looser tolerance (speed-up)
    # print(abs_approx_tol*10 / numSpots)
    return abs_approx_tol * 10 / numSpots
Esempio n. 4
0
def subdivision_solve_nd(funcs,
                         a,
                         b,
                         deg,
                         target_deg,
                         interval_data,
                         root_tracker,
                         tols,
                         max_level,
                         good_degs=None,
                         level=0,
                         method='svd',
                         use_target_tol=False,
                         trust_small_evals=False):
    """Finds the common zeros of the given functions.

    All the zeros will be stored in root_tracker.

    Parameters
    ----------
    funcs : list
        Each element of the list is a callable function.
    a : numpy array
        The lower bound on the interval.
    b : numpy array
        The upper bound on the interval.
    deg : int
        The degree to approximate with in the chebyshev approximation.
    target_deg : int
        The degree to subdivide down to before building the Macaulay matrix.
    interval_data : IntervalData
        A class to run the subinterval checks and keep track of the solve
        progress
    root_tracker : RootTracker
        A class to keep track of the roots that are found.
    tols : Tolerances
        The tolerances to be used.
    max_level : int
        The maximum level for the recursion
    good_degs : numpy array
        Interpoation degrees that are guaranteed to give an approximation valid
        to within approx_tol.
    level : int
        The current level of the recursion.
    method : str (optional)
        The method to use when reducing the Macaulay matrix. Valid options are
        svd, tvb, and qrt.
    use_target_tol : bool
        Whether or not to use tols.target_tol when making approximations. This
        is necessary to get a sufficiently accurate approximation from which to
        build the Macaulay matrix and run the solver.
    """

    if level >= max_level:
        # TODO Refine case where there may be a root and it goes too deep.
        interval_data.track_interval("Too Deep", [a, b])
        # Return potential roots if the residuals are small
        root_tracker.add_potential_roots((a + b) / 2, a, b, "Too Deep.")
        return

    dim = len(a)

    if tols.check_eval_error:
        # Using the first abs_approx_tol
        if not use_target_tol:
            tols.abs_approx_tol = tols.abs_approx_tols[tols.currTol]
            if level % tols.check_eval_freq == 0:
                numSpots = (deg * 2)**len(a) - (deg)**len(a)
                for func in funcs:
                    tols.abs_approx_tol = max(
                        tols.abs_approx_tol,
                        numSpots * get_abs_approx_tol(func, 3, a, b, dim))
        # Using target_tol
        else:
            tols.target_tol = tols.target_tols[tols.currTol]
            if level % tols.check_eval_freq == 0:
                numSpots = (deg * 2)**len(a) - (deg)**len(a)
                for func in funcs:
                    tols.target_tol = max(
                        tols.target_tol,
                        numSpots * get_abs_approx_tol(func, 3, a, b, dim))

    # Buffer the interval to solve on a larger interval to account for
    # corners. Right now, it's set to be 5e-10 so that on [-1, 1], the
    # buffer goes out 1e-9 around the initial search interval.
    # DETERMINED BY EXPERIMENTATION
    interval_buffer_size = (b - a) * 5e-10
    og_a = a.copy()
    og_b = b.copy()
    a -= interval_buffer_size
    b += interval_buffer_size

    cheb_approx_list = []
    interval_data.print_progress()
    if good_degs is None:
        good_degs = [None] * len(funcs)
    inf_norms = []
    approx_errors = []
    # Get the chebyshev approximations
    num_funcs = len(funcs)
    for func_num, (func, good_deg) in enumerate(zip(funcs, good_degs)):
        if use_target_tol:
            coeff, inf_norm, approx_error = full_cheb_approximate(
                func, a, b, deg, tols.target_tol, tols.rel_approx_tol,
                good_deg)
        else:
            coeff, inf_norm, approx_error = full_cheb_approximate(
                func, a, b, deg, tols.abs_approx_tol, tols.rel_approx_tol,
                good_deg)
        inf_norms.append(inf_norm)
        approx_errors.append(approx_error)
        # Subdivides if a bad approximation
        if coeff is None:
            if not trust_small_evals:
                approx_errors = [max(err, macheps) for err in approx_errors]
            intervals = interval_data.get_subintervals(og_a, og_b,
                                                       cheb_approx_list,
                                                       approx_errors, False)

            #reorder funcs. TODO: fancier things like how likely it is to pass checks
            funcs2 = funcs.copy()
            if func_num + 1 < num_funcs:
                del funcs2[func_num]
                funcs2.append(func)
            for new_a, new_b in intervals:
                subdivision_solve_nd(funcs2,
                                     new_a,
                                     new_b,
                                     deg,
                                     target_deg,
                                     interval_data,
                                     root_tracker,
                                     tols,
                                     max_level,
                                     level=level + 1,
                                     method=method,
                                     trust_small_evals=trust_small_evals)
            return
        else:
            # Run checks to try and throw out the interval
            if not trust_small_evals:
                approx_error = max(approx_error, macheps)
            if interval_data.check_interval(coeff, approx_error, og_a, og_b):
                return

            cheb_approx_list.append(coeff)

    # Reduce the degree of the approximations while not introducing too much error
    coeffs, good_approx, approx_errors = trim_coeffs(cheb_approx_list,
                                                     tols.abs_approx_tol,
                                                     tols.rel_approx_tol,
                                                     inf_norms, approx_errors)
    if not trust_small_evals:
        approx_errors = [max(err, macheps) for err in approx_errors]
    # Used if subdividing further.
    # Only choose good_degs if the approximation after trim_coeffs is good.
    if good_approx:
        # good_degs are assumed to be 1 higher than the current approximation
        # but no larger than the initial degree for more accurate performance.
        good_degs = [min(coeff.shape[0], deg) for coeff in coeffs]
        good_zeros_tol = max(
            tols.min_good_zeros_tol,
            sum(np.abs(approx_errors)) * tols.good_zeros_factor)

    # Check if the degree is small enough or if trim_coeffs introduced too much error
    if np.any(np.array([coeff.shape[0] for coeff in coeffs]) > target_deg +
              1) or not good_approx:
        intervals = interval_data.get_subintervals(og_a, og_b,
                                                   cheb_approx_list,
                                                   approx_errors, True)
        for new_a, new_b in intervals:
            subdivision_solve_nd(funcs,
                                 new_a,
                                 new_b,
                                 deg,
                                 target_deg,
                                 interval_data,
                                 root_tracker,
                                 tols,
                                 max_level,
                                 good_degs,
                                 level + 1,
                                 method=method,
                                 trust_small_evals=trust_small_evals,
                                 use_target_tol=True)

    # Check if any approx error is greater than target_tol for Macaulay method
    elif np.any(
            np.array(approx_errors) > np.array(tols.target_tol) +
            tols.rel_approx_tol * np.array(inf_norms)):
        intervals = interval_data.get_subintervals(og_a, og_b,
                                                   cheb_approx_list,
                                                   approx_errors, True)
        for new_a, new_b in intervals:
            subdivision_solve_nd(funcs,
                                 new_a,
                                 new_b,
                                 deg,
                                 target_deg,
                                 interval_data,
                                 root_tracker,
                                 tols,
                                 max_level,
                                 good_degs,
                                 level + 1,
                                 method=method,
                                 trust_small_evals=trust_small_evals,
                                 use_target_tol=True)

    # Check if everything is linear
    elif np.all(np.array([coeff.shape[0] for coeff in coeffs]) == 2):
        if deg != 2:
            subdivision_solve_nd(funcs,
                                 a,
                                 b,
                                 2,
                                 target_deg,
                                 interval_data,
                                 root_tracker,
                                 tols,
                                 max_level,
                                 good_degs,
                                 level,
                                 method=method,
                                 trust_small_evals=trust_small_evals,
                                 use_target_tol=True)
            return
        zero, cond = solve_linear(coeffs)
        # Store the information and exit
        zero = good_zeros_nd(zero, good_zeros_tol, good_zeros_tol)
        zero = transform(zero, a, b)
        zero = zeros_in_interval(zero, og_a, og_b, dim)
        interval_data.track_interval("Base Case", [a, b])
        root_tracker.add_roots(zero, a, b, "Base Case")

    # Solve using spectral methods if stable.
    else:
        polys = [
            MultiCheb(coeff, lead_term=[coeff.shape[0] - 1], clean_zeros=False)
            for coeff in coeffs
        ]
        res = multiplication(polys,
                             max_cond_num=tols.max_cond_num,
                             method=method)
        #check for a conditioning error
        if res[0] is None:
            # Subdivide but run some checks on the intervals first
            intervals = interval_data.get_subintervals(og_a, og_b,
                                                       cheb_approx_list,
                                                       approx_errors, True)
            for new_a, new_b in intervals:
                subdivision_solve_nd(funcs,
                                     new_a,
                                     new_b,
                                     deg,
                                     target_deg,
                                     interval_data,
                                     root_tracker,
                                     tols,
                                     max_level,
                                     good_degs,
                                     level + 1,
                                     method=method,
                                     trust_small_evals=trust_small_evals,
                                     use_target_tol=True)
        else:
            zeros = res
            zeros = good_zeros_nd(zeros, good_zeros_tol, good_zeros_tol)
            zeros = transform(zeros, a, b)
            zeros = zeros_in_interval(zeros, og_a, og_b, dim)
            interval_data.track_interval("Macaulay", [a, b])
            root_tracker.add_roots(zeros, a, b, "Macaulay")
Esempio n. 5
0
def subdivision_solve_1d(f,
                         a,
                         b,
                         deg,
                         target_deg,
                         interval_data,
                         root_tracker,
                         tols,
                         max_level,
                         level=0,
                         method='svd',
                         trust_small_evals=False):
    """Finds the roots of a one-dimensional function using subdivision and
    chebyshev approximation.

    Parameters
    ----------
    f : function from R -> R
        The function to interpolate.
    a : numpy array
        The lower bound on the interval.
    b : numpy array
        The upper bound on the interval.
    deg : int
        The degree of the approximation.
    target_deg : int
        The degree to subdivide down to before building the Macauly matrix.
    interval_data : IntervalData
        A class to run the subinterval checks and keep track of the solve progress
    root_tracker : RootTracker
        A class to keep track of the roots that are found.
    tols : Tolerances
        The tolerances to be used.
    max_level : int
        The maximum level for the recursion
    level : int
        The current level of the recursion.

    Returns
    -------
    coeffs : numpy array
        The coefficient of the chebyshev interpolating polynomial.
    """
    if level > max_level:
        # TODO Refine case where there may be a root and it goes too deep.
        interval_data.track_interval("Too Deep", [a, b])
        return

    # Determine the point at which to subdivide the interval
    RAND = 0.5139303900908738
    interval_data.print_progress()

    # Approximate the function using Chebyshev polynomials
    coeff = interval_approximate_1d(f, a, b, deg)
    coeff2, sign_change, inf_norm = interval_approximate_1d(
        f, a, b, deg * 2, return_bools=True, return_inf_norm=True)

    coeff2[slice_top(coeff.shape)] -= coeff

    # Calculate the approximate error between the deg and 2*deg approximations
    error = np.sum(np.abs(coeff2))
    allowed_error = tols.abs_approx_tol + tols.rel_approx_tol * inf_norm

    if error > allowed_error:
        # Subdivide the interval and recursively call the function.
        div_spot = a + (b - a) * RAND
        good_deg = deg
        subdivision_solve_1d(f, a, div_spot, good_deg, target_deg,
                             interval_data, root_tracker, tols, max_level,
                             level + 1)
        subdivision_solve_1d(f, div_spot, b, good_deg, target_deg,
                             interval_data, root_tracker, tols, max_level,
                             level + 1)
    else:
        # Trim the coefficient array (reduce the degree) as much as we can.
        # This identifies a 'good degree' with which to approximate the function
        # if it is less than the given approx degree.
        last_coeff_size = abs(coeff[-1])
        new_error = error + last_coeff_size
        while new_error < allowed_error:
            if len(coeff) == 1:
                break
            #maybe a list pop here? idk if worth it to switch away from arrays
            coeff = coeff[:-1]
            last_coeff_size = abs(coeff[-1])
            error = new_error
            new_error = error + last_coeff_size
        if not trust_small_evals:
            error = max(error, macheps)
        good_deg = max(len(coeff) - 1, 1)

        # Run interval checks to eliminate regions
        if not sign_change:  # Skip checks if there is a sign change
            if interval_data.check_interval(coeff, error, a, b):
                return

        try:
            good_zeros_tol = max(tols.min_good_zeros_tol,
                                 error * tols.good_zeros_factor)
            zeros = transform(
                good_zeros_1d(multCheb(coeff), good_zeros_tol, good_zeros_tol),
                a, b)
            interval_data.track_interval("Macaulay", [a, b])
            root_tracker.add_roots(zeros, a, b, "Macaulay")
        except (ConditioningError, TooManyRoots) as e:
            div_spot = a + (b - a) * RAND
            subdivision_solve_1d(f, a, div_spot, good_deg, target_deg,
                                 interval_data, root_tracker, tols, max_level,
                                 level + 1)
            subdivision_solve_1d(f, div_spot, b, good_deg, target_deg,
                                 interval_data, root_tracker, tols, max_level,
                                 level + 1)