def subdivision_solve_nd(funcs, a, b, deg, target_deg, interval_data, root_tracker, tols, max_level, good_degs=None, level=0, method='svd'): """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. """ 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 if tols.check_eval_error: 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 * getAbsApproxTol(func, 3, a, b)) cheb_approx_list = [] interval_data.print_progress() dim = len(a) if good_degs is None: good_degs = [None] * len(funcs) inf_norms = [] approx_errors = [] #Get the chebyshev approximations for func, good_deg in zip(funcs, good_degs): coeff, change_sign, 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: intervals = get_subintervals(a, b, change_sign, None, None, None, approx_errors) 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, level=level + 1, method=method) return else: #if the function changes sign on at least one subinterval, skip the checks if np.any(change_sign): cheb_approx_list.append(coeff) continue #Run checks to try and throw out the interval if interval_data.check_interval(coeff, approx_error, a, b): return cheb_approx_list.append(coeff) #Make the system stable to solve coeffs, good_approx, approx_errors = trim_coeffs(cheb_approx_list, tols.abs_approx_tol, tols.rel_approx_tol, inf_norms, approx_errors) #Used if subdividing further. good_degs = [coeff.shape[0] - 1 for coeff in coeffs] good_zeros_tol = max( tols.min_good_zeros_tol, np.sum(np.abs(approx_errors)) * tols.good_zeros_factor) #Check if everything is linear if 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) return zero, cond = solve_linear(coeffs) grad = [MultiCheb(c).grad(zero) for c in coeffs] #Store the information and exit zero = good_zeros_nd(zero, good_zeros_tol, good_zeros_tol) zero = transform(zero, a, b) interval_data.track_interval("Base Case", [a, b]) root_tracker.add_roots(zero, a, b, "Base Case") #Check if anything is linear # elif np.any(np.array([coeff.shape[0] for coeff in coeffs]) == 2): # #Subdivide but run some checks on the intervals first # intervals = get_subintervals(a,b,np.arange(dim),interval_data,cheb_approx_list,change_sign,approx_errors,True) # for new_a, new_b in intervals: # subdivision_solve_nd(method,funcs,new_a,new_b,deg, target_deg,interval_data,root_tracker,tols,max_level,good_degs,level+1) #Runs the same things as above, but we want to get rid of that eventually so keep them seperate. elif np.any( np.array([coeff.shape[0] for coeff in coeffs]) > target_deg) or not good_approx: intervals = get_subintervals(a, b, np.arange(dim), interval_data, cheb_approx_list, change_sign, 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) #Solve using spectral methods if stable. else: polys = [ MultiCheb(coeff, lead_term=[coeff.shape[0] - 1], clean_zeros=False) for coeff in coeffs ] try: zeros = multiplication(polys, max_cond_num=tols.max_cond_num, method=method) grad = [[poly.grad(z) for poly in polys] for z in zeros] zeros = good_zeros_nd(zeros, good_zeros_tol, good_zeros_tol) zeros = transform(zeros, a, b) interval_data.track_interval("Macaulay", [a, b]) root_tracker.add_roots(zeros, a, b, "Macaulay") except (ConditioningError, TooManyRoots) as e: #Subdivide but run some checks on the intervals first intervals = get_subintervals(a, b, np.arange(dim), interval_data, cheb_approx_list, change_sign, 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)
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 solve(polys, MSmatrix=0, eigvals=True, verbose=False, return_all_roots=True, max_cond_num=1.e6, macaulay_zero_tol=1.e-12): ''' Finds the roots of the given list of polynomials. Parameters ---------- polys : list of polynomial objects Polynomials to find the common roots of. MSmatrix : int Controls which Moller-Stetter matrix is constructed For a univariate polynomial, the options are: 0 (default) -- The companion or colleague matrix, rotated 180 degrees 1 -- The unrotated companion or colleague matrix -1 -- The inverse of the companion or colleague matrix For a multivariate polynomial, the options are: 0 (default) -- The Moller-Stetter matrix of a random polynomial Some positive integer i <= dimension -- The Moller-Stetter matrix of x_i, where variables are index from x1, ..., xn Some negative integer i >= -dimension -- The Moller-Stetter matrix of x_i-inverse eigvals : bool Whether to compute roots of univariate polynomials from eigenvalues (True) or eigenvectors (False). Roots of multivariate polynomials are always comptued from eigenvectors verbose : bool Prints information about how the roots are computed. return_all_roots : bool If True returns all the roots, otherwise just the ones in the unit box. max_cond_num : float The maximum condition number of the Macaulay Matrix Reduction macaulay_zero_tol : float What is considered 0 in the macaulay matrix reduction. returns ------- roots : numpy array The common roots of the polynomials. Each row is a root. ''' polys = match_poly_dimensions(polys) # Determine polynomial type and dimension of the system poly_type = is_power(polys, return_string=True) dim = polys[0].dim if dim == 1: if len(polys) == 1: return oneD.solve(polys[0], MSmatrix=MSmatrix, eigvals=eigvals, verbose=verbose) else: zeros = np.unique( oneD.solve(polys[0], MSmatrix=MSmatrix, eigvals=eigvals, verbose=verbose)) #Finds the roots of each succesive polynomial and checks which roots are common. for poly in polys[1:]: if len(zeros) == 0: break zeros2 = np.unique( oneD.solve(poly, MSmatrix=MSmatrix, eigvals=eigvals, verbose=verbose)) common = list() tol = 1.e-10 for zero in zeros2: spot = np.where(np.abs(zeros - zero) < tol) if len(spot[0]) > 0: common.append(zero) zeros = common return zeros else: if MSmatrix < 0: return division(polys, verbose=verbose, divisor_var=-MSmatrix - 1, return_all_roots=return_all_roots, max_cond_num=max_cond_num, macaulay_zero_tol=macaulay_zero_tol) else: return multiplication(polys, verbose=verbose, MSmatrix=MSmatrix, return_all_roots=return_all_roots, max_cond_num=max_cond_num, macaulay_zero_tol=macaulay_zero_tol)