def calculate_convergence(gcf: GeneralizedContinuedFraction, reference, plot=False, title=''): """ calculate convergence rate of General Continued Fraction (in reference to some constant x). the result is the average number of decimal digits, per term of the general continued fraction. :param plot: whether or not to plot the graph of the log10 difference between convergent and x. :param gcf: General Continued Fraction to calculate. :param title: (optional) title of graph. :param reference: x """ q_ = [0, 1] p_ = [1, gcf.a_[0]] log_diff = [mpmath.log10(abs((dec(p_[1]) / dec(q_[1])) - reference))] length = min(200, len(gcf.b_)) for i in range(1, length): q_.append(gcf.a_[i] * q_[i] + gcf.b_[i - 1] * q_[i - 1]) p_.append(gcf.a_[i] * p_[i] + gcf.b_[i - 1] * p_[i - 1]) if q_[i + 1] == 0: part_convergent = 0 else: part_convergent = dec(p_[i + 1]) / dec(q_[i + 1]) if not mpmath.isfinite(part_convergent): length = i break log_diff.append(mpmath.log10(abs(part_convergent - reference))) if plot: plt.plot(range(length), log_diff) plt.title(title) plt.show() log_slope = 2 * (log_diff[length - 1] - log_diff[length // 2]) / length return -log_slope
def __init__(self, num_of_iterations=300, enum_only_exp_conv=False, avoid_int_roots=True, should_gen_contfrac=True, avoid_zero_b=True, threshold=None, prec=80, special_params=None): """ init :param self: self :param num_of_iterations: if should_gen_contfrac=True, then how many iterations should be promoted. :param enum_only_exp_conv: skip contfracs that surely won't converge (at least) exponentially :param avoid_int_roots: avoid contfracs for which b (or any of its interlaces) have an integer root. it will affect only b polynomial of degree<=3. notice that if interlace is used, this may skip valid contfracs (the i root value may be assigned to another interlace polynomial instead of the one with the root). :param should_gen_contfrac: generate ContFrac object and return (cont_frac, pas, pbs) instead of (pas, pbs) :param avoid_zero_b: raise exception ZeroB and cancel if finite contfrac (b_i=0 for some i) is achieved :param threshold: threshold for considering contfrac_res == target_val if abs(contfrac_res-target_val) < threshold. Defaults to 10^-4. :param prec: decimal precision to be used for calculations :param special_params: unused. Used differently by inheriting subclasses. :return: nothing """ self.num_of_iterations = num_of_iterations self._enum_only_exp_conv = enum_only_exp_conv self._avoid_int_roots = avoid_int_roots self._should_gen_contfrac = should_gen_contfrac self._avoid_zero_b = avoid_zero_b self.good_params = [] if threshold is None: self.threshold = dec(10)**dec(-4) else: self.threshold = threshold self._prec = prec set_precision(prec)
def test_ulcd_evaluator(u, l, c, d, target_constant): ulcd_evaluator = lhs_evaluators.ULCDEvaluator((u, l, c, d), target_constant) expected_value = (u / target_constant + target_constant / l + c) / d assert abs(ulcd_evaluator.get_val() - expected_value) < dec('1E-38') ulcd_evaluator.add_int(3) expected_value += 3 assert abs(ulcd_evaluator.get_val() - expected_value) < dec('1E-38') ulcd_evaluator.flip_sign() expected_value *= -1 assert abs(ulcd_evaluator.get_val() - expected_value) < dec('1E-38')
def finalize_iterations(self, params, promo_mat, n): """Finalize the iteration by computing contfrac_res = p/q. p, q are taken from promo_mat (see self.iteration_algorithm for more info). contfrac_res is saved to params (see self.iteration_algorithm for more info). n is unused (and is here only to keep method signature).""" p_vec, q_vec = promo_mat p = dec(p_vec[0]) q = dec(q_vec[0]) if not isnormal(q): contfrac_res = dec('NaN') else: contfrac_res = p / q params['contfrac_res'] = contfrac_res return (params, promo_mat)
def __call__(self, x=None): """ apply transformation on X. :param x (for our project it will always be 1): :return: result of mobius transformation """ a, b, c, d = self.__values() if x is not None: numerator = dec(a * x + b) denominator = dec(c * x + d) else: numerator = dec(b) denominator = dec(d) return numerator / denominator
def find_transform(x, y, limit, threshold=1e-7): """ find a integer solution to ax + b - cxy - dy = 0 this will give us the mobius transform: T(x) = y :param x: numeric constant to check :param y: numeric manipulation of constant :param limit: range to look at :param threshold: optimal solution threshold. :return MobiusTransform in case of success or None. """ x1 = x x2 = dec(1.0) x3 = -x * y x4 = -y solver = Solver('mobius', Solver.CBC_MIXED_INTEGER_PROGRAMMING) a = solver.IntVar(-limit, limit, 'a') b = solver.IntVar(-limit, limit, 'b') c = solver.IntVar(-limit, limit, 'c') d = solver.IntVar(-limit, limit, 'd') f = solver.NumVar(0, 1, 'f') solver.Add(f == (a * x1 + b * x2 + c * x3 + d * x4)) solver.Add(a * x1 + b >= 1) # don't except trivial solutions and remove some redundancy solver.Minimize(f) status = solver.Solve() if status == Solver.OPTIMAL: if abs(solver.Objective().Value()) <= threshold: res_a, res_b, res_c, res_d = int(a.solution_value()), int(b.solution_value()),\ int(c.solution_value()), int(d.solution_value()) ret = MobiusTransform( np.array([[res_a, res_b], [res_c, res_d]], dtype=object)) ret.normalize() return ret else: return None
def test_rationalfunc_evaluator(numerator_coeffs, denominator_coeffs, target_constant): rationalfunc_evaluator = lhs_evaluators.RationalFuncEvaluator( (numerator_coeffs, denominator_coeffs, 0), target_constant) expected_value = MathOperations.subs_in_polynom(numerator_coeffs, target_constant) / \ MathOperations.subs_in_polynom(denominator_coeffs, target_constant) assert abs(rationalfunc_evaluator.get_val() - expected_value) < dec('1E-38') rationalfunc_evaluator.add_int(3) expected_value += 3 assert abs(rationalfunc_evaluator.get_val() - expected_value) < dec('1E-38') rationalfunc_evaluator.flip_sign() expected_value *= -1 assert abs(rationalfunc_evaluator.get_val() - expected_value) < dec('1E-38')
def eval_dec_contfrac_by_polys(a, b, calculation_depth): """a, b are given as polynomials with an option to interlaced polynomials. See documentation for ContFrac for more details. Calculates the continued fraction into 'calculation_depth' depth. Calculations are done with Decimal, which enables arbitrary precision. Please set the Decimal context precision as needed.""" if isinstance(a[0], (int, dec)): a = [a] if isinstance(b[0], (int, dec)): b = [b] a = [[dec(i) for i in p] for p in a] b = [[dec(i) for i in p] for p in b] return eval_contfrac([ MathOperations.subs_in_polynom(a[i % len(a)], i) for i in range(calculation_depth) ], [ MathOperations.subs_in_polynom(b[i % len(b)], i) for i in range(1, calculation_depth) ])
def evaluate_contfrac(ab, accuracy, convergence_info): cont_frac = cont_fracs.ContFrac(a_coeffs=ab[0], b_coeffs=ab[1]) if convergence_info: cont_frac.set_approach_type_and_params(convergence_info) # cont_frac.reinitialize(a_coeffs=ab[0], b_coeffs=ab[1]) # This is a horrible hack, slowing down EVERYTHING. As a first step, this should be replaced by a small # correction to the convergence parameters. Maybe taking the upper/lower bound of confidence from the # fitting results. # TODO: fix accoridng to the above comment. cont_frac.gen_iterations(num_of_iterations, dec('1E-%d' % (accuracy + 100))) return cont_frac.get_result()
def _calc_val(self): """Calculates the value of the LHS based on the LHS parameters (numerator, denominator, target constant and added int).""" self.numerator = MathOperations.subs_in_polynom( self.numerator_p, self.target_constant) self.denominator = MathOperations.subs_in_polynom( self.denominator_p, self.target_constant) # if not self.denominator.is_normal() or not self.numerator.is_normal(): if not isnormal(self.denominator) or not isnormal(self.numerator): self.val = dec('nan') else: self.val = self.numerator / self.denominator + self.added_int
def iteration_algorithm(self, params, state_mat, i, print_calc): """Generates the next iteration, using the "matrix product" of params and promo_matrix of: | P_{i+1} Q_{i+1} | | a_i b_i | | P_i Q_i | | P_0 Q_0 | | a_0 1 | | P_i Q_i | = | 1 0 | X | P_{i-1} Q_{i-1} | , | P_{-1} Q_{-1} | = | 1 0 | contfrac_i = P_i/Q_i params - {'a_coeffs': ..., 'b_coeffs': ..., 'contfrac_res': ...} state_mat - [[p_i, p_{i-1}], [q_i, q_{i-1}]], is actually the current state matrix. Name is only for signature compatibility. i - iteration index, used to generate the [a_i b_i; 1 0] matrix. print_calc - whether to print the calculation process. For debugging.""" i += 1 a_coeffs = params['a_coeffs'] b_coeffs = params['b_coeffs'] # state_mat - the [a_i b_i; 1 0] matrix. Actually, it's just a [a_i, b_i] list ab_mat = self._iter_promo_matrix_generator(i, a_coeffs, b_coeffs, print_calc) p_vec, q_vec = state_mat # TODO: if calculations are slow, once in a while (i % 100 == 0?) check if gcd(p_i,p_{i-1}) != 0 # TODO: and take it out as a factor (add p_gcd, q_gcd params) new_p_vec = (sum([l * m for l, m in zip(p_vec, ab_mat)]), p_vec[0]) new_q_vec = (sum([l * m for l, m in zip(q_vec, ab_mat)]), q_vec[0]) if dec('inf') in new_p_vec: raise ValueError('infinity p') if dec('inf') in new_q_vec: raise ValueError('infinity q') if self._logging: p_i = dec(new_p_vec[0]) q_i = dec(new_q_vec[0]) if not isnormal(q_i): contfrac_res_i = dec('NaN') else: contfrac_res_i = p_i / q_i params['contfrac_res'] = contfrac_res_i new_promo_mat = (new_p_vec, new_q_vec) return (params, new_promo_mat)
def check_and_modify_precision(const, transform, const_gen, offset): """ tried to use this to calculate and enlarge precision along the way.. but it only made it slower. right now this is unused. :param const: :param transform: :param const_gen: :param offset: :return: """ const_work = const while True: epsilon = dec(2)**(-mpmath.mp.prec + 5) const_d = const_work - epsilon const_u = const_work + epsilon next_d = floor(transform(const_d)) next_u = floor(transform(const_u)) if next_d == next_u: return next_d, const_work mpmath.mp.prec += 100 const_work = const_gen() + offset
(1, 1, 0, -1), (1, 1, 0, 1), (1, 1, 1, -2), })]) def test_ulcd_enumerator(u_range, l_range, c_range, d_range, results): ulcd_enum = lhs_evaluators.ULCDEnumerator( [u_range, l_range, c_range, d_range], target_constant=1) ulcds_generator = ulcd_enum.generator() assert {ulcd_eval.get_params() for ulcd_eval in ulcds_generator }.symmetric_difference(results) == set() @pytest.mark.parametrize( 'numerator_coeffs, denominator_coeffs, force_numerator_greater, target_constant, results', [([[-1, 2], [-1, 2]], [[-1, 2], [-1, 2]], False, dec(5), { ((-1, -1), (-1, -1)), ((-1, -1), (-1, 0)), ((-1, -1), (-1, 1)), ((-1, -1), (0, -1)), ((-1, -1), (0, 1)), ((-1, -1), (1, -1)), ((-1, -1), (1, 0)), ((-1, -1), (1, 1)), ((-1, 0), (-1, -1)), ((-1, 0), (-1, 1)), ((-1, 0), (0, -1)), ((-1, 0), (0, 1)), ((-1, 0), (1, -1)), ((-1, 0), (1, 1)), ((-1, 1), (-1, -1)),
def evaluate(self): if self.A == 0: return dec(0) return dec(self.B) / dec(self.A)
def _estimate_approach_type_and_params_inner_alg( self, find_poly_parameter=False, iters=5000, initial_cutoff=1500, iters_step=500, exponential_threshold=1.1): """Returns 'exp', 'super_exp', 'poly2sympoly', 'undefined', 'fast' and 'mixed', as a tuple of (string,num): (approach_type, approach_parameter) or ('poly2sympoly', (approach_parameter, R**2)).""" # This is an old function. It's still in use, but it's yucky and I don't feel like going through it and # document it. Sorry :( # Hopefully it will be replaced with some theoretical insights. # HOWEVER, IT IS GENERAL AND MAY BE USEFUL FOR NEW, FUTURE REPRESENTATION METHODS. # See the "convergence_analysis.pdf" file for further details about theory behind this function. # It evaluates and categorizes by computing linear fitting to the error/iterations plot. Regular or logarithmic. effective_zero = dec(10)**-mp.dps if iters_step < 6: ValueError('iters_step should be at least 4') approach_type = None approach_parameter = 0 delta_pair = [] delta_odd = [] self.gen_iterations(initial_cutoff) res_0 = self.get_result() self.add_iterations(1) res_1 = self.get_result() self.add_iterations(1) res_2 = self.get_result() self.add_iterations(1) res_3 = self.get_result() self.add_iterations(1) res_4 = self.get_result() self.add_iterations(1) res_5 = self.get_result() delta_pair.append((initial_cutoff, abs(res_2 - res_0))) delta_pair.append((initial_cutoff + 2, abs(res_4 - res_2))) delta_odd.append((initial_cutoff + 1, abs(res_3 - res_1))) delta_odd.append((initial_cutoff + 3, abs(res_5 - res_3))) for i in range(initial_cutoff + iters_step, iters + 1, iters_step): # -3 for the iterations of res_1, res_2, res_3 that were already executed self.add_iterations(iters_step - 5) res_0 = self.get_result() self.add_iterations(1) res_1 = self.get_result() self.add_iterations(1) res_2 = self.get_result() self.add_iterations(1) res_3 = self.get_result() self.add_iterations(1) res_4 = self.get_result() self.add_iterations(1) res_5 = self.get_result() delta_pair.append((i, abs(res_2 - res_0))) delta_pair.append((i + 2, abs(res_4 - res_2))) delta_odd.append((i + 1, abs(res_3 - res_1))) delta_odd.append((i + 3, abs(res_5 - res_3))) pair_diminish = False odd_diminish = False if len(delta_pair) > 3 and all( [abs(p[1]) < effective_zero for p in delta_pair[-3:]]): pair_diminish = True if len(delta_odd) > 3 and all( [abs(p[1]) < effective_zero for p in delta_odd[-3:]]): odd_diminish = True # if one diminishes and the other isn't, return 'undefined' if pair_diminish ^ odd_diminish: approach_type = 'undefined' elif pair_diminish and odd_diminish: approach_type = 'fast' # if approach_type: # return (approach_type, approach_parameter) pair_ratio = [ (delta_pair[i][0], delta_pair[i][1] / delta_pair[i + 1][1]) for i in range(0, len(delta_pair), 2) if delta_pair[i][1] != 0 and delta_pair[i + 1][1] != 0 and not isnan(delta_pair[i][1]) and not isnan(delta_pair[i + 1][1]) ] odd_ratio = [ (delta_odd[i][0], delta_odd[i][1] / delta_odd[i + 1][1]) for i in range(0, len(delta_odd), 2) if delta_odd[i][1] != 0 and delta_odd[i + 1][1] != 0 and not isnan(delta_odd[i][1]) and not isnan(delta_odd[i + 1][1]) ] if len(pair_ratio) < 6: return (approach_type, approach_parameter) mean_pair_ratio = sum([p for i, p in pair_ratio]) / len(pair_ratio) mean_pair_ratio_avg_square_error = sum([ (r - mean_pair_ratio)**2 for i, r in pair_ratio ]).sqrt() / len(pair_ratio) mean_odd_ratio = sum([p for i, p in odd_ratio]) / len(odd_ratio) mean_odd_ratio_avg_square_error = sum([ (r - mean_odd_ratio)**2 for i, r in odd_ratio ]).sqrt() / len(odd_ratio) relative_pair_sq_err = mean_pair_ratio_avg_square_error / mean_pair_ratio relative_odd_sq_err = mean_odd_ratio_avg_square_error / mean_odd_ratio if relative_pair_sq_err > 0.5 or relative_odd_sq_err > 0.5: if all([ i[1] > 2 for i in (pair_ratio[3 * int(len(pair_ratio) / 4):] + odd_ratio[3 * int(len(odd_ratio) / 4):]) ]): approach_type = 'super_exp' else: approach_type = 'undefined' if (relative_odd_sq_err <= 0.5 and relative_pair_sq_err <= 0.5) or approach_type == 'super_exp': is_pair_exp = mean_pair_ratio > exponential_threshold is_odd_exp = mean_odd_ratio > exponential_threshold # in case one is exponential and the other isn't return 'mixed' if is_pair_exp ^ is_odd_exp: approach_type = 'mixed' elif is_pair_exp and is_odd_exp: if approach_type != 'super_exp': approach_type = 'exp' approach_parameter_pair = mean_pair_ratio**type( mean_pair_ratio)(0.5) approach_parameter_odd = mean_odd_ratio**type(mean_odd_ratio)( 0.5) approach_parameter = min(approach_parameter_pair, approach_parameter_odd) approach_coeff_pair_list = [ abs(delta_pair[i][1] * approach_parameter**(delta_pair[i][0]) / (1 - approach_parameter**(-2))) for i in range(0, len(delta_pair)) if delta_pair[i][1] != 0 and not isnan(delta_pair[i][1]) ] approach_coeff_pair = sum(approach_coeff_pair_list) / len( approach_coeff_pair_list) approach_coeff_odd_list = [ abs(delta_odd[i][1] * approach_parameter**(delta_odd[i][0]) / (1 - approach_parameter**(-2))) for i in range(0, len(delta_odd)) if delta_odd[i][1] != 0 and not isnan(delta_odd[i][1]) ] approach_coeff_odd = sum(approach_coeff_odd_list) / len( approach_coeff_odd_list) approach_coeff = min(approach_coeff_pair, approach_coeff_odd) approach_parameter = (approach_parameter, approach_coeff) else: approach_type = 'poly2sympoly' if approach_type != 'poly2sympoly' or not find_poly_parameter: return (approach_type, approach_parameter) # We're requested to find the poly2sympoly parameter log_x_pair = [math.log(i) for i, d in delta_pair] log_y_pair = [math.log(d) for i, d in delta_pair] slope_pair, intercept_pair, r_value_pair, p_value_pair, std_err_pair = scipy.stats.linregress( log_x_pair, log_y_pair) log_x_odd = [math.log(i) for i, d in delta_odd] log_y_odd = [math.log(d) for i, d in delta_odd] slope_odd, intercept_odd, r_value_odd, p_value_odd, std_err_odd = scipy.stats.linregress( log_x_odd, log_y_odd) # TODO: replace the -1 by *0.95? need to make sure first what is the slop (is it negative?) approach_parameter = (min(abs(slope_pair), abs(slope_odd)) - 1, min(intercept_pair, intercept_odd), min(r_value_pair**2, r_value_odd**2)) return (approach_type, approach_parameter)
def test_rationalfunc_canonalize(): rationalfunc_eval = lhs_evaluators.RationalFuncEvaluator( [[0, 3 * 1, 3 * 2, 1], [0, 1, 2], 0], dec(5)) rationalfunc_eval.canonalize_params() rationalfunc_eval.get_params() == ([0, 0, 1], [1, 2], 3)
import cont_fracs import pytest from mpmath import mpf as dec import gen_consts @pytest.fixture() def cf(): return cont_fracs.ContFrac([[1]], [[1]]) @pytest.mark.parametrize('a,b,num_of_iters,target_val,threshold', [ ([[2]], [[1, -4, 4]], 550, 4/gen_consts.gen_pi_const()+1, dec('8E-4')), ([[6]], [[1, -4, 4]], 500, gen_consts.gen_pi_const()+3, dec('1E-8')), ([[1, 2]], [[0, 0, 1]], 200, 4/gen_consts.gen_pi_const(), dec('1E-15')), ([[2, 1]], [[0, -1]], 400, gen_consts.gen_e_const()/(gen_consts.gen_e_const()-1), dec('1E-15')), ([[1]], [[1]], 400, gen_consts.gen_phi_const(), dec('1E-15')), ([[2], [0, 12]], [[-1, 6], [-5, 6]], 400, dec('2')**(1/dec('12'))+1, dec('1E-15')), ]) def test_contfrac(cf, a, b, num_of_iters, target_val, threshold): cf.reinitialize(a, b) cf.gen_iterations(num_of_iters) assert cf.compare_result(target_val) < threshold