From 6563e48bdbc1bfb6298dbe1dd23b0bb38c8fde6b Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 29 Jun 2026 17:51:43 -0400 Subject: [PATCH 1/5] Implement Distribute --- SYMBOLS_MANIFEST.txt | 1 + mathics/builtin/numbers/algebra.py | 70 ++++++++++++- mathics/core/systemsymbols.py | 1 + mathics/eval/numbers/algebra/distribute.py | 108 +++++++++++++++++++++ test/builtin/numbers/test_algebra.py | 36 ++++--- 5 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 mathics/eval/numbers/algebra/distribute.py 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/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index a20f9413f..15a5857df 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, @@ -53,6 +54,7 @@ SymbolTable, ) from mathics.eval.list.eol import eval_Part +from mathics.eval.numbers.algebra.distribute import eval_Distribute from mathics.eval.numbers.algebra.fraction import eval_Denominator, eval_Numerator from mathics.eval.numbers.algebra.options import AlgebraicOptions from mathics.eval.numbers.algebra.polynomial import ( @@ -550,7 +552,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] @@ -664,6 +666,72 @@ def convert_options(self, options: dict, evaluation: Evaluation): return {"modulus": py_modulus, "trig": py_trig} +class Distribute(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Distribute.html + +
+
'Distribute'[$expr$] +
distributes $expr$ over 'Plus' (addition). +
'Distribute'[$expr$, $targetHead$] +
distributes $expr$ over the specified $targetHead$. +
'Distribute'[$expr$, $targetHead$, $f$] +
applies $f$ to each component of the result. +
+ + 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]] + """ + + 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, filter, evaluation: Evaluation): + "Distribute[expr_, operator_, filter_]" + + # Handle Identity filter + if filter is SymbolIdentity: + filter = None + + result = eval_Distribute(expr, operator, evaluation) + + if result is None: + return expr + + if filter: + return Expression(filter, result) + + return result + + class Expand(_Expand): """ 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/numbers/algebra/distribute.py b/mathics/eval/numbers/algebra/distribute.py new file mode 100644 index 000000000..f06cde3d6 --- /dev/null +++ b/mathics/eval/numbers/algebra/distribute.py @@ -0,0 +1,108 @@ +""" +Evaluation routines for Distribute[] +""" + +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol + + +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 + + 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, but check if head itself needs distribution. + return None + + # Get the element at the target position + target_elem = elements[operator_position] + + # If the element is the operator symbol (e.g., Plus), distribute over it. + 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) + + # 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 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 + + +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 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): From 7093ccc39107012c5cb2c5688235993ce00460b1 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 29 Jun 2026 18:12:54 -0400 Subject: [PATCH 2/5] Handle Distribute with lists --- mathics/builtin/numbers/algebra.py | 6 +++--- mathics/eval/numbers/algebra/distribute.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 15a5857df..26e997408 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -689,9 +689,9 @@ class Distribute(Builtin): >> 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} + 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] diff --git a/mathics/eval/numbers/algebra/distribute.py b/mathics/eval/numbers/algebra/distribute.py index f06cde3d6..560af0dd5 100644 --- a/mathics/eval/numbers/algebra/distribute.py +++ b/mathics/eval/numbers/algebra/distribute.py @@ -3,6 +3,7 @@ """ from mathics.core.expression import Expression +from mathics.core.list import ListExpression from mathics.core.symbols import Symbol @@ -14,6 +15,17 @@ def eval_Distribute(expr, operator_symbol, evaluation): 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 @@ -25,7 +37,7 @@ def eval_Distribute(expr, operator_symbol, evaluation): break if operator_position is None: - # No element contains operator_symbol, but check if head itself needs distribution. + # No element contains operator_symbol. return None # Get the element at the target position @@ -62,7 +74,7 @@ def eval_Distribute(expr, operator_symbol, evaluation): new_elements[operator_position] = recursive_result new_expr = Expression(head, *new_elements) - # Try to distribute the modified Expression again + # 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 From 3637b70187fcc9a237bc12c68b1e99a8a1854428 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 29 Jun 2026 18:23:46 -0400 Subject: [PATCH 3/5] Correct distributing one function over another. When distributing f over g, distribute the arguments of f across the elements of the g sub-expressions. --- mathics/builtin/numbers/algebra.py | 4 ++ mathics/eval/numbers/algebra/distribute.py | 61 +++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 26e997408..7dbd0b1fa 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -700,6 +700,10 @@ class Distribute(Builtin): 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]] """ attributes = A_PROTECTED diff --git a/mathics/eval/numbers/algebra/distribute.py b/mathics/eval/numbers/algebra/distribute.py index 560af0dd5..285d07107 100644 --- a/mathics/eval/numbers/algebra/distribute.py +++ b/mathics/eval/numbers/algebra/distribute.py @@ -43,7 +43,8 @@ def eval_Distribute(expr, operator_symbol, evaluation): # Get the element at the target position target_elem = elements[operator_position] - # If the element is the operator symbol (e.g., Plus), distribute over it. + # 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 @@ -63,6 +64,21 @@ def eval_Distribute(expr, operator_symbol, evaluation): 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) @@ -83,6 +99,49 @@ def eval_Distribute(expr, operator_symbol, evaluation): return None +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. From 10760d5c0d269e17e1f4d29c2e99f7905e3ca76d Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 29 Jun 2026 19:48:56 -0400 Subject: [PATCH 4/5] Stub out 5 argument Distribute. It is not working yet though. --- mathics/builtin/numbers/algebra.py | 54 +++++-- mathics/eval/numbers/algebra/distribute.py | 177 ++++++++++++++++++--- 2 files changed, 193 insertions(+), 38 deletions(-) diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 7dbd0b1fa..6d8828073 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -54,7 +54,9 @@ SymbolTable, ) from mathics.eval.list.eol import eval_Part -from mathics.eval.numbers.algebra.distribute import eval_Distribute +from mathics.eval.numbers.algebra.distribute import ( # eval_Distribute_with_replacement, + eval_Distribute, +) from mathics.eval.numbers.algebra.fraction import eval_Denominator, eval_Numerator from mathics.eval.numbers.algebra.options import AlgebraicOptions from mathics.eval.numbers.algebra.polynomial import ( @@ -673,15 +675,19 @@ class Distribute(Builtin):
'Distribute'[$expr$]
distributes $expr$ over 'Plus' (addition). -
'Distribute'[$expr$, $targetHead$] -
distributes $expr$ over the specified $targetHead$. -
'Distribute'[$expr$, $targetHead$, $f$] +
'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 @@ -693,9 +699,9 @@ class Distribute(Builtin): >> 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] + ## Applying a function to results: + ## >> Distribute[a(b + c), Plus, Square] + ## = Square[a b] + Square[a c] Special forms: >> Distribute[f[g[a + b]]] @@ -704,6 +710,10 @@ class Distribute(Builtin): 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 @@ -718,23 +728,35 @@ class Distribute(Builtin): summary_text = "distribute functions over a head" - def eval(self, expr, operator, filter, evaluation: Evaluation): - "Distribute[expr_, operator_, filter_]" + def eval(self, expr, operator, filt, evaluation: Evaluation): + "Distribute[expr_, operator_, filt_]" # Handle Identity filter - if filter is SymbolIdentity: - filter = None + if filt is SymbolIdentity: + filt = None result = eval_Distribute(expr, operator, evaluation) if result is None: return expr - if filter: - return Expression(filter, result) + 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 Expand(_Expand): """ @@ -917,9 +939,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/eval/numbers/algebra/distribute.py b/mathics/eval/numbers/algebra/distribute.py index 285d07107..45d37cced 100644 --- a/mathics/eval/numbers/algebra/distribute.py +++ b/mathics/eval/numbers/algebra/distribute.py @@ -7,6 +7,25 @@ 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. @@ -99,18 +118,151 @@ def eval_Distribute(expr, operator_symbol, evaluation): 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 + # 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 + # Extract the components for each position. position_components = [] for pos in operator_positions: elem = elements[pos] @@ -120,7 +272,7 @@ def distribute_across_multiple_positions(head, elements, operator_symbol, evalua # Should not happen, but handle gracefully position_components.append((pos, [elem])) - # Generate cartesian product of all components + # Generate cartesian product of all components. result_parts = [] def cartesian_product_helper(index, current_elements): @@ -158,22 +310,3 @@ def is_operator_symbol(expr, operator_symbol): ) return expr_head == operator_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 From 0674375d5553f02708da6ea88c21a23eb5acbb07 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 30 Jun 2026 10:41:34 -0400 Subject: [PATCH 5/5] Move Distribute to Structural Operations on Expressions --- mathics/builtin/exp_structure/__init__.py | 8 +- .../builtin/exp_structure/miscelleneous.py | 157 ++++++++++++++++++ mathics/builtin/numbers/algebra.py | 93 ----------- mathics/builtin/tensors.py | 57 ------- mathics/eval/exp_structure/__init__.py | 13 ++ .../algebra => exp_structure}/distribute.py | 7 +- 6 files changed, 181 insertions(+), 154 deletions(-) create mode 100644 mathics/builtin/exp_structure/miscelleneous.py create mode 100644 mathics/eval/exp_structure/__init__.py rename mathics/eval/{numbers/algebra => exp_structure}/distribute.py (98%) 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 6d8828073..36f3ab273 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -54,9 +54,6 @@ SymbolTable, ) from mathics.eval.list.eol import eval_Part -from mathics.eval.numbers.algebra.distribute import ( # eval_Distribute_with_replacement, - eval_Distribute, -) from mathics.eval.numbers.algebra.fraction import eval_Denominator, eval_Numerator from mathics.eval.numbers.algebra.options import AlgebraicOptions from mathics.eval.numbers.algebra.polynomial import ( @@ -668,96 +665,6 @@ def convert_options(self, options: dict, evaluation: Evaluation): return {"modulus": py_modulus, "trig": py_trig} -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 Expand(_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/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/numbers/algebra/distribute.py b/mathics/eval/exp_structure/distribute.py similarity index 98% rename from mathics/eval/numbers/algebra/distribute.py rename to mathics/eval/exp_structure/distribute.py index 45d37cced..269bc30f1 100644 --- a/mathics/eval/numbers/algebra/distribute.py +++ b/mathics/eval/exp_structure/distribute.py @@ -1,5 +1,10 @@ """ -Evaluation routines for Distribute[] +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