algebra_with_sympy.preparser

  1def algebra_with_sympy_preparser(lines):
  2    """
  3    In IPython compatible environments (Jupyter, IPython, etc...) this supports
  4    a special compact input method for equations.
  5
  6    The syntax supported is `equation_name =@ equation.lhs = equation.rhs`,
  7    where `equation_name` is a valid Python name that can be used to refer to
  8    the equation later. `equation.lhs` is the left-hand side of the equation
  9    and `equation.rhs` is the right-hand side of the equation. Each side of the
 10    equation must parse into a valid Sympy expression.
 11
 12    **Note**: This does not support line continuation. Long equations should be
 13    built by combining expressions using names short enough to do this on one
 14    line. The alternative is to use `equation_name = Eqn(long ...
 15    expressions ... with ... multiple ... lines)`.
 16
 17    **Note**: If the `equation_name` is omitted the equation will be formed,
 18    but it will not be assigned to a name that can be used to refer to it
 19    later. You may be able to access it through one of the special IPython
 20    underscore names. This is not recommended.
 21
 22    **THIS FUNCTION IS USED BY THE IPYTHON ENVIRONMENT TO PREPARSE THE INPUT
 23    BEFORE IT IS PASSED TO THE PYTHON INTERPRETER. IT IS NOT MEANT TO BE USED
 24    DIRECTLY BY A USER**
 25    """
 26    new_lines = []
 27    if isinstance(lines,str):
 28        lines = [lines]
 29    for k in lines:
 30        if '=@' in k:
 31            drop_comments = k.split('#')
 32            to_rephrase = ''
 33            if len(drop_comments) > 2:
 34                for i in range(len(drop_comments)-1):
 35                    to_rephrase += drop_comments[i]
 36            else:
 37                to_rephrase = drop_comments[0]
 38            linesplit = to_rephrase.split('=@')
 39            eqsplit = linesplit[1].split('=')
 40            if len(eqsplit)!=2:
 41                raise ValueError('The two sides of the equation must be' \
 42                                 ' separated by an \"=\" sign when using' \
 43                                 ' the \"=@\" special input method.')
 44            templine =''
 45            if eqsplit[0]!='' and eqsplit[1]!='':
 46                if eqsplit[1].endswith('\n'):
 47                    eqsplit[1] = eqsplit[1][:-1]
 48                if linesplit[0]!='':
 49                    templine = str(linesplit[0])+'= Eqn('+str(eqsplit[0])+',' \
 50                        ''+str(eqsplit[1])+')\n'
 51                else:
 52                    templine = 'Eqn('+str(eqsplit[0])+','+str(eqsplit[1])+')\n'
 53            new_lines.append(templine)
 54        else:
 55            new_lines.append(k)
 56    return new_lines
 57
 58
 59def toIntegerInSympyExpr(string):
 60    """ This function takes a string of valid Python and wraps integers within Sympy expressions
 61        in `sympy.Integer()` to make them Sympy integers rather than Python `Int()`. The
 62        advantage of this is that calculations with `Integer()` types can be exact. This function
 63        is careful not to wrap `Int()` types that are not part of Sympy expressions, making it
 64        possible for this functionality to exist with operations (e.g. array and numpy indexing)
 65        that are not compatible with the `Integer()` type.
 66    """
 67    from tokenize import generate_tokens, NEWLINE, OP, untokenize
 68    from io import StringIO
 69    ###
 70    # Internally used functions
 71    ###
 72    def isSympy(tokens, newSymObj):
 73        """ Checks list of tokens to see if it contains a Sympy Object
 74
 75        Parameters
 76        ==========
 77        tokens:list of tokens.
 78        newSymObj:list of string names of Sympy objects that have been declared
 79          in the current script/string being parsed.
 80        """
 81        from sympy import Basic
 82        from tokenize import NAME
 83        import __main__ as user_ns
 84        # print(dir(user_ns))
 85        sympy_obj = False
 86        for kind, string, start, end, line in tokens:
 87            if kind == NAME:
 88                # print('Checking: '+str(string))
 89                if hasattr(user_ns, string):
 90                    if isinstance(getattr(user_ns, string), Basic):
 91                        sympy_obj = True
 92                if string in newSymObj:
 93                    sympy_obj = True
 94        return sympy_obj
 95
 96    def toSympInteger(tokens):
 97        from tokenize import NUMBER, OP, NAME
 98        result = []
 99        for k in tokens:
100            if k[0] != NUMBER:
101                result.append((k[0], k[1]))
102            else:
103                if '.' in k[1] or 'j' in k[1].lower() or 'e' in k[1].lower():
104                    result.append((k[0], k[1]))
105                else:
106                    result.extend([
107                        (NAME, 'Integer'),
108                        (OP, '('),
109                        (NUMBER, k[1]),
110                        (OP, ')')
111                    ])
112        return result
113
114    def checkforSymObjDecl(token):
115        import re
116        from tokenize import NAME
117        syms = []
118        for kind, string, start, end, line in token:
119            if kind == NAME:
120                if string == 'var':
121                    match = re.search(r'\".*?\"|\'.*?\'', line)
122                    if (match):
123                        syms = match.group().replace('\"', '').replace('\'',
124                                                                   '').split(
125                        ' ')
126                if string == 'units':
127                    match = re.search(r'\".*?\"|\'.*?\'', line)
128                    if (match):
129                        syms = match.group().replace('\"', '').replace('\'',
130                                                                   '').split(
131                        ' ')
132                if string == 'symbols':
133                    parts = line.split('=')
134                    syms = parts[0].replace(' ', '').split(',')
135                if string == 'Symbol':
136                    parts = line.split('=')
137                    syms = parts[0].replace(' ', '').split(',')
138        return syms
139
140    ###
141    # The parsing and substitution.
142    ###
143    g = generate_tokens(StringIO(string).readline)
144    declaredSymObj = []
145    result = []
146    temptokens = []
147    openleft = 0
148    for k in g:
149        declaredSymObj.extend(checkforSymObjDecl([k]))
150        temptokens.append(k)
151        if k[0] == OP and k[1] == '(':
152            openleft += 1
153        if k[0] == OP and k[1] == ')':
154            openleft -= 1
155        if k[0] == NEWLINE and openleft == 0:
156            # This is where we check for sympy objects and replace int() with Integer()
157            hasSympyObj = isSympy(temptokens, declaredSymObj)
158            if hasSympyObj:
159                converted = toSympInteger(temptokens)
160                result.extend(converted)
161            else:
162                for j in temptokens:
163                    result.append((j[0],j[1]))
164            temptokens = []
165    return untokenize(result)
166
167def integers_as_exact(lines):
168    """This preparser uses `sympy.interactive.session.int_to_Integer` to
169    convert numbers without decimal points into sympy integers so that math
170    on them will be exact rather than defaulting to floating point. **This
171    should not be called directly by the user. It is plugged into the
172    IPython preparsing sequence when the feature is requested.** The default for
173    Algebra_with_sympy is to use this preparser. This can be turned on and
174    off using the Algebra_with_sympy functions:
175    * `set_integers_as_exact()`
176    * `unset_integers_as_exact()`
177    NOTE: This option does not work in plain vanilla Python sessions. You
178    must be running in an IPython environment (Jupyter, Notebook, Colab,
179    etc...).
180    """
181    #from sympy.interactive.session import int_to_Integer
182    string = ''
183    for k in lines:
184        string += k + '\n'
185    string = string[:-1] # remove the last '\n'
186    return toIntegerInSympyExpr(string)
187try:
188    from IPython import get_ipython
189    if get_ipython():
190        if hasattr(get_ipython(),'input_transformers_cleanup'):
191            get_ipython().input_transformers_post.\
192                append(algebra_with_sympy_preparser)
193        else:
194            import warnings
195            warnings.warn('Compact equation input unavailable.\nYou will have ' \
196                          'to use the form "eq1 = Eqn(lhs,rhs)" instead of ' \
197                          '"eq1=@lhs=rhs".\nIt appears you are running an ' \
198                          'outdated version of IPython.\nTo fix, update IPython ' \
199                          'using "pip install -U IPython".')
200except ModuleNotFoundError:
201    pass
def algebra_with_sympy_preparser(lines):
 2def algebra_with_sympy_preparser(lines):
 3    """
 4    In IPython compatible environments (Jupyter, IPython, etc...) this supports
 5    a special compact input method for equations.
 6
 7    The syntax supported is `equation_name =@ equation.lhs = equation.rhs`,
 8    where `equation_name` is a valid Python name that can be used to refer to
 9    the equation later. `equation.lhs` is the left-hand side of the equation
10    and `equation.rhs` is the right-hand side of the equation. Each side of the
11    equation must parse into a valid Sympy expression.
12
13    **Note**: This does not support line continuation. Long equations should be
14    built by combining expressions using names short enough to do this on one
15    line. The alternative is to use `equation_name = Eqn(long ...
16    expressions ... with ... multiple ... lines)`.
17
18    **Note**: If the `equation_name` is omitted the equation will be formed,
19    but it will not be assigned to a name that can be used to refer to it
20    later. You may be able to access it through one of the special IPython
21    underscore names. This is not recommended.
22
23    **THIS FUNCTION IS USED BY THE IPYTHON ENVIRONMENT TO PREPARSE THE INPUT
24    BEFORE IT IS PASSED TO THE PYTHON INTERPRETER. IT IS NOT MEANT TO BE USED
25    DIRECTLY BY A USER**
26    """
27    new_lines = []
28    if isinstance(lines,str):
29        lines = [lines]
30    for k in lines:
31        if '=@' in k:
32            drop_comments = k.split('#')
33            to_rephrase = ''
34            if len(drop_comments) > 2:
35                for i in range(len(drop_comments)-1):
36                    to_rephrase += drop_comments[i]
37            else:
38                to_rephrase = drop_comments[0]
39            linesplit = to_rephrase.split('=@')
40            eqsplit = linesplit[1].split('=')
41            if len(eqsplit)!=2:
42                raise ValueError('The two sides of the equation must be' \
43                                 ' separated by an \"=\" sign when using' \
44                                 ' the \"=@\" special input method.')
45            templine =''
46            if eqsplit[0]!='' and eqsplit[1]!='':
47                if eqsplit[1].endswith('\n'):
48                    eqsplit[1] = eqsplit[1][:-1]
49                if linesplit[0]!='':
50                    templine = str(linesplit[0])+'= Eqn('+str(eqsplit[0])+',' \
51                        ''+str(eqsplit[1])+')\n'
52                else:
53                    templine = 'Eqn('+str(eqsplit[0])+','+str(eqsplit[1])+')\n'
54            new_lines.append(templine)
55        else:
56            new_lines.append(k)
57    return new_lines

In IPython compatible environments (Jupyter, IPython, etc...) this supports a special compact input method for equations.

The syntax supported is equation_name =@ equation.lhs = equation.rhs, where equation_name is a valid Python name that can be used to refer to the equation later. equation.lhs is the left-hand side of the equation and equation.rhs is the right-hand side of the equation. Each side of the equation must parse into a valid Sympy expression.

Note: This does not support line continuation. Long equations should be built by combining expressions using names short enough to do this on one line. The alternative is to use equation_name = Eqn(long ... expressions ... with ... multiple ... lines).

Note: If the equation_name is omitted the equation will be formed, but it will not be assigned to a name that can be used to refer to it later. You may be able to access it through one of the special IPython underscore names. This is not recommended.

THIS FUNCTION IS USED BY THE IPYTHON ENVIRONMENT TO PREPARSE THE INPUT BEFORE IT IS PASSED TO THE PYTHON INTERPRETER. IT IS NOT MEANT TO BE USED DIRECTLY BY A USER

def toIntegerInSympyExpr(string):
 60def toIntegerInSympyExpr(string):
 61    """ This function takes a string of valid Python and wraps integers within Sympy expressions
 62        in `sympy.Integer()` to make them Sympy integers rather than Python `Int()`. The
 63        advantage of this is that calculations with `Integer()` types can be exact. This function
 64        is careful not to wrap `Int()` types that are not part of Sympy expressions, making it
 65        possible for this functionality to exist with operations (e.g. array and numpy indexing)
 66        that are not compatible with the `Integer()` type.
 67    """
 68    from tokenize import generate_tokens, NEWLINE, OP, untokenize
 69    from io import StringIO
 70    ###
 71    # Internally used functions
 72    ###
 73    def isSympy(tokens, newSymObj):
 74        """ Checks list of tokens to see if it contains a Sympy Object
 75
 76        Parameters
 77        ==========
 78        tokens:list of tokens.
 79        newSymObj:list of string names of Sympy objects that have been declared
 80          in the current script/string being parsed.
 81        """
 82        from sympy import Basic
 83        from tokenize import NAME
 84        import __main__ as user_ns
 85        # print(dir(user_ns))
 86        sympy_obj = False
 87        for kind, string, start, end, line in tokens:
 88            if kind == NAME:
 89                # print('Checking: '+str(string))
 90                if hasattr(user_ns, string):
 91                    if isinstance(getattr(user_ns, string), Basic):
 92                        sympy_obj = True
 93                if string in newSymObj:
 94                    sympy_obj = True
 95        return sympy_obj
 96
 97    def toSympInteger(tokens):
 98        from tokenize import NUMBER, OP, NAME
 99        result = []
100        for k in tokens:
101            if k[0] != NUMBER:
102                result.append((k[0], k[1]))
103            else:
104                if '.' in k[1] or 'j' in k[1].lower() or 'e' in k[1].lower():
105                    result.append((k[0], k[1]))
106                else:
107                    result.extend([
108                        (NAME, 'Integer'),
109                        (OP, '('),
110                        (NUMBER, k[1]),
111                        (OP, ')')
112                    ])
113        return result
114
115    def checkforSymObjDecl(token):
116        import re
117        from tokenize import NAME
118        syms = []
119        for kind, string, start, end, line in token:
120            if kind == NAME:
121                if string == 'var':
122                    match = re.search(r'\".*?\"|\'.*?\'', line)
123                    if (match):
124                        syms = match.group().replace('\"', '').replace('\'',
125                                                                   '').split(
126                        ' ')
127                if string == 'units':
128                    match = re.search(r'\".*?\"|\'.*?\'', line)
129                    if (match):
130                        syms = match.group().replace('\"', '').replace('\'',
131                                                                   '').split(
132                        ' ')
133                if string == 'symbols':
134                    parts = line.split('=')
135                    syms = parts[0].replace(' ', '').split(',')
136                if string == 'Symbol':
137                    parts = line.split('=')
138                    syms = parts[0].replace(' ', '').split(',')
139        return syms
140
141    ###
142    # The parsing and substitution.
143    ###
144    g = generate_tokens(StringIO(string).readline)
145    declaredSymObj = []
146    result = []
147    temptokens = []
148    openleft = 0
149    for k in g:
150        declaredSymObj.extend(checkforSymObjDecl([k]))
151        temptokens.append(k)
152        if k[0] == OP and k[1] == '(':
153            openleft += 1
154        if k[0] == OP and k[1] == ')':
155            openleft -= 1
156        if k[0] == NEWLINE and openleft == 0:
157            # This is where we check for sympy objects and replace int() with Integer()
158            hasSympyObj = isSympy(temptokens, declaredSymObj)
159            if hasSympyObj:
160                converted = toSympInteger(temptokens)
161                result.extend(converted)
162            else:
163                for j in temptokens:
164                    result.append((j[0],j[1]))
165            temptokens = []
166    return untokenize(result)

This function takes a string of valid Python and wraps integers within Sympy expressions in sympy.Integer() to make them Sympy integers rather than Python Int(). The advantage of this is that calculations with Integer() types can be exact. This function is careful not to wrap Int() types that are not part of Sympy expressions, making it possible for this functionality to exist with operations (e.g. array and numpy indexing) that are not compatible with the Integer() type.

def integers_as_exact(lines):
168def integers_as_exact(lines):
169    """This preparser uses `sympy.interactive.session.int_to_Integer` to
170    convert numbers without decimal points into sympy integers so that math
171    on them will be exact rather than defaulting to floating point. **This
172    should not be called directly by the user. It is plugged into the
173    IPython preparsing sequence when the feature is requested.** The default for
174    Algebra_with_sympy is to use this preparser. This can be turned on and
175    off using the Algebra_with_sympy functions:
176    * `set_integers_as_exact()`
177    * `unset_integers_as_exact()`
178    NOTE: This option does not work in plain vanilla Python sessions. You
179    must be running in an IPython environment (Jupyter, Notebook, Colab,
180    etc...).
181    """
182    #from sympy.interactive.session import int_to_Integer
183    string = ''
184    for k in lines:
185        string += k + '\n'
186    string = string[:-1] # remove the last '\n'
187    return toIntegerInSympyExpr(string)

This preparser uses sympy.interactive.session.int_to_Integer to convert numbers without decimal points into sympy integers so that math on them will be exact rather than defaulting to floating point. This should not be called directly by the user. It is plugged into the IPython preparsing sequence when the feature is requested. The default for Algebra_with_sympy is to use this preparser. This can be turned on and off using the Algebra_with_sympy functions:

  • set_integers_as_exact()
  • unset_integers_as_exact() NOTE: This option does not work in plain vanilla Python sessions. You must be running in an IPython environment (Jupyter, Notebook, Colab, etc...).