diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index d5af2742a..ef07d8364 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -348,6 +348,7 @@ System`Disk System`DiskBox System`DiskMatrix System`Dispatch +System`Distribute System`Divide System`DivideBy System`Divisible diff --git a/mathics/builtin/exp_structure/__init__.py b/mathics/builtin/exp_structure/__init__.py index 8ac7ae726..1489ee577 100644 --- a/mathics/builtin/exp_structure/__init__.py +++ b/mathics/builtin/exp_structure/__init__.py @@ -1,6 +1,8 @@ -""" -Expression Structure +r""" +Structural Operations on Expressions + +Here we have functions which work purely on \Mathics3 Expressions transforming them in some way. """ # This tells documentation how to sort this module -sort_order = "mathics.builtin.expression-structure" +sort_order = "mathics.builtin.structual-operations-on-expressions" diff --git a/mathics/builtin/exp_structure/miscelleneous.py b/mathics/builtin/exp_structure/miscelleneous.py new file mode 100644 index 000000000..fe274bc2b --- /dev/null +++ b/mathics/builtin/exp_structure/miscelleneous.py @@ -0,0 +1,157 @@ +""" +Miscellaneous Structural Operations on Expressions +""" + +from mathics.core.attributes import A_PROTECTED +from mathics.core.builtin import Builtin +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolIdentity +from mathics.eval.exp_structure import eval_Distribute +from mathics.eval.tensors import eval_Outer + + +class Distribute(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Distribute.html + +
+
'Distribute'[$expr$] +
distributes $expr$ over 'Plus' (addition). +
'Distribute'[$expr$, $operator$] +
distributes $expr$ over the specified $operator$. +
'Distribute'[$expr$, $operator$, $f$] +
applies $f$ to each component of the result. + + ##
'Distribute'[$expr$, $operator$, $f$, $gp$, $fp$] + ##
distributes $expr$ over $operator$, replacing outer function with $gp$ and inner function with $fp$. +
+ + Distribute multiplication over addition: + >> Distribute[a(b + c)] + = a b + a c + + >> Distribute[(a + b)(c + d)] + = a c + a d + b c + b d + + Using a custom target head: + >> Distribute[f[a + b, c], Plus] + = f[a, c] + f[b, c] + + Distribute can also work with lists: + >> Distribute[{a(b + c), d(e + f)}] + = {a b + a c, d e + d f} + + ## Applying a function to results: + ## >> Distribute[a(b + c), Plus, Square] + ## = Square[a b] + Square[a c] + + Special forms: + >> Distribute[f[g[a + b]]] + = f[g[a]] + f[g[b]] + + Distribute $f$ over $g$: + >> Distribute[f[g[a, b], g[c, d, e]], g] + = g[f[a, c], f[a, d], f[a, e], f[b, c], f[b, d], f[b, e]] + + ## Using a custom operator and functions: + ## >> Distribute[f[g[a, b], g[c, d, e]], g, f, gp, fp] + ## = gp[fp[a, c], fp[a, d], fp[a, e], fp[b, c], fp[b, d], fp[b, e]] + """ + + attributes = A_PROTECTED + + eval_error = Builtin.generic_argument_error + expected_args = range(1, 6) + + rules = { + "Distribute[expr_]": "Distribute[expr, Plus]", + "Distribute[expr_, operator_]": "Distribute[expr, operator, Identity]", + } + + summary_text = "distribute functions over a head" + + def eval(self, expr, operator, filt, evaluation: Evaluation): + "Distribute[expr_, operator_, filt_]" + + # Handle Identity filter + if filt is SymbolIdentity: + filt = None + + result = eval_Distribute(expr, operator, evaluation) + + if result is None: + return expr + + if filt: + return Expression(filt, result) + + return result + + # def eval_with_function_replacement( + # self, expr, operator, f, g, gp, fp, evaluation: Evaluation + # ): + # "Distribute[expr_, f_, g_, gp_, fp_]" + + # result = eval_Distribute_with_replacement(expr, f, g, gp, fp, evaluation) + + # if result is None: + # return expr + + # return result + + +class Outer(Builtin): + """ + :Outer product:https://en.wikipedia.org/wiki/Outer_product \ + (:WMA link: https://reference.wolfram.com/language/ref/Outer.html) + +
+
'Outer'[$f$, $x$, $y$] +
computes a generalised outer product of $x$ and $y$, using the function $f$ in place of multiplication. +
+ + >> Outer[f, {a, b}, {1, 2, 3}] + = {{f[a, 1], f[a, 2], f[a, 3]}, {f[b, 1], f[b, 2], f[b, 3]}} + + Outer product of two matrices: + >> Outer[Times, {{a, b}, {c, d}}, {{1, 2}, {3, 4}}] + = {{{{a, 2 a}, {3 a, 4 a}}, {{b, 2 b}, {3 b, 4 b}}}, {{{c, 2 c}, {3 c, 4 c}}, {{d, 2 d}, {3 d, 4 d}}}} + + Outer product of two sparse arrays: + >> Outer[Times, SparseArray[{{1, 2} -> a, {2, 1} -> b}], SparseArray[{{1, 2} -> c, {2, 1} -> d}]] + = SparseArray[Automatic, {2, 2, 2, 2}, 0, {{1, 2, 1, 2} ⇾ a c, {1, 2, 2, 1} ⇾ a d, {2, 1, 1, 2} ⇾ b c, {2, 1, 2, 1} ⇾ b d}] + + 'Outer' of multiple lists: + >> Outer[f, {a, b}, {x, y, z}, {1, 2}] + = {{{f[a, x, 1], f[a, x, 2]}, {f[a, y, 1], f[a, y, 2]}, {f[a, z, 1], f[a, z, 2]}}, {{f[b, x, 1], f[b, x, 2]}, {f[b, y, 1], f[b, y, 2]}, {f[b, z, 1], f[b, z, 2]}}} + + 'Outer' converts input sparse arrays to lists if f=!=Times, or if the input is a mixture of sparse arrays and lists: + >> Outer[f, SparseArray[{{1, 2} -> a, {2, 1} -> b}], SparseArray[{{1, 2} -> c, {2, 1} -> d}]] + = {{{{f[0, 0], f[0, c]}, {f[0, d], f[0, 0]}}, {{f[a, 0], f[a, c]}, {f[a, d], f[a, 0]}}}, {{{f[b, 0], f[b, c]}, {f[b, d], f[b, 0]}}, {{f[0, 0], f[0, c]}, {f[0, d], f[0, 0]}}}} + + >> Outer[Times, SparseArray[{{1, 2} -> a, {2, 1} -> b}], {c, d}] + = {{{0, 0}, {a c, a d}}, {{b c, b d}, {0, 0}}} + + Arrays can be ragged: + >> Outer[Times, {{1, 2}}, {{a, b}, {c, d, e}}] + = {{{{a, b}, {c, d, e}}, {{2 a, 2 b}, {2 c, 2 d, 2 e}}}} + + Word combinations: + >> Outer[StringJoin, {"", "re", "un"}, {"cover", "draw", "wind"}, {"", "ing", "s"}] // InputForm + = {{{"cover", "covering", "covers"}, {"draw", "drawing", "draws"}, {"wind", "winding", "winds"}}, {{"recover", "recovering", "recovers"}, {"redraw", "redrawing", "redraws"}, {"rewind", "rewinding", "rewinds"}}, {{"uncover", "uncovering", "uncovers"}, {"undraw", "undrawing", "undraws"}, {"unwind", "unwinding", "unwinds"}}} + + Compositions of trigonometric functions: + >> trigs = Outer[Composition, {Sin, Cos, Tan}, {ArcSin, ArcCos, ArcTan}] + = {{Composition[Sin, ArcSin], Composition[Sin, ArcCos], Composition[Sin, ArcTan]}, {Composition[Cos, ArcSin], Composition[Cos, ArcCos], Composition[Cos, ArcTan]}, {Composition[Tan, ArcSin], Composition[Tan, ArcCos], Composition[Tan, ArcTan]}} + Evaluate at 0: + >> Map[#[0] &, trigs, {2}] + = {{0, 1, 0}, {1, 0, 1}, {0, ComplexInfinity, 0}} + """ + + summary_text = "generalized outer product" + + def eval(self, f, lists, evaluation: Evaluation): + "Outer[f_, lists__]" + + return eval_Outer(f, lists, evaluation) diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index a20f9413f..36f3ab273 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -46,6 +46,7 @@ from mathics.core.systemsymbols import ( SymbolAssumptions, SymbolEqual, + SymbolIdentity, SymbolIndeterminate, SymbolLess, SymbolRule, @@ -550,7 +551,7 @@ class Collect(Builtin): def eval_var_filter(self, expr, varlist, filt, evaluation): """Collect[expr_, varlist_, filt_]""" - if filt is Symbol("Identity"): + if filt is SymbolIdentity: filt = None if isinstance(varlist, Symbol): var_exprs = [varlist] @@ -845,9 +846,9 @@ def eval(self, expr, evaluation: Evaluation, options: dict): return expand_polynomial(expr, False, True, **kwargs) -## Our expand_polynomial routine and SymPy's do not match -## what WMA is doing. Failing a good reason to get this working, -## I, rocky, do not thing it is worth the effort. +# Our expand_polynomial routine and SymPy's do not match +# what WMA is doing. Failing a good reason to get this working, +# I, rocky, do not thing it is worth the effort. # # class ExpandNumerator(_Expand): # """ diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py index b06ffc77f..aaa3a4455 100644 --- a/mathics/builtin/tensors.py +++ b/mathics/builtin/tensors.py @@ -26,7 +26,6 @@ from mathics.eval.tensors import ( eval_Inner, eval_LeviCivitaTensor, - eval_Outer, eval_Transpose, get_dimensions, ) @@ -168,62 +167,6 @@ def eval(self, f, list1, list2, g, evaluation: Evaluation): return eval_Inner(f, list1, list2, g, evaluation) -class Outer(Builtin): - """ - :Outer product:https://en.wikipedia.org/wiki/Outer_product \ - (:WMA link: https://reference.wolfram.com/language/ref/Outer.html) - -
-
'Outer'[$f$, $x$, $y$] -
computes a generalised outer product of $x$ and $y$, using the function $f$ in place of multiplication. -
- - >> Outer[f, {a, b}, {1, 2, 3}] - = {{f[a, 1], f[a, 2], f[a, 3]}, {f[b, 1], f[b, 2], f[b, 3]}} - - Outer product of two matrices: - >> Outer[Times, {{a, b}, {c, d}}, {{1, 2}, {3, 4}}] - = {{{{a, 2 a}, {3 a, 4 a}}, {{b, 2 b}, {3 b, 4 b}}}, {{{c, 2 c}, {3 c, 4 c}}, {{d, 2 d}, {3 d, 4 d}}}} - - Outer product of two sparse arrays: - >> Outer[Times, SparseArray[{{1, 2} -> a, {2, 1} -> b}], SparseArray[{{1, 2} -> c, {2, 1} -> d}]] - = SparseArray[Automatic, {2, 2, 2, 2}, 0, {{1, 2, 1, 2} ⇾ a c, {1, 2, 2, 1} ⇾ a d, {2, 1, 1, 2} ⇾ b c, {2, 1, 2, 1} ⇾ b d}] - - 'Outer' of multiple lists: - >> Outer[f, {a, b}, {x, y, z}, {1, 2}] - = {{{f[a, x, 1], f[a, x, 2]}, {f[a, y, 1], f[a, y, 2]}, {f[a, z, 1], f[a, z, 2]}}, {{f[b, x, 1], f[b, x, 2]}, {f[b, y, 1], f[b, y, 2]}, {f[b, z, 1], f[b, z, 2]}}} - - 'Outer' converts input sparse arrays to lists if f=!=Times, or if the input is a mixture of sparse arrays and lists: - >> Outer[f, SparseArray[{{1, 2} -> a, {2, 1} -> b}], SparseArray[{{1, 2} -> c, {2, 1} -> d}]] - = {{{{f[0, 0], f[0, c]}, {f[0, d], f[0, 0]}}, {{f[a, 0], f[a, c]}, {f[a, d], f[a, 0]}}}, {{{f[b, 0], f[b, c]}, {f[b, d], f[b, 0]}}, {{f[0, 0], f[0, c]}, {f[0, d], f[0, 0]}}}} - - >> Outer[Times, SparseArray[{{1, 2} -> a, {2, 1} -> b}], {c, d}] - = {{{0, 0}, {a c, a d}}, {{b c, b d}, {0, 0}}} - - Arrays can be ragged: - >> Outer[Times, {{1, 2}}, {{a, b}, {c, d, e}}] - = {{{{a, b}, {c, d, e}}, {{2 a, 2 b}, {2 c, 2 d, 2 e}}}} - - Word combinations: - >> Outer[StringJoin, {"", "re", "un"}, {"cover", "draw", "wind"}, {"", "ing", "s"}] // InputForm - = {{{"cover", "covering", "covers"}, {"draw", "drawing", "draws"}, {"wind", "winding", "winds"}}, {{"recover", "recovering", "recovers"}, {"redraw", "redrawing", "redraws"}, {"rewind", "rewinding", "rewinds"}}, {{"uncover", "uncovering", "uncovers"}, {"undraw", "undrawing", "undraws"}, {"unwind", "unwinding", "unwinds"}}} - - Compositions of trigonometric functions: - >> trigs = Outer[Composition, {Sin, Cos, Tan}, {ArcSin, ArcCos, ArcTan}] - = {{Composition[Sin, ArcSin], Composition[Sin, ArcCos], Composition[Sin, ArcTan]}, {Composition[Cos, ArcSin], Composition[Cos, ArcCos], Composition[Cos, ArcTan]}, {Composition[Tan, ArcSin], Composition[Tan, ArcCos], Composition[Tan, ArcTan]}} - Evaluate at 0: - >> Map[#[0] &, trigs, {2}] - = {{0, 1, 0}, {1, 0, 1}, {0, ComplexInfinity, 0}} - """ - - summary_text = "generalized outer product" - - def eval(self, f, lists, evaluation: Evaluation): - "Outer[f_, lists__]" - - return eval_Outer(f, lists, evaluation) - - class RotationTransform(Builtin): """ :WMA link: https://reference.wolfram.com/language/ref/RotationTransform.html diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index eea8861ee..f6dfd1d96 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -163,6 +163,7 @@ SymbolHoldPattern = Symbol("System`HoldPattern") SymbolHue = Symbol("System`Hue") SymbolI = Symbol("System`I") +SymbolIdentity = Symbol("System`Identity") SymbolIf = Symbol("System`If") SymbolIm = Symbol("System`Im") SymbolImage = Symbol("System`Image") diff --git a/mathics/eval/exp_structure/__init__.py b/mathics/eval/exp_structure/__init__.py new file mode 100644 index 000000000..b05a03337 --- /dev/null +++ b/mathics/eval/exp_structure/__init__.py @@ -0,0 +1,13 @@ +"Evaluation methods for builtin functions of mathics.builtin.exp_structure" + +from mathics.eval.exp_structure.distribute import eval_Distribute + +# from mathics.eval.exp_structure.outer import eval_Outer + +# TODO: add FlattenAt +# from mathics.eval.exp_structure.flattenAt import eval_At +__all__ = [ + "eval_Distribute", + # "eval_FlattenAt", + # "eval_Outer", +] diff --git a/mathics/eval/exp_structure/distribute.py b/mathics/eval/exp_structure/distribute.py new file mode 100644 index 000000000..269bc30f1 --- /dev/null +++ b/mathics/eval/exp_structure/distribute.py @@ -0,0 +1,317 @@ +""" +Evaluation routines and helper functions for Distribute[]. + +Implementation note: Distribute can be handles purely within the confines of +Expression manipulation. + +SymPy or numeric functions are not need here. +""" + +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol + + +def contains_operator_symbol(expr, operator_symbol): + """ + Check if expr contains operator_symbol anywhere. + """ + if not isinstance(expr, Expression): + return False + + # Check if this expression's head is the target + if is_operator_symbol(expr, operator_symbol): + return True + + # Recursively check sub-expressions + for elem in expr.elements: + if contains_operator_symbol(elem, operator_symbol): + return True + + return False + + +def eval_Distribute(expr, operator_symbol, evaluation): + """ + Recursively distribute operator_symbol over the expression. + Returns None if no distribution was performed. + """ + if not isinstance(expr, Expression): + return None + + # Handle ListExpression: apply distribution to each element. + if isinstance(expr, ListExpression): + distributed_elements = [] + for element in expr.elements: + distributed = eval_Distribute(element, operator_symbol, evaluation) + if distributed is not None: + distributed_elements.append(distributed) + else: + distributed_elements.append(element) + return ListExpression(*distributed_elements) + + head = expr.get_head() + elements = expr.elements + + # Find the first element containing the operator_symbol. + operator_position = None + for i, elem in enumerate(elements): + if contains_operator_symbol(elem, operator_symbol): + operator_position = i + break + + if operator_position is None: + # No element contains operator_symbol. + return None + + # Get the element at the target position + target_elem = elements[operator_position] + + # If the element is the operator symbol (e.g., g in f[g[...], g[...]]), + # distribute over it by distributing the outer function's arguments. + if is_operator_symbol(target_elem, operator_symbol): + # Get all components of the operator symbol + target_components = target_elem.elements + + # Create new expressions by replacing the operator position with each component. + result_parts = [] + for component in target_components: + # Replace the operator position with this component. + new_elements = list(elements) + new_elements[operator_position] = component + new_expr = Expression(head, *new_elements) + + # Recursively distribute in the new Expression. + recursive_result = eval_Distribute(new_expr, operator_symbol, evaluation) + if recursive_result is not None: + result_parts.append(recursive_result) + else: + result_parts.append(new_expr) + + # If we have multiple arguments containing the operator symbol at different positions, + # we need to create a cartesian product. Check if there are more positions with operator_symbol. + other_operator_positions = [] + for i, elem in enumerate(elements): + if i != operator_position and contains_operator_symbol( + elem, operator_symbol + ): + other_operator_positions.append(i) + + # If there are other arguments with the operator symbol, we need to distribute across all of them. + if other_operator_positions: + return distribute_across_multiple_positions( + head, elements, operator_symbol, evaluation + ) + + # Return the combination using the operator symbol. + return Expression(operator_symbol, *result_parts) + + # If the element contains but is not the operator symbol, recurse into it. + else: + recursive_result = eval_Distribute(target_elem, operator_symbol, evaluation) + if recursive_result is not None: + new_elements = list(elements) + new_elements[operator_position] = recursive_result + new_expr = Expression(head, *new_elements) + + # Try to distribute the modified Expression again. + second_result = eval_Distribute(new_expr, operator_symbol, evaluation) + if second_result is not None: + return second_result + return new_expr + + return None + + +# def eval_Distribute_with_replacement(expr, f_symbol, g_symbol, fp_symbol=None, gp_symbol=None, evaluation=None): +# """ +# Recursively distribute operator_symbol over the expression. +# Optionally replace outer function head with gp_symbol and inner with fp_symbol. +# Returns None if no distribution was performed. +# """ +# if not isinstance(expr, Expression): +# return None + +# # Default: use operator_symbol for both outer and inner if not specified +# if fp_symbol is None: +# fp_symbol = f_symbol +# if gp_symbol is None: +# gp_symbol = g_symbol + +# # Handle ListExpression: apply distribution to each element. +# if isinstance(expr, ListExpression): +# distributed_elements = [] +# for element in expr.elements: +# distributed = eval_Distribute_with_replacement(element, f_symbol, g_symbol, fp_symbol, gp_symbol, evaluation) +# if distributed is not None: +# distributed_elements.append(distributed) +# else: +# distributed_elements.append(element) +# return ListExpression(*distributed_elements) +# elif isinstance(expr, Expression): +# head = expr.get_head() +# element = expr.elements[0] +# distributed = eval_Distribute_with_replacement(expr, f_symbol, g_symbol, fp_symbol, gp_symbol, evaluation) +# if distributed is None: +# return None +# return Expression(head, distributed) + +# head = expr.get_head() +# elements = expr.elements + +# # Find the first element containing the operator_symbol. +# operator_position = None +# for i, elem in enumerate(elements): +# if contains_operator_symbol(elem, f_symbol): +# operator_position = i +# break + +# if operator_position is None: +# # No element contains operator_symbol +# return None + +# # Get the element at the target position +# target_elem = elements[operator_position] + +# # If the element is the operator symbol (e.g., g in f[g[...], g[...]]), +# # distribute over it by distributing the outer function's arguments. +# if is_operator_symbol(target_elem, f_symbol): +# # Get all components of the operator symbol +# target_components = target_elem.elements + +# # Create new expressions by replacing the operator position with each component. +# result_parts = [] +# for component in target_components: +# # Replace the operator position with this component. +# new_elements = list(elements) +# new_elements[operator_position] = component +# new_expr = Expression(head, *new_elements) + +# # Recursively distribute in the new Expression. +# recursive_result = eval_Distribute_with_replacement(new_expr, f_symbol, g_symbol, gp_symbol, fp_symbol, evaluation) +# if recursive_result is not None: +# result_parts.append(recursive_result) +# else: +# result_parts.append(new_expr) + +# # If we have multiple arguments containing the operator symbol at different positions, +# # create a cartesian product. Check if there are more positions with operator_symbol. +# other_operator_positions = [] +# for i, elem in enumerate(elements): +# if i != operator_position and contains_operator_symbol(elem, f_symbol): +# other_operator_positions.append(i) + +# # If there are other arguments with the operator symbol, distribute across all of them. +# if other_operator_positions: +# return distribute_across_multiple_positions_with_replacement( +# head, elements, f_symbol, gp_symbol, fp_symbol, evaluation +# ) + +# # Return the combination: use gp_symbol for outer, fp_symbol for inner +# # The inner expression uses fp_symbol, outer uses gp_symbol +# inner_expressions = [Expression(fp_symbol, *rp.elements) if isinstance(rp, Expression) and rp.get_head() == head else rp for rp in result_parts] +# return Expression(gp_symbol, *inner_expressions) + +# # If the element contains but is not the operator symbol, recurse into it. +# else: +# recursive_result = eval_Distribute_with_replacement(target_elem, f_symbol, gp_symbol, fp_symbol, evaluation) +# if recursive_result is not None: +# new_elements = list(elements) +# new_elements[operator_position] = recursive_result +# new_expr = Expression(head, *new_elements) + +# # Try to distribute the modified Expression again. +# second_result = eval_Distribute_with_replacement(new_expr, f_symbol, gp_symbol, fp_symbol, evaluation) +# if second_result is not None: +# return second_result +# return new_expr + +# return None + + +# def distribute_across_multiple_positions_with_replacement(head, elements, f_symbol, gp_symbol, fp_symbol, evaluation): +# """ +# When multiple arguments contain the f_symbol, distribute across all of them. +# This creates a cartesian product of the components. +# """ +# # Find all positions with the f_symbol +# operator_positions = [] +# for i, elem in enumerate(elements): +# if contains_operator_symbol(elem, f_symbol): +# operator_positions.append(i) + +# # Extract the components for each position. +# position_components = [] +# for pos in operator_positions: +# elem = elements[pos] +# if is_operator_symbol(elem, f_symbol): +# position_components.append((pos, elem.elements)) +# else: +# position_components.append((pos, [elem])) + +# # Generate cartesian product of all components. +# result_parts = [] + +# # Return the result wrapped in gp_symbol +# return Expression(gp_symbol, *result_parts) + + +def distribute_across_multiple_positions(head, elements, operator_symbol, evaluation): + """ + When multiple arguments contain the operator_symbol, distribute across all of them. + This creates a cartesian product of the components. + """ + # Find all positions with the operator_symbol. + operator_positions = [] + for i, elem in enumerate(elements): + if contains_operator_symbol(elem, operator_symbol): + operator_positions.append(i) + + # Extract the components for each position. + position_components = [] + for pos in operator_positions: + elem = elements[pos] + if is_operator_symbol(elem, operator_symbol): + position_components.append((pos, elem.elements)) + else: + # Should not happen, but handle gracefully + position_components.append((pos, [elem])) + + # Generate cartesian product of all components. + result_parts = [] + + def cartesian_product_helper(index, current_elements): + if index == len(position_components): + # We've filled all positions, create the expression + new_expr = Expression(head, *current_elements) + result_parts.append(new_expr) + return + + pos, components = position_components[index] + for component in components: + new_elements = list(current_elements) + new_elements[pos] = component + cartesian_product_helper(index + 1, new_elements) + + cartesian_product_helper(0, list(elements)) + + # Return the result wrapped in the operator_symbol + return Expression(operator_symbol, *result_parts) + + +def is_operator_symbol(expr, operator_symbol): + """ + Check if expr's head is exactly the operator_symbol. + """ + if not isinstance(expr, Expression): + return False + + expr_head = expr.get_head() + + if isinstance(operator_symbol, Symbol): + return ( + isinstance(expr_head, Symbol) + and expr_head.get_name() == operator_symbol.get_name() + ) + + return expr_head == operator_symbol diff --git a/test/builtin/numbers/test_algebra.py b/test/builtin/numbers/test_algebra.py index 6620da3e5..19c3bd4e1 100644 --- a/test/builtin/numbers/test_algebra.py +++ b/test/builtin/numbers/test_algebra.py @@ -405,7 +405,7 @@ def test_fullsimplify(): [ ("Attributes[f] = {HoldAll}; Apart[f[x + x]]", None, "f[x + x]", None), ("Attributes[f] = {}; Apart[f[x + x]]", None, "f[2 x]", None), - ## Errors: + # Errors: ( "Coefficient[x + y + 3]", ("Coefficient called with 1 argument; 2 or 3 arguments are expected.",), @@ -469,7 +469,7 @@ def test_fullsimplify(): "24 x / (5 + 3 x + x ^ 2) ^ 3 + 8 x ^ 2 / (5 + 3 x + x ^ 2) ^ 3 + 18 / (5 + 3 x + x ^ 2) ^ 3", None, ), - ## Modulus option + # Modulus option ( "ExpandDenominator[1 / (x + y)^3, Modulus -> 3]", None, @@ -542,21 +542,21 @@ def test_fullsimplify(): "True", None, ), - ## TODO: MMA and Sympy handle these cases differently - ## #> PolynomialQ[x^(1/2) + 6xyz] - ## : No variable is not supported in PolynomialQ. - ## = True - ## #> PolynomialQ[x^(1/2) + 6xyz, {}] - ## : No variable is not supported in PolynomialQ. - ## = True - ## #> PolynomialQ[x^3 - 2 x/y + 3xz] - ## : No variable is not supported in PolynomialQ. - ## = False - ## #> PolynomialQ[x^3 - 2 x/y + 3xz, {}] - ## : No variable is not supported in PolynomialQ. - ## = False + # TODO: MMA and Sympy handle these cases differently + # #> PolynomialQ[x^(1/2) + 6xyz] + # : No variable is not supported in PolynomialQ. + # = True + # #> PolynomialQ[x^(1/2) + 6xyz, {}] + # : No variable is not supported in PolynomialQ. + # = True + # #> PolynomialQ[x^3 - 2 x/y + 3xz] + # : No variable is not supported in PolynomialQ. + # = False + # #> PolynomialQ[x^3 - 2 x/y + 3xz, {}] + # : No variable is not supported in PolynomialQ. + # = False ("f[x]/x+f[x]/x^2//Together", None, "f[x] (1 + x) / x ^ 2", None), - ## failing test case from MMA docs + # failing test case from MMA docs ("Variables[E^x]", None, "{}", None), ], ) @@ -616,6 +616,10 @@ def test_integer(str_expr, msgs, str_expected, fail_msg): "Exponent", "2 or 3 arguments are", ), + ( + "Distribute", + "between 1 and 5 arguments are", + ), ], ) def test_arg_count_errors(function_name, msg_fragment):