rgwfuncs 0.0.21__py3-none-any.whl → 0.0.54__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- rgwfuncs/__init__.py +5 -2
- rgwfuncs/algebra_lib.py +901 -0
- rgwfuncs/df_lib.py +111 -61
- rgwfuncs/docs_lib.py +51 -0
- rgwfuncs/interactive_shell_lib.py +32 -0
- rgwfuncs/str_lib.py +8 -44
- {rgwfuncs-0.0.21.dist-info → rgwfuncs-0.0.54.dist-info}/METADATA +517 -92
- rgwfuncs-0.0.54.dist-info/RECORD +12 -0
- rgwfuncs-0.0.21.dist-info/RECORD +0 -9
- {rgwfuncs-0.0.21.dist-info → rgwfuncs-0.0.54.dist-info}/LICENSE +0 -0
- {rgwfuncs-0.0.21.dist-info → rgwfuncs-0.0.54.dist-info}/WHEEL +0 -0
- {rgwfuncs-0.0.21.dist-info → rgwfuncs-0.0.54.dist-info}/entry_points.txt +0 -0
- {rgwfuncs-0.0.21.dist-info → rgwfuncs-0.0.54.dist-info}/top_level.txt +0 -0
rgwfuncs/algebra_lib.py
ADDED
@@ -0,0 +1,901 @@
|
|
1
|
+
import re
|
2
|
+
import math
|
3
|
+
import ast
|
4
|
+
import subprocess
|
5
|
+
import tempfile
|
6
|
+
from sympy import symbols, latex, simplify, solve, diff, Expr, factor, cancel, Eq
|
7
|
+
from sympy.core.sympify import SympifyError
|
8
|
+
from sympy.core import S
|
9
|
+
from sympy.parsing.sympy_parser import parse_expr
|
10
|
+
from sympy import __all__ as sympy_functions
|
11
|
+
from sympy.parsing.sympy_parser import (standard_transformations, implicit_multiplication_application)
|
12
|
+
from typing import Tuple, List, Dict, Optional, Any
|
13
|
+
import numpy as np
|
14
|
+
import matplotlib.pyplot as plt
|
15
|
+
from io import BytesIO
|
16
|
+
|
17
|
+
|
18
|
+
def compute_prime_factors(n: int) -> str:
|
19
|
+
"""
|
20
|
+
Computes the prime factors of a number and returns the factorization as a LaTeX string.
|
21
|
+
|
22
|
+
Determines the prime factorization of the given integer. The result is formatted as a LaTeX
|
23
|
+
string, enabling easy integration into documents or presentations that require mathematical notation.
|
24
|
+
|
25
|
+
Parameters:
|
26
|
+
n (int): The number for which to compute prime factors.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
str: The LaTeX representation of the prime factorization.
|
30
|
+
"""
|
31
|
+
|
32
|
+
factors = []
|
33
|
+
while n % 2 == 0:
|
34
|
+
factors.append(2)
|
35
|
+
n //= 2
|
36
|
+
for i in range(3, int(math.sqrt(n)) + 1, 2):
|
37
|
+
while n % i == 0:
|
38
|
+
factors.append(i)
|
39
|
+
n //= i
|
40
|
+
if n > 2:
|
41
|
+
factors.append(n)
|
42
|
+
|
43
|
+
factor_counts = {factor: factors.count(factor) for factor in set(factors)}
|
44
|
+
latex_factors = [f"{factor}^{{{count}}}" if count > 1 else str(
|
45
|
+
factor) for factor, count in factor_counts.items()]
|
46
|
+
return " \\cdot ".join(latex_factors)
|
47
|
+
|
48
|
+
|
49
|
+
def compute_constant_expression(expression: str) -> float:
|
50
|
+
"""
|
51
|
+
Computes the numerical result of a given expression, which can evaluate to a constant,
|
52
|
+
represented as a float.
|
53
|
+
|
54
|
+
Evaluates an constant expression provided as a string and returns the computed result.
|
55
|
+
Supports various arithmetic operations, including addition, subtraction, multiplication,
|
56
|
+
division, and modulo, as well as mathematical functions from the math module.
|
57
|
+
|
58
|
+
Parameters:
|
59
|
+
expression (str): The constant expression to compute. This should be a string consisting
|
60
|
+
of arithmetic operations and Python's math module functions.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
float: The evaluated numerical result of the expression.
|
64
|
+
|
65
|
+
Raises:
|
66
|
+
ValueError: If the expression cannot be evaluated due to syntax errors or other issues.
|
67
|
+
"""
|
68
|
+
try:
|
69
|
+
# Direct numerical evaluation
|
70
|
+
# Safely evaluate the expression using the math module
|
71
|
+
numeric_result = eval(expression, {"__builtins__": None, "math": math})
|
72
|
+
|
73
|
+
# Convert to float if possible
|
74
|
+
return float(numeric_result)
|
75
|
+
except Exception as e:
|
76
|
+
raise ValueError(f"Error computing expression: {e}")
|
77
|
+
|
78
|
+
|
79
|
+
def compute_constant_expression_involving_matrices(expression: str) -> str:
|
80
|
+
"""
|
81
|
+
Computes the result of a constant expression involving matrices and returns it as a LaTeX string.
|
82
|
+
|
83
|
+
Parameters:
|
84
|
+
expression (str): The constant expression involving matrices. Example format includes operations such as "+",
|
85
|
+
"-", "*", "/".
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
str: The LaTeX-formatted string representation of the result or a message indicating an error in dimensions.
|
89
|
+
"""
|
90
|
+
|
91
|
+
def elementwise_operation(matrix1: List[List[float]], matrix2: List[List[float]], operation: str) -> List[List[float]]:
|
92
|
+
if len(matrix1) != len(matrix2) or any(len(row1) != len(row2) for row1, row2 in zip(matrix1, matrix2)):
|
93
|
+
return "Operations between matrices must involve matrices of the same dimension"
|
94
|
+
|
95
|
+
if operation == '+':
|
96
|
+
return [[a + b for a, b in zip(row1, row2)] for row1, row2 in zip(matrix1, matrix2)]
|
97
|
+
elif operation == '-':
|
98
|
+
return [[a - b for a, b in zip(row1, row2)] for row1, row2 in zip(matrix1, matrix2)]
|
99
|
+
elif operation == '*':
|
100
|
+
return [[a * b for a, b in zip(row1, row2)] for row1, row2 in zip(matrix1, matrix2)]
|
101
|
+
elif operation == '/':
|
102
|
+
return [[a / b for a, b in zip(row1, row2) if b != 0] for row1, row2 in zip(matrix1, matrix2)]
|
103
|
+
else:
|
104
|
+
return f"Unsupported operation {operation}"
|
105
|
+
|
106
|
+
try:
|
107
|
+
# Use a stack-based method to properly parse matrices
|
108
|
+
elements = []
|
109
|
+
buffer = ''
|
110
|
+
bracket_level = 0
|
111
|
+
operators = set('+-*/')
|
112
|
+
|
113
|
+
for char in expression:
|
114
|
+
if char == '[':
|
115
|
+
if bracket_level == 0 and buffer.strip():
|
116
|
+
elements.append(buffer.strip())
|
117
|
+
buffer = ''
|
118
|
+
bracket_level += 1
|
119
|
+
elif char == ']':
|
120
|
+
bracket_level -= 1
|
121
|
+
if bracket_level == 0:
|
122
|
+
buffer += char
|
123
|
+
elements.append(buffer.strip())
|
124
|
+
buffer = ''
|
125
|
+
continue
|
126
|
+
if bracket_level == 0 and char in operators:
|
127
|
+
if buffer.strip():
|
128
|
+
elements.append(buffer.strip())
|
129
|
+
buffer = ''
|
130
|
+
elements.append(char)
|
131
|
+
else:
|
132
|
+
buffer += char
|
133
|
+
|
134
|
+
if buffer.strip():
|
135
|
+
elements.append(buffer.strip())
|
136
|
+
|
137
|
+
result = ast.literal_eval(elements[0])
|
138
|
+
|
139
|
+
if not any(isinstance(row, list) for row in result):
|
140
|
+
result = [result] # Convert 1D matrix to 2D
|
141
|
+
|
142
|
+
i = 1
|
143
|
+
while i < len(elements):
|
144
|
+
operation = elements[i]
|
145
|
+
matrix = ast.literal_eval(elements[i + 1])
|
146
|
+
|
147
|
+
if not any(isinstance(row, list) for row in matrix):
|
148
|
+
matrix = [matrix]
|
149
|
+
|
150
|
+
operation_result = elementwise_operation(result, matrix, operation)
|
151
|
+
|
152
|
+
# Check if the operation resulted in an error message
|
153
|
+
if isinstance(operation_result, str):
|
154
|
+
return operation_result
|
155
|
+
|
156
|
+
result = operation_result
|
157
|
+
i += 2
|
158
|
+
|
159
|
+
# Create a LaTeX-style matrix representation
|
160
|
+
matrix_entries = '\\\\'.join(' & '.join(str(x) for x in row) for row in result)
|
161
|
+
return r"\begin{bmatrix}" + f"{matrix_entries}" + r"\end{bmatrix}"
|
162
|
+
|
163
|
+
except Exception as e:
|
164
|
+
return f"Error computing matrix operation: {e}"
|
165
|
+
|
166
|
+
|
167
|
+
def compute_constant_expression_involving_ordered_series(expression: str) -> str:
|
168
|
+
"""
|
169
|
+
Computes the result of a constant expression involving ordered series, and returns it as a Latex string.
|
170
|
+
Supports operations lile "+", "-", "*", "/", as well as "dd()" (the discrete difference operator).
|
171
|
+
|
172
|
+
The function first applies the discrete difference operator to any series where applicable, then evaluates
|
173
|
+
arithmetic operations between series.
|
174
|
+
|
175
|
+
Parameters:
|
176
|
+
expression (str): The series operation expression to compute. Includes operations "+", "-", "*", "/", and "dd()".
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
str: The string representation of the resultant series after performing operations, or an error message
|
180
|
+
if the series lengths do not match.
|
181
|
+
|
182
|
+
Raises:
|
183
|
+
ValueError: If the expression cannot be evaluated.
|
184
|
+
"""
|
185
|
+
|
186
|
+
def elementwise_operation(series1: List[float], series2: List[float], operation: str) -> List[float]:
|
187
|
+
if len(series1) != len(series2):
|
188
|
+
return "Operations between ordered series must involve series of equal length"
|
189
|
+
|
190
|
+
if operation == '+':
|
191
|
+
return [a + b for a, b in zip(series1, series2)]
|
192
|
+
elif operation == '-':
|
193
|
+
return [a - b for a, b in zip(series1, series2)]
|
194
|
+
elif operation == '*':
|
195
|
+
return [a * b for a, b in zip(series1, series2)]
|
196
|
+
elif operation == '/':
|
197
|
+
return [a / b for a, b in zip(series1, series2) if b != 0]
|
198
|
+
else:
|
199
|
+
return f"Unsupported operation {operation}"
|
200
|
+
|
201
|
+
def discrete_difference(series: list) -> list:
|
202
|
+
"""Computes the discrete difference of a series."""
|
203
|
+
return [series[i + 1] - series[i] for i in range(len(series) - 1)]
|
204
|
+
|
205
|
+
try:
|
206
|
+
# First, apply the discrete difference operator where applicable
|
207
|
+
pattern = r'dd\((\[[^\]]*\])\)'
|
208
|
+
matches = re.findall(pattern, expression)
|
209
|
+
|
210
|
+
for match in matches:
|
211
|
+
if match.strip() == '[]':
|
212
|
+
result_series = [] # Handle the empty list case
|
213
|
+
else:
|
214
|
+
series = ast.literal_eval(match)
|
215
|
+
result_series = discrete_difference(series)
|
216
|
+
expression = expression.replace(f'dd({match})', str(result_series))
|
217
|
+
|
218
|
+
# Now parse and evaluate the full expression with basic operations
|
219
|
+
elements = []
|
220
|
+
buffer = ''
|
221
|
+
bracket_level = 0
|
222
|
+
operators = set('+-*/')
|
223
|
+
|
224
|
+
for char in expression:
|
225
|
+
if char == '[':
|
226
|
+
if bracket_level == 0 and buffer.strip():
|
227
|
+
elements.append(buffer.strip())
|
228
|
+
buffer = ''
|
229
|
+
bracket_level += 1
|
230
|
+
elif char == ']':
|
231
|
+
bracket_level -= 1
|
232
|
+
if bracket_level == 0:
|
233
|
+
buffer += char
|
234
|
+
elements.append(buffer.strip())
|
235
|
+
buffer = ''
|
236
|
+
continue
|
237
|
+
if bracket_level == 0 and char in operators:
|
238
|
+
if buffer.strip():
|
239
|
+
elements.append(buffer.strip())
|
240
|
+
buffer = ''
|
241
|
+
elements.append(char)
|
242
|
+
else:
|
243
|
+
buffer += char
|
244
|
+
|
245
|
+
if buffer.strip():
|
246
|
+
elements.append(buffer.strip())
|
247
|
+
|
248
|
+
result = ast.literal_eval(elements[0])
|
249
|
+
|
250
|
+
i = 1
|
251
|
+
while i < len(elements):
|
252
|
+
operation = elements[i]
|
253
|
+
series = ast.literal_eval(elements[i + 1])
|
254
|
+
operation_result = elementwise_operation(result, series, operation)
|
255
|
+
|
256
|
+
# Check if the operation resulted in an error message
|
257
|
+
if isinstance(operation_result, str):
|
258
|
+
return operation_result
|
259
|
+
|
260
|
+
result = operation_result
|
261
|
+
i += 2
|
262
|
+
|
263
|
+
return str(result)
|
264
|
+
|
265
|
+
except Exception as e:
|
266
|
+
return f"Error computing ordered series operation: {e}"
|
267
|
+
|
268
|
+
|
269
|
+
def python_polynomial_expression_to_latex(
|
270
|
+
expression: str,
|
271
|
+
subs: Optional[Dict[str, float]] = None
|
272
|
+
) -> str:
|
273
|
+
"""
|
274
|
+
Converts a polynomial expression written in Python syntax to LaTeX format.
|
275
|
+
|
276
|
+
This function takes an algebraic expression written in Python syntax and converts it
|
277
|
+
to a LaTeX formatted string. The expression is assumed to be in terms acceptable by
|
278
|
+
sympy, with named variables, and optionally includes substitutions for variables.
|
279
|
+
|
280
|
+
Parameters:
|
281
|
+
expression (str): The algebraic expression to convert to LaTeX. The expression should
|
282
|
+
be written using Python syntax.
|
283
|
+
subs (Optional[Dict[str, float]]): An optional dictionary of substitutions for variables
|
284
|
+
in the expression.
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
str: The expression represented as a LaTeX string.
|
288
|
+
|
289
|
+
Raises:
|
290
|
+
ValueError: If the expression cannot be parsed due to syntax errors.
|
291
|
+
"""
|
292
|
+
transformations = standard_transformations + (implicit_multiplication_application,)
|
293
|
+
|
294
|
+
def parse_and_convert_expression(expr_str: str, sym_vars: Dict[str, Expr]) -> Expr:
|
295
|
+
try:
|
296
|
+
# Parse with transformations to handle implicit multiplication
|
297
|
+
expr = parse_expr(expr_str, local_dict=sym_vars, transformations=transformations)
|
298
|
+
if subs:
|
299
|
+
subs_symbols = {symbols(k): v for k, v in subs.items()}
|
300
|
+
expr = expr.subs(subs_symbols)
|
301
|
+
return expr
|
302
|
+
except (SyntaxError, ValueError, TypeError) as e:
|
303
|
+
raise ValueError(f"Error parsing expression: {expr_str}. Error: {e}")
|
304
|
+
|
305
|
+
# Extract variable names used in the expression
|
306
|
+
variable_names = set(re.findall(r'\b[a-zA-Z]\w*\b', expression))
|
307
|
+
sym_vars = {var: symbols(var) for var in variable_names}
|
308
|
+
|
309
|
+
# Import all general function names from SymPy into local scope
|
310
|
+
|
311
|
+
# Dynamically add SymPy functions to the symbol dictionary
|
312
|
+
for func_name in sympy_functions:
|
313
|
+
try:
|
314
|
+
candidate = globals().get(func_name) or locals().get(func_name)
|
315
|
+
if callable(candidate): # Ensure it's actually a callable
|
316
|
+
sym_vars[func_name] = candidate
|
317
|
+
except KeyError:
|
318
|
+
continue # Skip any non-callable or unavailable items
|
319
|
+
|
320
|
+
# Attempt to parse the expression
|
321
|
+
expr = parse_and_convert_expression(expression, sym_vars)
|
322
|
+
|
323
|
+
# Convert the expression to LaTeX format
|
324
|
+
latex_result = latex(expr)
|
325
|
+
return latex_result
|
326
|
+
|
327
|
+
|
328
|
+
def expand_polynomial_expression(
|
329
|
+
expression: str,
|
330
|
+
subs: Optional[Dict[str, float]] = None
|
331
|
+
) -> str:
|
332
|
+
"""
|
333
|
+
Expands a polynomial expression written in Python syntax and converts it to LaTeX format.
|
334
|
+
|
335
|
+
This function takes an algebraic expression written in Python syntax,
|
336
|
+
applies polynomial expansion, and converts the expanded expression
|
337
|
+
to a LaTeX formatted string. The expression should be compatible with sympy.
|
338
|
+
|
339
|
+
Parameters:
|
340
|
+
expression (str): The algebraic expression to expand and convert to LaTeX.
|
341
|
+
The expression should be written using Python syntax.
|
342
|
+
subs (Optional[Dict[str, float]]): An optional dictionary of substitutions
|
343
|
+
to apply to variables in the expression
|
344
|
+
before expansion.
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
str: The expanded expression represented as a LaTeX string.
|
348
|
+
|
349
|
+
Raises:
|
350
|
+
ValueError: If the expression cannot be parsed due to syntax errors.
|
351
|
+
"""
|
352
|
+
transformations = standard_transformations + (implicit_multiplication_application,)
|
353
|
+
|
354
|
+
def parse_and_expand_expression(expr_str: str, sym_vars: Dict[str, symbols]) -> symbols:
|
355
|
+
try:
|
356
|
+
expr = parse_expr(expr_str, local_dict=sym_vars, transformations=transformations)
|
357
|
+
if subs:
|
358
|
+
# Ensure that subs is a dictionary
|
359
|
+
if not isinstance(subs, dict):
|
360
|
+
raise ValueError(f"Substitutions must be a dictionary. Received: {subs}")
|
361
|
+
subs_symbols = {symbols(k): v for k, v in subs.items()}
|
362
|
+
expr = expr.subs(subs_symbols)
|
363
|
+
return expr.expand()
|
364
|
+
except (SyntaxError, ValueError, TypeError, AttributeError) as e:
|
365
|
+
raise ValueError(f"Error parsing expression: {expr_str}. Error: {e}")
|
366
|
+
|
367
|
+
variable_names = set(re.findall(r'\b[a-zA-Z]\w*\b', expression))
|
368
|
+
sym_vars = {var: symbols(var) for var in variable_names}
|
369
|
+
|
370
|
+
expr = parse_and_expand_expression(expression, sym_vars)
|
371
|
+
latex_result = latex(expr)
|
372
|
+
return latex_result
|
373
|
+
|
374
|
+
|
375
|
+
def factor_polynomial_expression(
|
376
|
+
expression: str,
|
377
|
+
subs: Optional[Dict[str, float]] = None
|
378
|
+
) -> str:
|
379
|
+
"""
|
380
|
+
Factors a polynomial expression written in Python syntax and converts it to LaTeX format.
|
381
|
+
|
382
|
+
This function accepts an algebraic expression in Python syntax, performs polynomial factoring,
|
383
|
+
and translates the factored expression into a LaTeX formatted string.
|
384
|
+
|
385
|
+
Parameters:
|
386
|
+
expression (str): The algebraic expression to factor and convert to LaTeX.
|
387
|
+
subs (Optional[Dict[str, float]]): An optional dictionary of substitutions to apply before factoring.
|
388
|
+
|
389
|
+
Returns:
|
390
|
+
str: The LaTeX formatted string of the factored expression.
|
391
|
+
|
392
|
+
Raises:
|
393
|
+
ValueError: If the expression cannot be parsed due to syntax errors.
|
394
|
+
"""
|
395
|
+
transformations = standard_transformations + (implicit_multiplication_application,)
|
396
|
+
|
397
|
+
def parse_and_factor_expression(expr_str: str, sym_vars: Dict[str, symbols]) -> symbols:
|
398
|
+
try:
|
399
|
+
expr = parse_expr(expr_str, local_dict=sym_vars, transformations=transformations)
|
400
|
+
if subs:
|
401
|
+
if not isinstance(subs, dict):
|
402
|
+
raise ValueError(f"Substitutions must be a dictionary. Received: {subs}")
|
403
|
+
subs_symbols = {symbols(k): v for k, v in subs.items()}
|
404
|
+
expr = expr.subs(subs_symbols)
|
405
|
+
return factor(expr)
|
406
|
+
except (SyntaxError, ValueError, TypeError, AttributeError) as e:
|
407
|
+
raise ValueError(f"Error parsing expression: {expr_str}. Error: {e}")
|
408
|
+
|
409
|
+
variable_names = set(re.findall(r'\b[a-zA-Z]\w*\b', expression))
|
410
|
+
sym_vars = {var: symbols(var) for var in variable_names}
|
411
|
+
|
412
|
+
expr = parse_and_factor_expression(expression, sym_vars)
|
413
|
+
latex_result = latex(expr)
|
414
|
+
return latex_result
|
415
|
+
|
416
|
+
|
417
|
+
def simplify_polynomial_expression(
|
418
|
+
expression: str,
|
419
|
+
subs: Optional[Dict[str, float]] = None
|
420
|
+
) -> str:
|
421
|
+
"""
|
422
|
+
Simplifies an algebraic expression in polynomial form and returns it in LaTeX format.
|
423
|
+
|
424
|
+
Takes an algebraic expression, in polynomial form, written in Python syntax and simplifies it.
|
425
|
+
The result is returned as a LaTeX formatted string, suitable for academic or professional
|
426
|
+
documentation.
|
427
|
+
|
428
|
+
Parameters:
|
429
|
+
expression (str): The algebraic expression, in polynomial form, to simplify. For instance,
|
430
|
+
the expression `np.diff(8*x**30)` is a polynomial, whereas np.diff([2,5,9,11)
|
431
|
+
is not a polynomial.
|
432
|
+
subs (Optional[Dict[str, float]]): An optional dictionary of substitutions for variables
|
433
|
+
in the expression.
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
str: The simplified expression represented as a LaTeX string.
|
437
|
+
|
438
|
+
Raises:
|
439
|
+
ValueError: If the expression cannot be simplified due to errors in expression or parameters.
|
440
|
+
"""
|
441
|
+
|
442
|
+
def recursive_parse_function_call(
|
443
|
+
func_call: str, prefix: str, sym_vars: Dict[str, Expr]) -> Tuple[str, List[Expr]]:
|
444
|
+
# print(f"Parsing function call: {func_call}")
|
445
|
+
|
446
|
+
# Match the function name and arguments
|
447
|
+
match = re.match(fr'{prefix}\.(\w+)\((.*)\)', func_call, re.DOTALL)
|
448
|
+
if not match:
|
449
|
+
raise ValueError(f"Invalid function call: {func_call}")
|
450
|
+
|
451
|
+
func_name = match.group(1)
|
452
|
+
args_str = match.group(2)
|
453
|
+
|
454
|
+
# Check if it's a list for np
|
455
|
+
if prefix == 'np' and args_str.startswith(
|
456
|
+
"[") and args_str.endswith("]"):
|
457
|
+
parsed_args = [ast.literal_eval(args_str.strip())]
|
458
|
+
else:
|
459
|
+
parsed_args = []
|
460
|
+
raw_args = re.split(r',(?![^{]*\})', args_str)
|
461
|
+
for arg in raw_args:
|
462
|
+
arg = arg.strip()
|
463
|
+
if re.match(r'\w+\.\w+\(', arg):
|
464
|
+
# Recursively evaluate the argument if it's another
|
465
|
+
# function call
|
466
|
+
arg_val = recursive_eval_func(
|
467
|
+
re.match(r'\w+\.\w+\(.*\)', arg), sym_vars)
|
468
|
+
parsed_args.append(
|
469
|
+
parse_expr(
|
470
|
+
arg_val,
|
471
|
+
local_dict=sym_vars))
|
472
|
+
else:
|
473
|
+
parsed_args.append(parse_expr(arg, local_dict=sym_vars))
|
474
|
+
|
475
|
+
# print(f"Function name: {func_name}, Parsed arguments: {parsed_args}")
|
476
|
+
return func_name, parsed_args
|
477
|
+
|
478
|
+
def recursive_eval_func(match: re.Match, sym_vars: Dict[str, Expr]) -> str:
|
479
|
+
# print("152", match)
|
480
|
+
func_call = match.group(0)
|
481
|
+
# print(f"153 Evaluating function call: {func_call}")
|
482
|
+
|
483
|
+
if func_call.startswith("np."):
|
484
|
+
func_name, args = recursive_parse_function_call(
|
485
|
+
func_call, 'np', sym_vars)
|
486
|
+
if func_name == 'diff':
|
487
|
+
expr = args[0]
|
488
|
+
if isinstance(expr, list):
|
489
|
+
# Calculate discrete difference
|
490
|
+
diff_result = [expr[i] - expr[i - 1]
|
491
|
+
for i in range(1, len(expr))]
|
492
|
+
return str(diff_result)
|
493
|
+
# Perform symbolic differentiation
|
494
|
+
diff_result = diff(expr)
|
495
|
+
return str(diff_result)
|
496
|
+
|
497
|
+
if func_call.startswith("math."):
|
498
|
+
func_name, args = recursive_parse_function_call(
|
499
|
+
func_call, 'math', sym_vars)
|
500
|
+
if hasattr(math, func_name):
|
501
|
+
result = getattr(math, func_name)(*args)
|
502
|
+
return str(result)
|
503
|
+
|
504
|
+
if func_call.startswith("sym."):
|
505
|
+
initial_method_match = re.match(
|
506
|
+
r'(sym\.\w+\([^()]*\))(\.(\w+)\((.*?)\))*', func_call, re.DOTALL)
|
507
|
+
if initial_method_match:
|
508
|
+
base_expr_str = initial_method_match.group(1)
|
509
|
+
base_func_name, base_args = recursive_parse_function_call(
|
510
|
+
base_expr_str, 'sym', sym_vars)
|
511
|
+
if base_func_name == 'solve':
|
512
|
+
solutions = solve(base_args[0], base_args[1])
|
513
|
+
# print(f"Solutions found: {solutions}")
|
514
|
+
|
515
|
+
method_chain = re.findall(
|
516
|
+
r'\.(\w+)\((.*?)\)', func_call, re.DOTALL)
|
517
|
+
final_solutions = [execute_chained_methods(sol, [(m, [method_args.strip(
|
518
|
+
)]) for m, method_args in method_chain], sym_vars) for sol in solutions]
|
519
|
+
|
520
|
+
return "[" + ",".join(latex(simplify(sol))
|
521
|
+
for sol in final_solutions) + "]"
|
522
|
+
|
523
|
+
raise ValueError(f"Unknown function call: {func_call}")
|
524
|
+
|
525
|
+
def execute_chained_methods(sym_expr: Expr,
|
526
|
+
method_chain: List[Tuple[str,
|
527
|
+
List[str]]],
|
528
|
+
sym_vars: Dict[str,
|
529
|
+
Expr]) -> Expr:
|
530
|
+
for method_name, method_args in method_chain:
|
531
|
+
# print(f"Executing method: {method_name} with arguments: {method_args}")
|
532
|
+
method = getattr(sym_expr, method_name, None)
|
533
|
+
if method:
|
534
|
+
if method_name == 'subs' and isinstance(method_args[0], dict):
|
535
|
+
kwargs = method_args[0]
|
536
|
+
kwargs = {
|
537
|
+
parse_expr(
|
538
|
+
k,
|
539
|
+
local_dict=sym_vars): parse_expr(
|
540
|
+
v,
|
541
|
+
local_dict=sym_vars) for k,
|
542
|
+
v in kwargs.items()}
|
543
|
+
sym_expr = method(kwargs)
|
544
|
+
else:
|
545
|
+
args = [parse_expr(arg.strip(), local_dict=sym_vars)
|
546
|
+
for arg in method_args]
|
547
|
+
sym_expr = method(*args)
|
548
|
+
# print(f"Result after {method_name}: {sym_expr}")
|
549
|
+
return sym_expr
|
550
|
+
|
551
|
+
variable_names = set(re.findall(r'\b[a-zA-Z]\w*\b', expression))
|
552
|
+
sym_vars = {var: symbols(var) for var in variable_names}
|
553
|
+
|
554
|
+
patterns = {
|
555
|
+
# "numpy_diff_brackets": r"np\.diff\(\[.*?\]\)",
|
556
|
+
"numpy_diff_no_brackets": r"np\.diff\([^()]*\)",
|
557
|
+
"math_functions": r"math\.\w+\((?:[^()]*(?:\([^()]*\)[^()]*)*)\)",
|
558
|
+
# "sympy_functions": r"sym\.\w+\([^()]*\)(?:\.\w+\([^()]*\))?",
|
559
|
+
}
|
560
|
+
|
561
|
+
function_pattern = '|'.join(patterns.values())
|
562
|
+
|
563
|
+
# Use a lambda function to pass additional arguments
|
564
|
+
processed_expression = re.sub(
|
565
|
+
function_pattern, lambda match: recursive_eval_func(
|
566
|
+
match, sym_vars), expression)
|
567
|
+
# print("Level 2 processed_expression:", processed_expression)
|
568
|
+
|
569
|
+
try:
|
570
|
+
# Parse the expression
|
571
|
+
expr = parse_expr(processed_expression, local_dict=sym_vars)
|
572
|
+
|
573
|
+
# Apply substitutions if provided
|
574
|
+
if subs:
|
575
|
+
subs_symbols = {symbols(k): v for k, v in subs.items()}
|
576
|
+
expr = expr.subs(subs_symbols)
|
577
|
+
|
578
|
+
# Simplify the expression
|
579
|
+
final_result = simplify(expr)
|
580
|
+
|
581
|
+
# Convert the result to LaTeX format
|
582
|
+
if final_result.free_symbols:
|
583
|
+
latex_result = latex(final_result)
|
584
|
+
return latex_result
|
585
|
+
else:
|
586
|
+
return str(final_result)
|
587
|
+
|
588
|
+
except Exception as e:
|
589
|
+
raise ValueError(f"Error simplifying expression: {e}")
|
590
|
+
|
591
|
+
|
592
|
+
def cancel_polynomial_expression(
|
593
|
+
expression: str,
|
594
|
+
subs: Optional[Dict[str, float]] = None
|
595
|
+
) -> str:
|
596
|
+
"""
|
597
|
+
Cancels common factors within a polynomial expression and converts it to LaTeX format.
|
598
|
+
|
599
|
+
This function parses an algebraic expression given in Python syntax, cancels any common factors,
|
600
|
+
and converts the resulting simplified expression into a LaTeX formatted string. The function can
|
601
|
+
also handle optional substitutions of variables before performing the cancellation.
|
602
|
+
|
603
|
+
Parameters:
|
604
|
+
expression (str): The algebraic expression to simplify and convert to LaTeX.
|
605
|
+
It should be a valid expression formatted using Python syntax.
|
606
|
+
subs (Optional[Dict[str, float]]): An optional dictionary where the keys are variable names in the
|
607
|
+
expression, and the values are the corresponding numbers to substitute
|
608
|
+
into the expression before simplification.
|
609
|
+
|
610
|
+
Returns:
|
611
|
+
str: The LaTeX formatted string of the simplified expression. If the expression involves
|
612
|
+
indeterminate forms due to operations like division by zero, a descriptive error message is returned instead.
|
613
|
+
|
614
|
+
Raises:
|
615
|
+
ValueError: If the expression cannot be parsed due to syntax errors or if operations result in
|
616
|
+
undefined behavior, such as division by zero.
|
617
|
+
|
618
|
+
"""
|
619
|
+
transformations = standard_transformations + (implicit_multiplication_application,)
|
620
|
+
|
621
|
+
def parse_and_cancel_expression(expr_str: str, sym_vars: Dict[str, symbols]) -> symbols:
|
622
|
+
try:
|
623
|
+
expr = parse_expr(expr_str, local_dict=sym_vars, transformations=transformations)
|
624
|
+
if subs:
|
625
|
+
if not isinstance(subs, dict):
|
626
|
+
raise ValueError(f"Substitutions must be a dictionary. Received: {subs}")
|
627
|
+
subs_symbols = {symbols(k): v for k, v in subs.items()}
|
628
|
+
expr = expr.subs(subs_symbols)
|
629
|
+
|
630
|
+
canceled_expr = cancel(expr)
|
631
|
+
|
632
|
+
# Check for NaN or indeterminate forms
|
633
|
+
if canceled_expr.has(S.NaN) or canceled_expr.has(S.Infinity) or canceled_expr.has(S.ComplexInfinity):
|
634
|
+
return "Undefined result. This could be a division by zero error."
|
635
|
+
|
636
|
+
return canceled_expr
|
637
|
+
|
638
|
+
except (SyntaxError, ValueError, TypeError, AttributeError, ZeroDivisionError, SympifyError) as e:
|
639
|
+
return f"Error: {str(e)}"
|
640
|
+
|
641
|
+
variable_names = set(re.findall(r'\b[a-zA-Z]\w*\b', expression))
|
642
|
+
sym_vars = {var: symbols(var) for var in variable_names}
|
643
|
+
|
644
|
+
expr = parse_and_cancel_expression(expression, sym_vars)
|
645
|
+
|
646
|
+
# If the expression is already a string (i.e., "Undefined" or error message), return it directly
|
647
|
+
if isinstance(expr, str):
|
648
|
+
return expr
|
649
|
+
|
650
|
+
# Otherwise, convert to LaTeX as usual
|
651
|
+
latex_result = latex(expr)
|
652
|
+
return latex_result
|
653
|
+
|
654
|
+
|
655
|
+
def solve_homogeneous_polynomial_expression(
|
656
|
+
expression: str,
|
657
|
+
variable: str,
|
658
|
+
subs: Optional[Dict[str, float]] = None
|
659
|
+
) -> str:
|
660
|
+
"""
|
661
|
+
Solves a homogeneous polynomial expression for a specified variable and returns solutions
|
662
|
+
in LaTeX format.
|
663
|
+
|
664
|
+
Assumes that the expression is homoegeneous (i.e. equal to zero), and solves for a
|
665
|
+
designated variable. May optionally include substitutions for other variables in the
|
666
|
+
equation. The solutions are provided as a LaTeX formatted string.
|
667
|
+
|
668
|
+
Parameters:
|
669
|
+
expression (str): The homogeneous polynomial expression to solve.
|
670
|
+
variable (str): The variable to solve the equation for.
|
671
|
+
subs (Optional[Dict[str, float]]): An optional dictionary of substitutions for variables
|
672
|
+
in the equation.
|
673
|
+
|
674
|
+
Returns:
|
675
|
+
str: The solutions of the equation, formatted as a LaTeX string.
|
676
|
+
|
677
|
+
Raises:
|
678
|
+
ValueError: If the equation cannot be solved due to errors in expression or parameters.
|
679
|
+
"""
|
680
|
+
|
681
|
+
try:
|
682
|
+
# Handle symbols
|
683
|
+
variable_symbols = set(re.findall(r'\b[a-zA-Z]\w*\b', expression))
|
684
|
+
sym_vars = {var: symbols(var) for var in variable_symbols}
|
685
|
+
|
686
|
+
# Parse the expression
|
687
|
+
expr = parse_expr(expression, local_dict=sym_vars)
|
688
|
+
|
689
|
+
# Apply substitutions
|
690
|
+
if subs:
|
691
|
+
expr = expr.subs({symbols(k): v for k, v in subs.items()})
|
692
|
+
|
693
|
+
# Solve the equation
|
694
|
+
var_symbol = symbols(variable)
|
695
|
+
eq = Eq(expr, 0)
|
696
|
+
solutions = solve(eq, var_symbol)
|
697
|
+
|
698
|
+
# Convert solutions to LaTeX strings with handling for exact representations
|
699
|
+
latex_solutions = [latex(sol) for sol in solutions]
|
700
|
+
|
701
|
+
result = r"\left[" + ", ".join(latex_solutions) + r"\right]"
|
702
|
+
print("693", result)
|
703
|
+
return result
|
704
|
+
|
705
|
+
except Exception as e:
|
706
|
+
raise ValueError(f"Error solving the expression: {e}")
|
707
|
+
|
708
|
+
|
709
|
+
def plot_polynomial_functions(
|
710
|
+
functions: List[Dict[str, Dict[str, Any]]],
|
711
|
+
zoom: float = 10.0,
|
712
|
+
show_legend: bool = True,
|
713
|
+
open_file: bool = False,
|
714
|
+
save_path: Optional[str] = None,
|
715
|
+
) -> str:
|
716
|
+
"""
|
717
|
+
Plots expressions described by a list of dictionaries of the form:
|
718
|
+
[
|
719
|
+
{ "expression_string": { "x": "*", "a":..., "b":... } },
|
720
|
+
{ "expression_string": { "x": np.linspace(...), "a":..., ... } },
|
721
|
+
...
|
722
|
+
]
|
723
|
+
|
724
|
+
In each top-level dictionary, there is exactly one key (a string
|
725
|
+
representing a Python/NumPy expression) and one value (a dictionary of
|
726
|
+
substitutions). This substitutions dictionary must have an "x" key:
|
727
|
+
• "x": "*" -> Use a default domain from -zoom..+zoom.
|
728
|
+
• "x": np.array(...) -> Use that array as the domain.
|
729
|
+
Other variables (like "a", "b", etc.) may also appear in the same dict.
|
730
|
+
|
731
|
+
Additionally, we use latexify_expression(...) to transform the expression
|
732
|
+
into a nice LaTeX form for the legend, including a special Δ notation for np.diff(...).
|
733
|
+
|
734
|
+
Parameters
|
735
|
+
----------
|
736
|
+
functions : List[Dict[str, Dict[str, Any]]]
|
737
|
+
A list of items. Each item is a dictionary:
|
738
|
+
key = expression string (e.g., "x**2", "np.diff(x,2)", etc.)
|
739
|
+
value = a dictionary of substitutions. Must contain "x",
|
740
|
+
either as "*" or a NumPy array. May contain additional
|
741
|
+
parameters like "a", "b", etc.
|
742
|
+
zoom : float
|
743
|
+
Sets the numeric axis range from -zoom..+zoom in both x and y.
|
744
|
+
show_legend : bool
|
745
|
+
Whether to add a legend to the plot (defaults to True).
|
746
|
+
open_file : bool
|
747
|
+
If saving to path is not desirable, opens the SVG as a temp file;
|
748
|
+
otherwise opens the file from the actual location using the system's
|
749
|
+
default viewer (defaults to False).
|
750
|
+
save_path : Optional[str]
|
751
|
+
If specified, saves the output string as a .svg at the indicated path
|
752
|
+
(defaults to None).
|
753
|
+
|
754
|
+
Returns
|
755
|
+
-------
|
756
|
+
str
|
757
|
+
The raw SVG markup of the resulting plot.
|
758
|
+
"""
|
759
|
+
|
760
|
+
def latexify_expression(expr_str: str) -> str:
|
761
|
+
# Regex to locate np.diff(...) with an optional second argument
|
762
|
+
DIFF_PATTERN = r"np\.diff\s*\(\s*([^,\)]+)(?:,\s*(\d+))?\)"
|
763
|
+
|
764
|
+
def diff_replacer(match: re.Match) -> str:
|
765
|
+
inside = match.group(1).strip()
|
766
|
+
exponent = match.group(2)
|
767
|
+
inside_no_np = inside.replace("np.", "")
|
768
|
+
if exponent:
|
769
|
+
return rf"\Delta^{exponent}\left({inside_no_np}\right)"
|
770
|
+
else:
|
771
|
+
return rf"\Delta\left({inside_no_np}\right)"
|
772
|
+
|
773
|
+
expr_tmp = re.sub(DIFF_PATTERN, diff_replacer, expr_str)
|
774
|
+
expr_tmp = expr_tmp.replace("np.", "")
|
775
|
+
|
776
|
+
# Attempt to convert basic Pythonic polynomial expressions to LaTeX
|
777
|
+
try:
|
778
|
+
# Suppose you have a helper function python_polynomial_expression_to_latex
|
779
|
+
# If not, you can do a naive replacement or skip
|
780
|
+
from python_latex_helpers import python_polynomial_expression_to_latex
|
781
|
+
latex_expr = python_polynomial_expression_to_latex(expr_tmp)
|
782
|
+
return latex_expr
|
783
|
+
except Exception:
|
784
|
+
# Fallback: naive ** -> ^
|
785
|
+
return expr_tmp.replace("**", "^")
|
786
|
+
|
787
|
+
def handle_open_and_save(svg_string: str, open_it: bool, path: Optional[str]) -> None:
|
788
|
+
# Save the SVG to a file if a path is provided
|
789
|
+
if path:
|
790
|
+
try:
|
791
|
+
with open(path, 'w', encoding='utf-8') as file:
|
792
|
+
file.write(svg_string)
|
793
|
+
print(f"[INFO] SVG saved to: {path}")
|
794
|
+
except IOError as e:
|
795
|
+
print(f"[ERROR] Failed to save SVG to {path}. IOError: {e}")
|
796
|
+
|
797
|
+
# Handle opening the file if requested
|
798
|
+
if open_it and path:
|
799
|
+
result = subprocess.run(["xdg-open", path], stderr=subprocess.DEVNULL)
|
800
|
+
if result.returncode != 0:
|
801
|
+
print("[ERROR] Failed to open the SVG file with the default viewer.")
|
802
|
+
elif open_it:
|
803
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".svg") as tmpfile:
|
804
|
+
temp_svg_path = tmpfile.name
|
805
|
+
tmpfile.write(svg_string.encode('utf-8'))
|
806
|
+
result = subprocess.run(["xdg-open", temp_svg_path], stderr=subprocess.DEVNULL)
|
807
|
+
if result.returncode != 0:
|
808
|
+
print("[ERROR] Failed to open the SVG file with the default viewer.")
|
809
|
+
|
810
|
+
buffer = BytesIO()
|
811
|
+
fig, ax = plt.subplots()
|
812
|
+
|
813
|
+
for entry in functions:
|
814
|
+
# Each entry is something like {"x**2": {"x": "*", "a": ...}}
|
815
|
+
if len(entry) != 1:
|
816
|
+
print("[WARNING] Skipping invalid item. Must have exactly 1 expression->substitutions pair.")
|
817
|
+
continue
|
818
|
+
|
819
|
+
# Extract the expression string and substitutions
|
820
|
+
expression, sub_dict = next(iter(entry.items()))
|
821
|
+
|
822
|
+
# Check presence of "x"
|
823
|
+
if "x" not in sub_dict:
|
824
|
+
print(f"[WARNING] Skipping '{expression}' because there is no 'x' key.")
|
825
|
+
continue
|
826
|
+
|
827
|
+
x_val = sub_dict["x"]
|
828
|
+
|
829
|
+
# 1) If x == "*", generate from -zoom..+zoom
|
830
|
+
if isinstance(x_val, str) and x_val == "*":
|
831
|
+
x_values = np.linspace(-zoom, zoom, 1201)
|
832
|
+
sub_dict["x"] = x_values # might as well update it in place
|
833
|
+
# 2) If x is already a NumPy array, use as-is
|
834
|
+
elif isinstance(x_val, np.ndarray):
|
835
|
+
x_values = x_val
|
836
|
+
else:
|
837
|
+
print(f"[WARNING] Skipping '{expression}' because 'x' is neither '*' nor a NumPy array.")
|
838
|
+
continue
|
839
|
+
|
840
|
+
# Evaluate the expression with the variables from sub_dict
|
841
|
+
# We'll inject them into an eval() context, including 'np'
|
842
|
+
try:
|
843
|
+
eval_context = {"np": np}
|
844
|
+
# Put all user-provided variables (like a=1.23) in:
|
845
|
+
eval_context.update(sub_dict)
|
846
|
+
y_values = eval(expression, {"np": np}, eval_context)
|
847
|
+
except Exception as e:
|
848
|
+
print(f"[ERROR] Could not evaluate '{expression}' -> {e}")
|
849
|
+
continue
|
850
|
+
|
851
|
+
# Check we got a NumPy array
|
852
|
+
if not isinstance(y_values, np.ndarray):
|
853
|
+
print(f"[WARNING] Skipping '{expression}' because it did not produce a NumPy array.")
|
854
|
+
continue
|
855
|
+
|
856
|
+
# If y is shorter (like np.diff), truncate x
|
857
|
+
if len(y_values) < len(x_values):
|
858
|
+
x_values = x_values[:len(y_values)]
|
859
|
+
|
860
|
+
# Convert the expression to a LaTeX label
|
861
|
+
label_expr = latexify_expression(expression)
|
862
|
+
ax.plot(x_values, y_values, label=rf"${label_expr}$")
|
863
|
+
|
864
|
+
# Configure axes
|
865
|
+
ax.set_xlim(-zoom, zoom)
|
866
|
+
ax.set_ylim(-zoom, zoom)
|
867
|
+
|
868
|
+
# Place spines at center
|
869
|
+
ax.spines['left'].set_position('zero')
|
870
|
+
ax.spines['bottom'].set_position('zero')
|
871
|
+
# Hide the right and top spines
|
872
|
+
ax.spines['right'].set_color('none')
|
873
|
+
ax.spines['top'].set_color('none')
|
874
|
+
ax.xaxis.set_ticks_position('bottom')
|
875
|
+
ax.yaxis.set_ticks_position('left')
|
876
|
+
|
877
|
+
# Ensure equal aspect ratio
|
878
|
+
ax.set_aspect('equal', 'box')
|
879
|
+
ax.grid(True)
|
880
|
+
|
881
|
+
# If requested, show the legend
|
882
|
+
if show_legend:
|
883
|
+
leg = ax.legend(
|
884
|
+
loc='upper center',
|
885
|
+
bbox_to_anchor=(0.5, -0.03),
|
886
|
+
fancybox=True,
|
887
|
+
shadow=True,
|
888
|
+
ncol=1
|
889
|
+
)
|
890
|
+
plt.savefig(buffer, format='svg', bbox_inches='tight', bbox_extra_artists=[leg])
|
891
|
+
else:
|
892
|
+
plt.savefig(buffer, format='svg', bbox_inches='tight')
|
893
|
+
|
894
|
+
plt.close(fig)
|
895
|
+
svg_string = buffer.getvalue().decode('utf-8')
|
896
|
+
|
897
|
+
# Optionally open/save the file
|
898
|
+
handle_open_and_save(svg_string, open_file, save_path)
|
899
|
+
|
900
|
+
return svg_string
|
901
|
+
|