rgwfuncs 0.0.100__py3-none-any.whl → 0.0.103__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/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