rgwfuncs 0.0.101__py3-none-any.whl → 0.0.106__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rgwfuncs/__init__.py +1 -2
- rgwfuncs/interactive_shell_lib.py +0 -1
- rgwfuncs/str_lib.py +170 -0
- {rgwfuncs-0.0.101.dist-info → rgwfuncs-0.0.106.dist-info}/METADATA +7 -409
- rgwfuncs-0.0.106.dist-info/RECORD +11 -0
- rgwfuncs/algebra_lib.py +0 -1064
- rgwfuncs-0.0.101.dist-info/RECORD +0 -12
- {rgwfuncs-0.0.101.dist-info → rgwfuncs-0.0.106.dist-info}/WHEEL +0 -0
- {rgwfuncs-0.0.101.dist-info → rgwfuncs-0.0.106.dist-info}/entry_points.txt +0 -0
- {rgwfuncs-0.0.101.dist-info → rgwfuncs-0.0.106.dist-info}/licenses/LICENSE +0 -0
- {rgwfuncs-0.0.101.dist-info → rgwfuncs-0.0.106.dist-info}/top_level.txt +0 -0
rgwfuncs/algebra_lib.py
DELETED
@@ -1,1064 +0,0 @@
|
|
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
|
-
|
902
|
-
|
903
|
-
def plot_x_points_of_polynomial_functions(
|
904
|
-
functions: List[Dict[str, Dict[str, Any]]],
|
905
|
-
zoom: float = 10.0,
|
906
|
-
show_legend: bool = True,
|
907
|
-
open_file: bool = False,
|
908
|
-
save_path: Optional[str] = None,
|
909
|
-
) -> str:
|
910
|
-
"""
|
911
|
-
Plots one or more expressions described by a list of dictionaries. For each
|
912
|
-
item in the list, the function evaluates the given Python/NumPy expression
|
913
|
-
at the specified x-values (converted to NumPy arrays if they are Python lists)
|
914
|
-
and plots the resulting points on a single figure.
|
915
|
-
|
916
|
-
Parameters
|
917
|
-
----------
|
918
|
-
functions : List[Dict[str, Dict[str, Any]]] A list of one or more items,
|
919
|
-
each of which has exactly one key-value pair:
|
920
|
-
- Key (`str`): A valid Python/NumPy expression (e.g., `x**2`,
|
921
|
-
`np.sin(x)`, `x - a`).
|
922
|
-
- Value (`Dict[str, Any]`): Must assign `x` a value
|
923
|
-
zoom : float, optional
|
924
|
-
Determines the numeric axis range from -zoom..+zoom in both x and y
|
925
|
-
(default is 10.0).
|
926
|
-
show_legend : bool, optional
|
927
|
-
Whether to include a legend in the plot (default is True).
|
928
|
-
open_file : bool, optional
|
929
|
-
If saving to path is not desirable, opens the SVG as a temp file;
|
930
|
-
otherwise opens the file from the indicated path using the system's
|
931
|
-
default viewer (defaults to False).
|
932
|
-
save_path : Optional[str], optional
|
933
|
-
If specified, saves the output SVG at the given path (defaults to None).
|
934
|
-
|
935
|
-
Returns
|
936
|
-
-------
|
937
|
-
str
|
938
|
-
The raw SVG markup of the resulting scatter plot.
|
939
|
-
|
940
|
-
"""
|
941
|
-
def latexify_expression(expr_str: str) -> str:
|
942
|
-
# Regex to locate np.diff(...) with an optional second argument
|
943
|
-
DIFF_PATTERN = r"np\.diff\s*\(\s*([^,\)]+)(?:,\s*(\d+))?\)"
|
944
|
-
|
945
|
-
def diff_replacer(match: re.Match) -> str:
|
946
|
-
inside = match.group(1).strip()
|
947
|
-
exponent = match.group(2)
|
948
|
-
inside_no_np = inside.replace("np.", "")
|
949
|
-
if exponent:
|
950
|
-
return rf"\Delta^{exponent}\left({inside_no_np}\right)"
|
951
|
-
else:
|
952
|
-
return rf"\Delta\left({inside_no_np}\right)"
|
953
|
-
|
954
|
-
expr_tmp = re.sub(DIFF_PATTERN, diff_replacer, expr_str)
|
955
|
-
expr_tmp = expr_tmp.replace("np.", "")
|
956
|
-
|
957
|
-
# Attempt to convert basic Pythonic polynomial expressions into LaTeX
|
958
|
-
try:
|
959
|
-
from python_latex_helpers import python_polynomial_expression_to_latex
|
960
|
-
latex_expr = python_polynomial_expression_to_latex(expr_tmp)
|
961
|
-
return latex_expr
|
962
|
-
except Exception:
|
963
|
-
# Fallback: naive ** -> ^
|
964
|
-
return expr_tmp.replace("**", "^")
|
965
|
-
|
966
|
-
def handle_open_and_save(svg_string: str, open_it: bool, path: Optional[str]) -> None:
|
967
|
-
# Save the SVG to a file if a path is provided
|
968
|
-
if path:
|
969
|
-
try:
|
970
|
-
with open(path, 'w', encoding='utf-8') as file:
|
971
|
-
file.write(svg_string)
|
972
|
-
print(f"[INFO] SVG saved to: {path}")
|
973
|
-
except IOError as e:
|
974
|
-
print(f"[ERROR] Failed to save SVG to {path}. IOError: {e}")
|
975
|
-
|
976
|
-
# Handle opening the file if requested
|
977
|
-
if open_it and path:
|
978
|
-
result = subprocess.run(["xdg-open", path], stderr=subprocess.DEVNULL)
|
979
|
-
if result.returncode != 0:
|
980
|
-
print("[ERROR] Failed to open the SVG file with the default viewer.")
|
981
|
-
elif open_it:
|
982
|
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".svg") as tmpfile:
|
983
|
-
temp_svg_path = tmpfile.name
|
984
|
-
tmpfile.write(svg_string.encode('utf-8'))
|
985
|
-
result = subprocess.run(["xdg-open", temp_svg_path], stderr=subprocess.DEVNULL)
|
986
|
-
if result.returncode != 0:
|
987
|
-
print("[ERROR] Failed to open the SVG file with the default viewer.")
|
988
|
-
|
989
|
-
# Set up a buffer for the SVG output
|
990
|
-
buffer = BytesIO()
|
991
|
-
fig, ax = plt.subplots()
|
992
|
-
|
993
|
-
# Iterate over each expression-substitution dictionary
|
994
|
-
for item in functions:
|
995
|
-
# Each entry in 'functions' must have exactly one key-value pair
|
996
|
-
if len(item) != 1:
|
997
|
-
print("[WARNING] Skipping invalid item. It must have exactly 1 expression->substitutions pair.")
|
998
|
-
continue
|
999
|
-
|
1000
|
-
expression, sub_dict = next(iter(item.items()))
|
1001
|
-
|
1002
|
-
# Ensure 'x' is present
|
1003
|
-
if "x" not in sub_dict:
|
1004
|
-
print(f"[WARNING] Skipping '{expression}' because there is no 'x' key.")
|
1005
|
-
continue
|
1006
|
-
|
1007
|
-
x_vals = sub_dict["x"]
|
1008
|
-
# Convert to numpy array if needed
|
1009
|
-
if not isinstance(x_vals, np.ndarray):
|
1010
|
-
x_vals = np.array(x_vals)
|
1011
|
-
|
1012
|
-
# Evaluate expression with the given variables
|
1013
|
-
try:
|
1014
|
-
eval_context = {"np": np}
|
1015
|
-
eval_context.update(sub_dict) # put all user-provided variables in the context
|
1016
|
-
y_vals = eval(expression, {"np": np}, eval_context)
|
1017
|
-
except Exception as e:
|
1018
|
-
print(f"[ERROR] Could not evaluate expression '{expression}': {e}")
|
1019
|
-
continue
|
1020
|
-
|
1021
|
-
# Convert y-values to a numpy array if needed
|
1022
|
-
if not isinstance(y_vals, np.ndarray):
|
1023
|
-
y_vals = np.array(y_vals)
|
1024
|
-
|
1025
|
-
# Prepare label (LaTeXified)
|
1026
|
-
label_expr = latexify_expression(expression)
|
1027
|
-
|
1028
|
-
# Scatter plot
|
1029
|
-
ax.scatter(x_vals, y_vals, label=rf"${label_expr}$")
|
1030
|
-
|
1031
|
-
# Configure axes
|
1032
|
-
ax.set_xlim(-zoom, zoom)
|
1033
|
-
ax.set_ylim(-zoom, zoom)
|
1034
|
-
|
1035
|
-
# Place spines at center (optional styling preference)
|
1036
|
-
ax.spines['left'].set_position('zero')
|
1037
|
-
ax.spines['bottom'].set_position('zero')
|
1038
|
-
ax.spines['right'].set_color('none')
|
1039
|
-
ax.spines['top'].set_color('none')
|
1040
|
-
ax.xaxis.set_ticks_position('bottom')
|
1041
|
-
ax.yaxis.set_ticks_position('left')
|
1042
|
-
ax.set_aspect('equal', 'box')
|
1043
|
-
ax.grid(True)
|
1044
|
-
|
1045
|
-
# If requested, show the legend
|
1046
|
-
if show_legend:
|
1047
|
-
leg = ax.legend(
|
1048
|
-
loc='upper center',
|
1049
|
-
bbox_to_anchor=(0.5, -0.03),
|
1050
|
-
fancybox=True,
|
1051
|
-
shadow=True,
|
1052
|
-
ncol=1
|
1053
|
-
)
|
1054
|
-
plt.savefig(buffer, format='svg', bbox_inches='tight', bbox_extra_artists=[leg])
|
1055
|
-
else:
|
1056
|
-
plt.savefig(buffer, format='svg', bbox_inches='tight')
|
1057
|
-
|
1058
|
-
plt.close(fig)
|
1059
|
-
svg_string = buffer.getvalue().decode('utf-8')
|
1060
|
-
|
1061
|
-
# Optionally open/save the file
|
1062
|
-
handle_open_and_save(svg_string, open_file, save_path)
|
1063
|
-
|
1064
|
-
return svg_string
|