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
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
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.
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...).