|
|
from typing import Any, Dict as tDict, Tuple as tTuple
from itertools import product import re from sympy.core.sympify import sympify
def mathematica(s, additional_translations=None): '''
Users can add their own translation dictionary. variable-length argument needs '*' character.
Examples ========
>>> from sympy.parsing.mathematica import mathematica >>> mathematica('Log3[9]', {'Log3[x]':'log(x,3)'}) 2 >>> mathematica('F[7,5,3]', {'F[*x]':'Max(*x)*Min(*x)'}) 21
'''
parser = MathematicaParser(additional_translations) return sympify(parser.parse(s))
def _deco(cls): cls._initialize_class() return cls
@_deco class MathematicaParser: '''An instance of this class converts a string of a basic Mathematica
expression to SymPy style. Output is string type.'''
# left: Mathematica, right: SymPy CORRESPONDENCES = { 'Sqrt[x]': 'sqrt(x)', 'Exp[x]': 'exp(x)', 'Log[x]': 'log(x)', 'Log[x,y]': 'log(y,x)', 'Log2[x]': 'log(x,2)', 'Log10[x]': 'log(x,10)', 'Mod[x,y]': 'Mod(x,y)', 'Max[*x]': 'Max(*x)', 'Min[*x]': 'Min(*x)', 'Pochhammer[x,y]':'rf(x,y)', 'ArcTan[x,y]':'atan2(y,x)', 'ExpIntegralEi[x]': 'Ei(x)', 'SinIntegral[x]': 'Si(x)', 'CosIntegral[x]': 'Ci(x)', 'AiryAi[x]': 'airyai(x)', 'AiryAiPrime[x]': 'airyaiprime(x)', 'AiryBi[x]' :'airybi(x)', 'AiryBiPrime[x]' :'airybiprime(x)', 'LogIntegral[x]':' li(x)', 'PrimePi[x]': 'primepi(x)', 'Prime[x]': 'prime(x)', 'PrimeQ[x]': 'isprime(x)' }
# trigonometric, e.t.c. for arc, tri, h in product(('', 'Arc'), ( 'Sin', 'Cos', 'Tan', 'Cot', 'Sec', 'Csc'), ('', 'h')): fm = arc + tri + h + '[x]' if arc: # arc func fs = 'a' + tri.lower() + h + '(x)' else: # non-arc func fs = tri.lower() + h + '(x)' CORRESPONDENCES.update({fm: fs})
REPLACEMENTS = { ' ': '', '^': '**', '{': '[', '}': ']', }
RULES = { # a single whitespace to '*' 'whitespace': ( re.compile(r'''
(?<=[a-zA-Z\d]) # a letter or a number \ # a whitespace (?=[a-zA-Z\d]) # a letter or a number ''', re.VERBOSE),
'*'),
# add omitted '*' character 'add*_1': ( re.compile(r'''
(?<=[])\d]) # ], ) or a number # '' (?=[(a-zA-Z]) # ( or a single letter ''', re.VERBOSE),
'*'),
# add omitted '*' character (variable letter preceding) 'add*_2': ( re.compile(r'''
(?<=[a-zA-Z]) # a letter \( # ( as a character (?=.) # any characters ''', re.VERBOSE),
'*('),
# convert 'Pi' to 'pi' 'Pi': ( re.compile(r'''
(?: \A|(?<=[^a-zA-Z]) ) Pi # 'Pi' is 3.14159... in Mathematica (?=[^a-zA-Z]) ''', re.VERBOSE),
'pi'), }
# Mathematica function name pattern FM_PATTERN = re.compile(r'''
(?: \A|(?<=[^a-zA-Z]) # at the top or a non-letter ) [A-Z][a-zA-Z\d]* # Function (?=\[) # [ as a character ''', re.VERBOSE)
# list or matrix pattern (for future usage) ARG_MTRX_PATTERN = re.compile(r'''
\{.*\} ''', re.VERBOSE)
# regex string for function argument pattern ARGS_PATTERN_TEMPLATE = r'''
(?: \A|(?<=[^a-zA-Z]) ) {arguments} # model argument like x, y,... (?=[^a-zA-Z]) '''
# will contain transformed CORRESPONDENCES dictionary TRANSLATIONS = {} # type: tDict[tTuple[str, int], tDict[str, Any]]
# cache for a raw users' translation dictionary cache_original = {} # type: tDict[tTuple[str, int], tDict[str, Any]]
# cache for a compiled users' translation dictionary cache_compiled = {} # type: tDict[tTuple[str, int], tDict[str, Any]]
@classmethod def _initialize_class(cls): # get a transformed CORRESPONDENCES dictionary d = cls._compile_dictionary(cls.CORRESPONDENCES) cls.TRANSLATIONS.update(d)
def __init__(self, additional_translations=None): self.translations = {}
# update with TRANSLATIONS (class constant) self.translations.update(self.TRANSLATIONS)
if additional_translations is None: additional_translations = {}
# check the latest added translations if self.__class__.cache_original != additional_translations: if not isinstance(additional_translations, dict): raise ValueError('The argument must be dict type')
# get a transformed additional_translations dictionary d = self._compile_dictionary(additional_translations)
# update cache self.__class__.cache_original = additional_translations self.__class__.cache_compiled = d
# merge user's own translations self.translations.update(self.__class__.cache_compiled)
@classmethod def _compile_dictionary(cls, dic): # for return d = {}
for fm, fs in dic.items(): # check function form cls._check_input(fm) cls._check_input(fs)
# uncover '*' hiding behind a whitespace fm = cls._apply_rules(fm, 'whitespace') fs = cls._apply_rules(fs, 'whitespace')
# remove whitespace(s) fm = cls._replace(fm, ' ') fs = cls._replace(fs, ' ')
# search Mathematica function name m = cls.FM_PATTERN.search(fm)
# if no-hit if m is None: err = "'{f}' function form is invalid.".format(f=fm) raise ValueError(err)
# get Mathematica function name like 'Log' fm_name = m.group()
# get arguments of Mathematica function args, end = cls._get_args(m)
# function side check. (e.g.) '2*Func[x]' is invalid. if m.start() != 0 or end != len(fm): err = "'{f}' function form is invalid.".format(f=fm) raise ValueError(err)
# check the last argument's 1st character if args[-1][0] == '*': key_arg = '*' else: key_arg = len(args)
key = (fm_name, key_arg)
# convert '*x' to '\\*x' for regex re_args = [x if x[0] != '*' else '\\' + x for x in args]
# for regex. Example: (?:(x|y|z)) xyz = '(?:(' + '|'.join(re_args) + '))'
# string for regex compile patStr = cls.ARGS_PATTERN_TEMPLATE.format(arguments=xyz)
pat = re.compile(patStr, re.VERBOSE)
# update dictionary d[key] = {} d[key]['fs'] = fs # SymPy function template d[key]['args'] = args # args are ['x', 'y'] for example d[key]['pat'] = pat
return d
def _convert_function(self, s): '''Parse Mathematica function to SymPy one'''
# compiled regex object pat = self.FM_PATTERN
scanned = '' # converted string cur = 0 # position cursor while True: m = pat.search(s)
if m is None: # append the rest of string scanned += s break
# get Mathematica function name fm = m.group()
# get arguments, and the end position of fm function args, end = self._get_args(m)
# the start position of fm function bgn = m.start()
# convert Mathematica function to SymPy one s = self._convert_one_function(s, fm, args, bgn, end)
# update cursor cur = bgn
# append converted part scanned += s[:cur]
# shrink s s = s[cur:]
return scanned
def _convert_one_function(self, s, fm, args, bgn, end): # no variable-length argument if (fm, len(args)) in self.translations: key = (fm, len(args))
# x, y,... model arguments x_args = self.translations[key]['args']
# make CORRESPONDENCES between model arguments and actual ones d = {k: v for k, v in zip(x_args, args)}
# with variable-length argument elif (fm, '*') in self.translations: key = (fm, '*')
# x, y,..*args (model arguments) x_args = self.translations[key]['args']
# make CORRESPONDENCES between model arguments and actual ones d = {} for i, x in enumerate(x_args): if x[0] == '*': d[x] = ','.join(args[i:]) break d[x] = args[i]
# out of self.translations else: err = "'{f}' is out of the whitelist.".format(f=fm) raise ValueError(err)
# template string of converted function template = self.translations[key]['fs']
# regex pattern for x_args pat = self.translations[key]['pat']
scanned = '' cur = 0 while True: m = pat.search(template)
if m is None: scanned += template break
# get model argument x = m.group()
# get a start position of the model argument xbgn = m.start()
# add the corresponding actual argument scanned += template[:xbgn] + d[x]
# update cursor to the end of the model argument cur = m.end()
# shrink template template = template[cur:]
# update to swapped string s = s[:bgn] + scanned + s[end:]
return s
@classmethod def _get_args(cls, m): '''Get arguments of a Mathematica function'''
s = m.string # whole string anc = m.end() + 1 # pointing the first letter of arguments square, curly = [], [] # stack for brakets args = []
# current cursor cur = anc for i, c in enumerate(s[anc:], anc): # extract one argument if c == ',' and (not square) and (not curly): args.append(s[cur:i]) # add an argument cur = i + 1 # move cursor
# handle list or matrix (for future usage) if c == '{': curly.append(c) elif c == '}': curly.pop()
# seek corresponding ']' with skipping irrevant ones if c == '[': square.append(c) elif c == ']': if square: square.pop() else: # empty stack args.append(s[cur:i]) break
# the next position to ']' bracket (the function end) func_end = i + 1
return args, func_end
@classmethod def _replace(cls, s, bef): aft = cls.REPLACEMENTS[bef] s = s.replace(bef, aft) return s
@classmethod def _apply_rules(cls, s, bef): pat, aft = cls.RULES[bef] return pat.sub(aft, s)
@classmethod def _check_input(cls, s): for bracket in (('[', ']'), ('{', '}'), ('(', ')')): if s.count(bracket[0]) != s.count(bracket[1]): err = "'{f}' function form is invalid.".format(f=s) raise ValueError(err)
if '{' in s: err = "Currently list is not supported." raise ValueError(err)
def parse(self, s): # input check self._check_input(s)
# uncover '*' hiding behind a whitespace s = self._apply_rules(s, 'whitespace')
# remove whitespace(s) s = self._replace(s, ' ')
# add omitted '*' character s = self._apply_rules(s, 'add*_1') s = self._apply_rules(s, 'add*_2')
# translate function s = self._convert_function(s)
# '^' to '**' s = self._replace(s, '^')
# 'Pi' to 'pi' s = self._apply_rules(s, 'Pi')
# '{', '}' to '[', ']', respectively # s = cls._replace(s, '{') # currently list is not taken into account # s = cls._replace(s, '}')
return s
|