""" Parser for FullForm[Downvalues[]] of Mathematica rules. This parser is customised to parse the output in MatchPy rules format. Multiple `Constraints` are divided into individual `Constraints` because it helps the MatchPy's `ManyToOneReplacer` to backtrack earlier and improve the speed. Parsed output is formatted into readable format by using `sympify` and print the expression using `sstr`. This replaces `And`, `Mul`, 'Pow' by their respective symbols. Mathematica =========== To get the full form from Wolfram Mathematica, type: ``` ShowSteps = False Import["RubiLoader.m"] Export["output.txt", ToString@FullForm@DownValues@Int] ``` The file ``output.txt`` will then contain the rules in parseable format. References ========== [1] http://reference.wolfram.com/language/ref/FullForm.html [2] http://reference.wolfram.com/language/ref/DownValues.html [3] https://gist.github.com/Upabjojr/bc07c49262944f9c1eb0 """ import re import os import inspect from sympy.core.function import Function from sympy.core.symbol import Symbol from sympy.core.sympify import sympify from sympy.sets.sets import Set from sympy.printing import StrPrinter from sympy.utilities.misc import debug class RubiStrPrinter(StrPrinter): def _print_Not(self, expr): return "Not(%s)" % self._print(expr.args[0]) def rubi_printer(expr, **settings): return RubiStrPrinter(settings).doprint(expr) replacements = dict( # Mathematica equivalent functions in SymPy Times="Mul", Plus="Add", Power="Pow", Log='log', Exp='exp', Sqrt='sqrt', Cos='cos', Sin='sin', Tan='tan', Cot='1/tan', cot='1/tan', Sec='1/cos', sec='1/cos', Csc='1/sin', csc='1/sin', ArcSin='asin', ArcCos='acos', # ArcTan='atan', ArcCot='acot', ArcSec='asec', ArcCsc='acsc', Sinh='sinh', Cosh='cosh', Tanh='tanh', Coth='1/tanh', coth='1/tanh', Sech='1/cosh', sech='1/cosh', Csch='1/sinh', csch='1/sinh', ArcSinh='asinh', ArcCosh='acosh', ArcTanh='atanh', ArcCoth='acoth', ArcSech='asech', ArcCsch='acsch', Expand='expand', Im='im', Re='re', Flatten='flatten', Polylog='polylog', Cancel='cancel', #Gamma='gamma', TrigExpand='expand_trig', Sign='sign', Simplify='simplify', Defer='UnevaluatedExpr', Identity = 'S', Sum = 'Sum_doit', Module = 'With', Block = 'With', Null = 'None' ) temporary_variable_replacement = { # Temporarily rename because it can raise errors while sympifying 'gcd' : "_gcd", 'jn' : "_jn", } permanent_variable_replacement = { # Permamenely rename these variables r"\[ImaginaryI]" : 'ImaginaryI', "$UseGamma": '_UseGamma', } # These functions have different return type in different cases. So better to use a try and except in the constraints, when any of these appear f_diff_return_type = ['BinomialParts', 'BinomialDegree', 'TrinomialParts', 'GeneralizedBinomialParts', 'GeneralizedTrinomialParts', 'PseudoBinomialParts', 'PerfectPowerTest', 'SquareFreeFactorTest', 'SubstForFractionalPowerOfQuotientOfLinears', 'FractionalPowerOfQuotientOfLinears', 'InverseFunctionOfQuotientOfLinears', 'FractionalPowerOfSquareQ', 'FunctionOfLinear', 'FunctionOfInverseLinear', 'FunctionOfTrig', 'FindTrigFactor', 'FunctionOfLog', 'PowerVariableExpn', 'FunctionOfSquareRootOfQuadratic', 'SubstForFractionalPowerOfLinear', 'FractionalPowerOfLinear', 'InverseFunctionOfLinear', 'Divides', 'DerivativeDivides', 'TrigSquare', 'SplitProduct', 'SubstForFractionalPowerOfQuotientOfLinears', 'InverseFunctionOfQuotientOfLinears', 'FunctionOfHyperbolic', 'SplitSum'] def contains_diff_return_type(a): """ This function returns whether an expression contains functions which have different return types in diiferent cases. """ if isinstance(a, list): for i in a: if contains_diff_return_type(i): return True elif type(a) == Function('With') or type(a) == Function('Module'): for i in f_diff_return_type: if a.has(Function(i)): return True else: if a in f_diff_return_type: return True return False def parse_full_form(wmexpr): """ Parses FullForm[Downvalues[]] generated by Mathematica """ out = [] stack = [out] generator = re.finditer(r'[\[\],]', wmexpr) last_pos = 0 for match in generator: if match is None: break position = match.start() last_expr = wmexpr[last_pos:position].replace(',', '').replace(']', '').replace('[', '').strip() if match.group() == ',': if last_expr != '': stack[-1].append(last_expr) elif match.group() == ']': if last_expr != '': stack[-1].append(last_expr) stack.pop() elif match.group() == '[': stack[-1].append([last_expr]) stack.append(stack[-1][-1]) last_pos = match.end() return out[0] def get_default_values(parsed, default_values={}): """ Returns Optional variables and their values in the pattern """ if not isinstance(parsed, list): return default_values if parsed[0] == "Times": # find Default arguments for "Times" for i in parsed[1:]: if i[0] == "Optional": default_values[(i[1][1])] = 1 if parsed[0] == "Plus": # find Default arguments for "Plus" for i in parsed[1:]: if i[0] == "Optional": default_values[(i[1][1])] = 0 if parsed[0] == "Power": # find Default arguments for "Power" for i in parsed[1:]: if i[0] == "Optional": default_values[(i[1][1])] = 1 if len(parsed) == 1: return default_values for i in parsed: default_values = get_default_values(i, default_values) return default_values def add_wildcards(string, optional={}): """ Replaces `Pattern(variable)` by `variable` in `string`. Returns the free symbols present in the string. """ symbols = [] # stores symbols present in the expression p = r'(Optional\(Pattern\((\w+), Blank\)\))' matches = re.findall(p, string) for i in matches: string = string.replace(i[0], "WC('{}', S({}))".format(i[1], optional[i[1]])) symbols.append(i[1]) p = r'(Pattern\((\w+), Blank\))' matches = re.findall(p, string) for i in matches: string = string.replace(i[0], i[1] + '_') symbols.append(i[1]) p = r'(Pattern\((\w+), Blank\(Symbol\)\))' matches = re.findall(p, string) for i in matches: string = string.replace(i[0], i[1] + '_') symbols.append(i[1]) return string, symbols def seperate_freeq(s, variables=[], x=None): """ Returns list of symbols in FreeQ. """ if s[0] == 'FreeQ': if len(s[1]) == 1: variables = [s[1]] else: variables = s[1][1:] x = s[2] else: for i in s[1:]: variables, x = seperate_freeq(i, variables, x) return variables, x return variables, x def parse_freeq(l, x, cons_index, cons_dict, cons_import, symbols=None): """ Converts FreeQ constraints into MatchPy constraint """ res = [] cons = '' for i in l: if isinstance(i, str): r = ' return FreeQ({}, {})'.format(i, x) # First it checks if a constraint is already present in `cons_dict`, If yes, use it else create a new one. if r not in cons_dict.values(): cons_index += 1 c = '\n def cons_f{}({}, {}):\n'.format(cons_index, i, x) c += r c += '\n\n cons{} = CustomConstraint({})\n'.format(cons_index, 'cons_f{}'.format(cons_index)) cons_name = 'cons{}'.format(cons_index) cons_dict[cons_name] = r else: c = '' cons_name = next(key for key, value in sorted(cons_dict.items()) if value == r) elif isinstance(i, list): s = sorted(set(get_free_symbols(i, symbols))) s = ', '.join(s) r = ' return FreeQ({}, {})'.format(generate_sympy_from_parsed(i), x) if r not in cons_dict.values(): cons_index += 1 c = '\n def cons_f{}({}):\n'.format(cons_index, s) c += r c += '\n\n cons{} = CustomConstraint({})\n'.format(cons_index, 'cons_f{}'.format(cons_index)) cons_name = 'cons{}'.format(cons_index) cons_dict[cons_name] = r else: c = '' cons_name = next(key for key, value in cons_dict.items() if value == r) if cons_name not in cons_import: cons_import.append(cons_name) res.append(cons_name) cons += c if res != []: return ', ' + ', '.join(res), cons, cons_index return '', cons, cons_index def generate_sympy_from_parsed(parsed, wild=False, symbols=(), replace_Int=False): """ Parses list into Python syntax. Parameters ========== wild : When set to True, the symbols are replaced as wild symbols. symbols : Symbols already present in the pattern. replace_Int: when set to True, `Int` is replaced by `Integral`(used to parse pattern). """ out = "" if not isinstance(parsed, list): try: # return S(number) if parsed is Number float(parsed) return "S({})".format(parsed) except: pass if parsed in symbols: if wild: return parsed + '_' return parsed if parsed[0] == 'Rational': return 'S({})/S({})'.format(generate_sympy_from_parsed(parsed[1], wild=wild, symbols=symbols, replace_Int=replace_Int), generate_sympy_from_parsed(parsed[2], wild=wild, symbols=symbols, replace_Int=replace_Int)) if parsed[0] in replacements: out += replacements[parsed[0]] elif parsed[0] == 'Int' and replace_Int: out += 'Integral' else: out += parsed[0] if len(parsed) == 1: return out result = [generate_sympy_from_parsed(i, wild=wild, symbols=symbols, replace_Int=replace_Int) for i in parsed[1:]] if '' in result: result.remove('') out += "(" out += ", ".join(result) out += ")" return out def get_free_symbols(s, symbols, free_symbols=None): """ Returns free_symbols present in `s`. """ free_symbols = free_symbols or [] if not isinstance(s, list): if s in symbols: free_symbols.append(s) return free_symbols for i in s: free_symbols = get_free_symbols(i, symbols, free_symbols) return free_symbols def set_matchq_in_constraint(a, cons_index): """ Takes care of the case, when a pattern matching has to be done inside a constraint. """ lst = [] res = '' if isinstance(a, list): if a[0] == 'MatchQ': s = a optional = get_default_values(s, {}) r = generate_sympy_from_parsed(s, replace_Int=True) r, free_symbols = add_wildcards(r, optional=optional) free_symbols = sorted(set(free_symbols)) # remove common symbols r = sympify(r, locals={"Or": Function("Or"), "And": Function("And"), "Not":Function("Not")}) pattern = r.args[1].args[0] cons = r.args[1].args[1] pattern = rubi_printer(pattern, sympy_integers=True) pattern = setWC(pattern) res = ' def _cons_f_{}({}):\n return {}\n'.format(cons_index, ', '.join(free_symbols), cons) res += ' _cons_{} = CustomConstraint(_cons_f_{})\n'.format(cons_index, cons_index) res += ' pat = Pattern(UtilityOperator({}, x), _cons_{})\n'.format(pattern, cons_index) res += ' result_matchq = is_match(UtilityOperator({}, x), pat)'.format(r.args[0]) return "result_matchq", res else: for i in a: if isinstance(i, list): r = set_matchq_in_constraint(i, cons_index) lst.append(r[0]) res = r[1] else: lst.append(i) return lst, res def _divide_constriant(s, symbols, cons_index, cons_dict, cons_import): # Creates a CustomConstraint of the form `CustomConstraint(lambda a, x: FreeQ(a, x))` lambda_symbols = sorted(set(get_free_symbols(s, symbols, []))) r = generate_sympy_from_parsed(s) r = sympify(r, locals={"Or": Function("Or"), "And": Function("And"), "Not":Function("Not")}) if r.has(Function('MatchQ')): match_res = set_matchq_in_constraint(s, cons_index) res = match_res[1] res += '\n return {}'.format(rubi_printer(sympify(generate_sympy_from_parsed(match_res[0]), locals={"Or": Function("Or"), "And": Function("And"), "Not":Function("Not")}), sympy_integers = True)) elif contains_diff_return_type(s): res = ' try:\n return {}\n except (TypeError, AttributeError):\n return False'.format(rubi_printer(r, sympy_integers=True)) else: res = ' return {}'.format(rubi_printer(r, sympy_integers=True)) # First it checks if a constraint is already present in `cons_dict`, If yes, use it else create a new one. if res not in cons_dict.values(): cons_index += 1 cons = '\n def cons_f{}({}):\n'.format(cons_index, ', '.join(lambda_symbols)) if 'x' in lambda_symbols: cons += ' if isinstance(x, (int, Integer, float, Float)):\n return False\n' cons += res cons += '\n\n cons{} = CustomConstraint({})\n'.format(cons_index, 'cons_f{}'.format(cons_index)) cons_name = 'cons{}'.format(cons_index) cons_dict[cons_name] = res else: cons = '' cons_name = next(key for key, value in cons_dict.items() if value == res) if cons_name not in cons_import: cons_import.append(cons_name) return cons_name, cons, cons_index def divide_constraint(s, symbols, cons_index, cons_dict, cons_import): """ Divides multiple constraints into smaller constraints. Parameters ========== s : constraint as list symbols : all the symbols present in the expression """ result =[] cons = '' if s[0] == 'And': for i in s[1:]: if i[0]!= 'FreeQ': a = _divide_constriant(i, symbols, cons_index, cons_dict, cons_import) result.append(a[0]) cons += a[1] cons_index = a[2] else: a = _divide_constriant(s, symbols, cons_index, cons_dict, cons_import) result.append(a[0]) cons += a[1] cons_index = a[2] r = [''] for i in result: if i != '': r.append(i) return ', '.join(r),cons, cons_index def setWC(string): """ Replaces `WC(a, b)` by `WC('a', S(b))` """ p = r'(WC\((\w+), S\(([-+]?\d)\)\))' matches = re.findall(p, string) for i in matches: string = string.replace(i[0], "WC('{}', S({}))".format(i[1], i[2])) return string def process_return_type(a1, L): """ Functions like `Set`, `With` and `CompoundExpression` has to be taken special care. """ a = sympify(a1[1]) x = '' processed = False return_value = '' if type(a) == Function('With') or type(a) == Function('Module'): for i in a.args: for s in i.args: if isinstance(s, Set) and s not in L: x += '\n {} = {}'.format(s.args[0], rubi_printer(s.args[1], sympy_integers=True)) if not type(i) in (Function('List'), Function('CompoundExpression')) and not i.has(Function('CompoundExpression')): return_value = i processed = True elif type(i) == Function('CompoundExpression'): return_value = i.args[-1] processed = True elif type(i.args[0]) == Function('CompoundExpression'): C = i.args[0] return_value = '{}({}, {})'.format(i.func, C.args[-1], i.args[1]) processed = True return x, return_value, processed def extract_set(s, L): """ this function extracts all `Set` functions """ lst = [] if isinstance(s, Set) and s not in L: lst.append(s) else: try: for i in s.args: lst += extract_set(i, L) except: # when s has no attribute args (like `bool`) pass return lst def replaceWith(s, symbols, index): """ Replaces `With` and `Module by python functions` """ return_type = None with_value = '' if type(s) == Function('With') or type(s) == Function('Module'): constraints = ' ' result = '\n\n\ndef With{}({}):'.format(index, ', '.join(symbols)) if type(s.args[0]) == Function('List'): # get all local variables of With and Module L = list(s.args[0].args) else: L = [s.args[0]] lst = [] for i in s.args[1:]: lst += extract_set(i, L) L += lst for i in L: # define local variables if isinstance(i, Set): with_value += '\n {} = {}'.format(i.args[0], rubi_printer(i.args[1], sympy_integers=True)) elif isinstance(i, Symbol): with_value += "\n {} = Symbol('{}')".format(i, i) #result += with_value if type(s.args[1]) == Function('CompoundExpression'): # Expand CompoundExpression C = s.args[1] result += with_value if isinstance(C.args[0], Set): result += '\n {} = {}'.format(C.args[0].args[0], C.args[0].args[1]) result += '\n return {}'.format(rubi_printer(C.args[1], sympy_integers=True)) return result, constraints, return_type elif type(s.args[1]) == Function('Condition'): C = s.args[1] if len(C.args) == 2: if all(j in symbols for j in [str(i) for i in C.free_symbols]): result += with_value #constraints += 'CustomConstraint(lambda {}: {})'.format(', '.join([str(i) for i in C.free_symbols]), sstr(C.args[1], sympy_integers=True)) result += '\n return {}'.format(rubi_printer(C.args[0], sympy_integers=True)) else: if 'x' in symbols: result += '\n if isinstance(x, (int, Integer, float, Float)):\n return False' if contains_diff_return_type(s): n_with_value = with_value.replace('\n', '\n ') result += '\n try:{}\n res = {}'.format(n_with_value, rubi_printer(C.args[1], sympy_integers=True)) result += '\n except (TypeError, AttributeError):\n return False' result += '\n if res:' else: result+=with_value result += '\n if {}:'.format(rubi_printer(C.args[1], sympy_integers=True)) return_type = (with_value, rubi_printer(C.args[0], sympy_integers=True)) return_type1 = process_return_type(return_type, L) if return_type1[2]: return_type = (with_value+return_type1[0], rubi_printer(return_type1[1])) result += '\n return True' result += '\n return False' constraints = ', CustomConstraint(With{})'.format(index) return result, constraints, return_type elif type(s.args[1]) == Function('Module') or type(s.args[1]) == Function('With'): C = s.args[1] result += with_value return_type = (with_value, rubi_printer(C, sympy_integers=True)) return_type1 = process_return_type(return_type, L) if return_type1[2]: return_type = (with_value+return_type1[0], rubi_printer(return_type1[1])) result += return_type1[0] result += '\n return {}'.format(rubi_printer(return_type1[1])) return result, constraints, None elif s.args[1].has(Function("CompoundExpression")): C = s.args[1].args[0] result += with_value if isinstance(C.args[0], Set): result += '\n {} = {}'.format(C.args[0].args[0], C.args[0].args[1]) result += '\n return {}({}, {})'.format(s.args[1].func, C.args[-1], s.args[1].args[1]) return result, constraints, None result += with_value result += '\n return {}'.format(rubi_printer(s.args[1], sympy_integers=True)) return result, constraints, return_type else: return rubi_printer(s, sympy_integers=True), '', return_type def downvalues_rules(r, header, cons_dict, cons_index, index): """ Function which generates parsed rules by substituting all possible combinations of default values. """ rules = '[' parsed = '\n\n' repl_funcs = '\n\n' cons = '' cons_import = [] # it contains name of constraints that need to be imported for rules. for i in r: debug('parsing rule {}'.format(r.index(i) + 1)) # Parse Pattern if i[1][1][0] == 'Condition': p = i[1][1][1].copy() else: p = i[1][1].copy() optional = get_default_values(p, {}) pattern = generate_sympy_from_parsed(p.copy(), replace_Int=True) pattern, free_symbols = add_wildcards(pattern, optional=optional) free_symbols = sorted(set(free_symbols)) #remove common symbols # Parse Transformed Expression and Constraints if i[2][0] == 'Condition': # parse rules without constraints separately constriant, constraint_def, cons_index = divide_constraint(i[2][2], free_symbols, cons_index, cons_dict, cons_import) # separate And constraints into individual constraints FreeQ_vars, FreeQ_x = seperate_freeq(i[2][2].copy()) # separate FreeQ into individual constraints transformed = generate_sympy_from_parsed(i[2][1].copy(), symbols=free_symbols) else: constriant = '' constraint_def = '' FreeQ_vars, FreeQ_x = [], [] transformed = generate_sympy_from_parsed(i[2].copy(), symbols=free_symbols) FreeQ_constraint, free_cons_def, cons_index = parse_freeq(FreeQ_vars, FreeQ_x, cons_index, cons_dict, cons_import, free_symbols) pattern = sympify(pattern, locals={"Or": Function("Or"), "And": Function("And"), "Not":Function("Not") }) pattern = rubi_printer(pattern, sympy_integers=True) pattern = setWC(pattern) transformed = sympify(transformed, locals={"Or": Function("Or"), "And": Function("And"), "Not":Function("Not") }) constraint_def = constraint_def + free_cons_def cons += constraint_def index += 1 # below are certain if - else condition depending on various situation that may be encountered if type(transformed) == Function('With') or type(transformed) == Function('Module'): # define separate function when With appears transformed, With_constraints, return_type = replaceWith(transformed, free_symbols, index) if return_type is None: repl_funcs += '{}'.format(transformed) parsed += '\n pattern' + str(index) + ' = Pattern(' + pattern + '' + FreeQ_constraint + '' + constriant + ')' parsed += '\n ' + 'rule' + str(index) + ' = ReplacementRule(' + 'pattern' + rubi_printer(index, sympy_integers=True) + ', With{}'.format(index) + ')\n' else: repl_funcs += '{}'.format(transformed) parsed += '\n pattern' + str(index) + ' = Pattern(' + pattern + '' + FreeQ_constraint + '' + constriant + With_constraints + ')' repl_funcs += '\n\n\ndef replacement{}({}):\n'.format( index, ', '.join(free_symbols) ) + return_type[0] + '\n return '.format(index) + return_type[1] parsed += '\n ' + 'rule' + str(index) + ' = ReplacementRule(' + 'pattern' + rubi_printer(index, sympy_integers=True) + ', replacement{}'.format(index) + ')\n' else: transformed = rubi_printer(transformed, sympy_integers=True) parsed += '\n pattern' + str(index) + ' = Pattern(' + pattern + '' + FreeQ_constraint + '' + constriant + ')' repl_funcs += '\n\n\ndef replacement{}({}):\n return '.format(index, ', '.join(free_symbols), index) + transformed parsed += '\n ' + 'rule' + str(index) + ' = ReplacementRule(' + 'pattern' + rubi_printer(index, sympy_integers=True) + ', replacement{}'.format(index) + ')\n' rules += 'rule{}, '.format(index) rules += ']' parsed += ' return ' + rules +'\n' header += ' from sympy.integrals.rubi.constraints import ' + ', '.join(word for word in cons_import) parsed = header + parsed + repl_funcs return parsed, cons_index, cons, index def rubi_rule_parser(fullform, header=None, module_name='rubi_object'): """ Parses rules in MatchPy format. Parameters ========== fullform : FullForm of the rule as string. header : Header imports for the file. Uses default imports if None. module_name : name of RUBI module References ========== [1] http://reference.wolfram.com/language/ref/FullForm.html [2] http://reference.wolfram.com/language/ref/DownValues.html [3] https://gist.github.com/Upabjojr/bc07c49262944f9c1eb0 """ if header is None: # use default header values path_header = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) header = open(os.path.join(path_header, "header.py.txt")).read() header = header.format(module_name) cons_dict = {} # dict keeps track of constraints that has been encountered, thus avoids repetition of constraints. cons_index = 0 # for index of a constraint index = 0 # indicates the number of a rule. cons = '' # Temporarily rename these variables because it # can raise errors while sympifying for i in temporary_variable_replacement: fullform = fullform.replace(i, temporary_variable_replacement[i]) # Permanently rename these variables for i in permanent_variable_replacement: fullform = fullform.replace(i, permanent_variable_replacement[i]) rules = [] for i in parse_full_form(fullform): # separate all rules if i[0] == 'RuleDelayed': rules.append(i) parsed = downvalues_rules(rules, header, cons_dict, cons_index, index) result = parsed[0].strip() + '\n' cons += parsed[2] # Replace temporary variables by actual values for i in temporary_variable_replacement: cons = cons.replace(temporary_variable_replacement[i], i) result = result.replace(temporary_variable_replacement[i], i) cons = "\n".join(header.split("\n")[:-2]) + '\n' + cons return result, cons