def test_quadratic_check(): #keep this updated with the deg_dim used in subdivision solve deg_dim = {1: 100, 2: 20, 3: 9, 4: 9} num_tests_per_dim = 100 tests_per_batch = num_tests_per_dim // 2 tol = 1.e-4 for dim in deg_dim.keys(): print(dim) deg = deg_dim[dim] interval_data = IntervalData(-np.ones(dim), np.ones(dim), []) subintervals = interval_data.get_subintervals(interval_data.a, interval_data.b, [], tol, False) _quadratic_check = lambda c, tol: ~quadratic_check( c, interval_data.mask, tol, interval_data.RAND, subintervals) np.random.seed(42) rand_test_cases = np.random.rand(*[tests_per_batch] + [deg] * dim) * 2 - 1 randn_test_cases = np.random.randn(*[tests_per_batch] + [deg] * dim) for c in rand_test_cases: assert np.allclose(base_quadratic_check(c, tol), _quadratic_check(c, tol).flatten()) for c in randn_test_cases: assert np.allclose(base_quadratic_check(c, tol), _quadratic_check(c, tol).flatten())
def test_zero_check2D(): #curvature_check was causing import errors so it's not included... interval_checks = [constant_term_check, full_quad_check, full_cubic_check] a = -np.ones(2) b = np.ones(2) interval_data = IntervalData(a, b, []) tol = 1.e-4 interval_checks.extend([lambda x, tol: linear_check(x, [(a, b)], tol)[0]]) interval_checks.extend([ lambda x, tol: ~quadratic_check(x, interval_data.mask, tol, interval_data.RAND, np.array([(a, b)] * 4))[0] ]) test_cases = [ np.array([[100, 2, -2, -1], [2, 2, 1, 0], [-1, 1, 0, 0], [1, 0, 0, 0]]), np.array([[6.5, 2, -2, -1], [2, 2, 1, 0], [-1, 1, 0, 0], [1, 0, 0, 0]]), np.array([ [1.019, .2, .5], [0.2, .001, 0], [0.5, 0, 0], ]), np.array([ [-1.3, .2, 0.5], [.2, .001, 0], [.5, 0, 0], ]), np.array([[-1.6, .2, .5, .1], [.2, .001, .1, 0], [0.5, .1, 0, 0], [0.1, 0, 0, 0]]) ] correct_results = [False, True, True, True, True, True] for method in interval_checks[:-2]: for res, c in zip(correct_results, test_cases): assert res == method(c, tol) for res, c in zip(correct_results, test_cases): assert res == ~interval_checks[-1](c, tol)[0] print(~interval_checks[-1](c, tol)[0])
def solve(funcs, a, b, rel_approx_tol=1.e-15, abs_approx_tol=1.e-12, max_cond_num=1e5, good_zeros_factor=100, min_good_zeros_tol=1e-5, check_eval_error=True, check_eval_freq=1, plot=False, plot_intervals=False, deg=None, target_deg=2, return_potentials=False, method='svd', target_tol=1.01 * macheps, trust_small_evals=False, intervalReductions=["improveBound", "getBoundingParallelogram"]): """ Finds the real roots of the given list of functions on a given interval. All of the tolerances can be passed in as numbers of iterable types. If multiple are passed in as iterable types they must have the same length. When the length is more than 1, they are used one after the other to polish the roots. Parameters ---------- funcs : list of vectorized, callable functions Functions to find the common roots of. More efficient if functions have an 'evaluate_grid' method handle function evaluation at an grid of points. a : numpy array The lower bound on the interval. b : numpy array The upper bound on the interval. rel_approx_tol : float or list The relative tolerance used in the approximation tolerance. The error is bouned by error < abs_approx_tol + rel_approx_tol * inf_norm_of_approximation abs_approx_tol : float or list The absolute tolerance used in the approximation tolerance. The error is bouned by error < abs_approx_tol + rel_approx_tol * inf_norm_of_approximation max_cond_num : float or list The maximum condition number of the Macaulay Matrix Reduction macaulay_zero_tol : float or list What is considered 0 in the macaulay matrix reduction. good_zeros_factor : float or list Multiplying this by the approximation error gives how far outside of [-1, 1] a root can be and still be considered inside the interval. min_good_zeros_tol : float or list The smallest the good_zeros_tol can be, which is how far outside of [-1, 1] a root can be and still be considered inside the interval. check_eval_error : bool Whether to compute the evaluation error on the fly and replace the approx tol with it. check_eval_freq : int The evaluation error will be computed on levels that are multiples of this. plot : bool If True plots the zeros-loci of the functions along with the computed roots plot_intervals : bool If True, plot is True, and the functions are 2 dimensional, plots what check/method solved each part of the interval. deg : int The degree used for the approximation. If None, the following degrees are used. Degree 100 for 1D functions. Degree 20 for 2D functions. Degree 9 for 3D functions. Degree 9 for 4D functions. Degree 2 for 5D functions and above. target_deg : int The degree the approximation needs to be trimmed down to before the Macaulay solver is called. If unspecified, it will either be 5 (for 2D functions) or match the deg argument. return_potentials : bool If True, returns the potential roots. Else, it does not. method : str (optional) The method to use when reducing the Macaulay matrix. Valid options are svd, tvb, and qrt. target_tol : float The final absolute approximation tolerance to use before using any sort of solver (Macaulay, linear, etc). trust_small_evals : bool Whether or not to trust function evaluations that may give floats smaller than machine epsilon. This should only be set to True if the function evaluations are very accurate. intervalReductions : list A list specifying the types of interval reductions that should be performed on each subinterval. The order of methods in the list determines the order in which the interval reductions are performed. To stop any interval reduction method from being run, pass in an empty list to this parameter. If finding roots of a univariate function, `funcs` does not need to be a list, and `a` and `b` can be floats instead of arrays. Returns ------- zeros : numpy array The common zeros of the polynomials. Each row is a root. """ # Detect the dimension if isinstance(funcs, list): dim = len(funcs) elif callable(funcs): dim = 1 else: raise ValueError('`funcs` must be a callable or list of callables.') # make a and b the right type a = np.float64(a) b = np.float64(b) # Choose an appropriate max degree for the given dimension if none is specified. if deg is None: deg_dim = {1: 100, 2: 20, 3: 9, 4: 9} if dim > 4: deg = 2 else: deg = deg_dim[dim] # Sets up the tolerances. if isinstance(abs_approx_tol, list): abs_approx_tol = [max(tol, 1.01 * macheps) for tol in abs_approx_tol] else: abs_approx_tol = max(abs_approx_tol, 1.01 * macheps) tols = Tolerances(rel_approx_tol=rel_approx_tol, abs_approx_tol=abs_approx_tol, max_cond_num=max_cond_num, good_zeros_factor=good_zeros_factor, min_good_zeros_tol=min_good_zeros_tol, check_eval_error=check_eval_error, check_eval_freq=check_eval_freq, target_tol=target_tol) tols.nextTols() # Set up the interval data and root tracker classes and cheb blocky copy arr interval_data = IntervalData(a, b, intervalReductions) root_tracker = RootTracker() values_arr.memo = {} initialize_values_arr(dim, 2 * (deg + 3)) if dim == 1: # In one dimension, we don't use target_deg; it's the same as deg target_deg = deg solve_func = subdivision_solve_1d if isinstance(funcs, list): funcs = funcs[0] else: solve_func = subdivision_solve_nd # TODO : Set the maximum number of subdivisions so that # intervals cannot possibly be smaller than 2^-51 max_level = 52 # Initial Solve solve_func(funcs, a, b, deg, target_deg, interval_data, root_tracker, tols, max_level, method=method, trust_small_evals=trust_small_evals) root_tracker.keep_possible_duplicates() # Polishing while tols.nextTols(): polish_intervals = root_tracker.get_polish_intervals() interval_data.add_polish_intervals(polish_intervals) for new_a, new_b in polish_intervals: interval_data.start_polish_interval() solve_func(funcs, new_a, new_b, deg, target_deg, interval_data, root_tracker, tols, max_level, method=method) root_tracker.keep_possible_duplicates(), print("\rPercent Finished: 100%{}".format(' ' * 50)) # Print results interval_data.print_results() # Plotting if plot: if dim == 1: x = np.linspace(a, b, 1000) plt.plot(x, funcs(x), color='k') plt.plot(np.real(root_tracker.roots), np.zeros(len(root_tracker.roots)), 'o', color='none', markeredgecolor='r') plt.show() elif dim == 2: interval_data.plot_results(funcs, root_tracker.roots, plot_intervals) if len(root_tracker.potential_roots) != 0: warnings.warn( "Some intervals subdivided too deep and some potential roots were found. To access these roots, rerun the solver with the keyword return_potentials=True" ) if return_potentials: return root_tracker.roots, root_tracker.potential_roots else: return root_tracker.roots
def solve(funcs, a, b, rel_approx_tol=1.e-12, abs_approx_tol=1.e-8, max_cond_num=1e7, good_zeros_factor=100, min_good_zeros_tol=1e-5, check_eval_error=True, check_eval_freq=1, plot=False, plot_intervals=False, deg=9, target_deg=5, max_level=999, return_potentials=False, method='svd'): ''' Finds the real roots of the given list of functions on a given interval. All of the tolerances can be passed in as numbers of iterable types. If multiple are passed in as iterable types they must have the same length. When the length is more than 1, they are used one after the other to polish the roots. Parameters ---------- funcs : list of vectorized, callable functions Functions to find the common roots of. More efficient if functions have an 'evaluate_grid' method handle function evaluation at an grid of points. a : numpy array The lower bound on the interval. b : numpy array The upper bound on the interval. rel_approx_tol : float or list The relative tolerance used in the approximation tolerance. The error is bouned by error < abs_approx_tol + rel_approx_tol * inf_norm_of_approximation abs_approx_tol : float or list The absolute tolerance used in the approximation tolerance. The error is bouned by error < abs_approx_tol + rel_approx_tol * inf_norm_of_approximation max_cond_num : float or list The maximum condition number of the Macaulay Matrix Reduction macaulay_zero_tol : float or list What is considered 0 in the macaulay matrix reduction. good_zeros_factor : float or list Multiplying this by the approximation error gives how far outside of [-1,1] a root can be and still be considered inside the interval. min_good_zeros_tol : float or list The smallest the good_zeros_tol can be, which is how far outside of [-1,1] a root can be and still be considered inside the interval. check_eval_error : bool Whether to compute the evaluation error on the fly and replace the approx tol with it. check_eval_freq : int The evaluation error will be computed on levels that are multiples of this. plot : bool If True plots the zeros-loci of the functions along with the computed roots plot_intervals : bool If True, plot is True, and the functions are 2 dimensional, plots what check/method solved each part of the interval. deg : int The degree used for the approximation. If None, the following degrees are used. Degree 50 for 1D functions. Degree 9 for 2D functions. Degree 5 for 3D functions. Degree 3 for 4D functions. Degree 2 for 5D functions and above. target_deg : int The degree the approximation needs to be trimmed down to before the Macaulay solver is called. If unspecified, it will either be 5 (for 2D functions) or match the deg argument. max_level : int The maximum levels deep the recursion will go. Increasing it above 999 may result in recursion error! return_potentials : bool If True, returns the potential roots. Else, it does not. method : str (optional) The method to use when reducing the Macaulay matrix. Valid options are svd, tvb, and qrt. If finding roots of a univariate function, `funcs` does not need to be a list, and `a` and `b` can be floats instead of arrays. Returns ------- zeros : numpy array The common zeros of the polynomials. Each row is a root. ''' #Detect the dimension if not isinstance(funcs, list): dim = 1 else: dim = len(funcs) #make a and b the right type a = np.float64(a) b = np.float64(b) # Choose an appropriate max degree for the given dimension if none is specified. if deg is None: deg_dim = {1: 50, 2: 9, 3: 5, 4: 3} if dim > 4: deg = 2 else: deg = deg_dim[dim] # Choose an appropriate target degree if none is specified if target_deg is None: if dim != 2: target_deg = deg else: target_deg = 5 #Sets up the tolerances. tols = Tolerances(rel_approx_tol=rel_approx_tol, abs_approx_tol=abs_approx_tol, max_cond_num=max_cond_num, good_zeros_factor=good_zeros_factor, min_good_zeros_tol=min_good_zeros_tol, check_eval_error=check_eval_error, check_eval_freq=check_eval_freq) tols.nextTols() #Set up the interval data and root tracker classes interval_data = IntervalData(a, b) root_tracker = RootTracker() if dim == 1: solve_func = subdivision_solve_1d if isinstance(funcs, list): funcs = funcs[0] else: solve_func = subdivision_solve_nd #Initial Solve solve_func(funcs, a, b, deg, target_deg, interval_data, \ root_tracker, tols, max_level, method=method) root_tracker.keep_possible_duplicates() #Polishing while tols.nextTols(): polish_intervals = root_tracker.get_polish_intervals() interval_data.add_polish_intervals(polish_intervals) for new_a, new_b in polish_intervals: interval_data.start_polish_interval() solve_func(funcs, new_a, new_b, deg, target_deg, interval_data, root_tracker, tols, max_level, method=method) root_tracker.keep_possible_duplicates(), print("\rPercent Finished: 100%{}".format(' ' * 50)) #Print results interval_data.print_results() #Plotting if plot: if dim == 1: x = np.linspace(a, b, 1000) plt.plot(x, funcs(x), color='k') plt.plot(np.real(root_tracker.roots), np.zeros(len(root_tracker.roots)), 'o', color='none', markeredgecolor='r') plt.show() elif dim == 2: interval_data.plot_results(funcs, root_tracker.roots, plot_intervals) if len(root_tracker.potential_roots) != 0: warnings.warn( "Some intervals subdivided too deep and some potential roots were found. To access these roots, rerun the solver with the keyword return_potentials=True" ) if return_potentials: return root_tracker.roots, root_tracker.potential_roots else: return root_tracker.roots
def base_quadratic_check(test_coeff, tol): """Slow nd-quadratic check to test against. """ #get the dimension and make sure the coeff tensor has all the right # quadratic coeff spots, set to zero if necessary dim = test_coeff.ndim interval_data = IntervalData(-np.ones(dim), np.ones(dim), []) intervals = interval_data.get_subintervals(interval_data.a, interval_data.b, [], tol, False) padding = [(0, max(0, 3 - i)) for i in test_coeff.shape] test_coeff = np.pad(test_coeff.copy(), padding, mode='constant') #Possible extrema of qudaratic part are where D_xk = 0 for some subset of the variables xk # with the other variables are fixed to a boundary value #Dxk = c[0,...,0,1,0,...0] (k-spot is 1) + 4c[0,...,0,2,0,...0] xk (k-spot is 2) # + \Sum_{j\neq k} xj c[0,...,0,1,0,...,0,1,0,...0] (k and j spot are 1) #This gives a symmetric system of equations AX+B = 0 #We will fix different columns of X each time, resulting in slightly different #systems, but storing A and B now will be helpful later #pull out coefficients we care about quad_coeff = np.zeros([3] * dim) A = np.zeros([dim, dim]) B = np.zeros(dim) for spot in itertools.product(np.arange(3), repeat=dim): if np.sum(spot) < 3: spot_array = np.array(spot) if np.sum(spot_array != 0) == 2: #coef of cross terms i, j = np.where(spot_array != 0)[0] A[i, j] = test_coeff[spot].copy() A[j, i] = test_coeff[spot].copy() elif np.any(spot_array == 2): #coef of pure quadratic terms i = np.where(spot_array != 0)[0][0] A[i, i] = 4 * test_coeff[spot].copy() elif np.any(spot_array == 1): #coef of linear terms i = np.where(spot_array != 0)[0][0] B[i] = test_coeff[spot].copy() quad_coeff[spot] = test_coeff[spot] test_coeff[spot] = 0 #create a poly object for evaluations quad_poly = MultiCheb(quad_coeff) #The sum of the absolute values of everything else other_sum = np.sum(np.abs(test_coeff)) def powerset(iterable): "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" s = list(iterable) return itertools.chain.from_iterable(itertools.combinations(s, r)\ for r in range(len(s)+1)) mask = [] for interval_num, interval in enumerate(intervals): extreme_points = [] for fixed in powerset(np.arange(dim)): fixed = np.array(fixed) if len(fixed) == 0: #fix no vars--> interior if np.linalg.matrix_rank(A) < A.shape[0]: #no interior critical point continue X = la.solve(A, -B, assume_a='sym') #make sure it's in the domain if np.all([ interval[0][i] <= X[i] <= interval[1][i] for i in range(dim) ]): extreme_points.append(quad_poly(X)) elif len(fixed) == dim: #fix all variables--> corners for corner in itertools.product([0, 1], repeat=dim): #j picks if upper/lower bound. i is which var extreme_points.append( quad_poly( [interval[j][i] for i, j in enumerate(corner)])) else: #fixed some variables --> "sides" #we only care about the equations from the unfixed variables unfixed = np.delete(np.arange(dim), fixed) A_ = A[unfixed][:, unfixed] if np.linalg.matrix_rank(A_) < A_.shape[0]: #no solutions continue fixed_A = A[unfixed][:, fixed] B_ = B[unfixed] for side in itertools.product([0, 1], repeat=len(fixed)): X0 = np.array([interval[j][i] for i, j in enumerate(side)]) X_ = la.solve(A_, -B_ - fixed_A @ X0, assume_a='sym') X = np.zeros(dim) X[fixed] = X0 X[unfixed] = X_ if np.all([ interval[0][i] <= X[i] <= interval[1][i] for i in range(dim) ]): extreme_points.append(quad_poly(X)) #No root if min(extreme_points) > (other_sum + tol) # OR max(extreme_points) < -(other_sum+tol) #Logical negation gives the boolean we want mask.append( np.min(extreme_points) < (other_sum + tol) and np.max(extreme_points) > -(other_sum + tol)) return mask