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)]
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]
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
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")
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)