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):