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.
@@ -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
+