diff --git a/CHANGES.rst b/CHANGES.rst index 76f29ff356..aeeb5040bd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ New variables and builtins ++++++++++++++++++++++++++ * ``Arg`` +* ``CoefficientArrays`` and ``Collect`` (#1174, #1194) * ``Dispatch`` * ``FullSimplify`` * ``LetterNumber`` #1298. The ``alphabet`` parameter supports only a minimal number of languages. @@ -29,6 +30,8 @@ Enhancements * ``Attributes`` accepts a string parameter. * ``ColorNegate`` for colors is supported. * ``D`` and ``Derivative`` improvements. +* ``Expand`` and ``ExpandAll`` now support a second parameter ``patt`` (#1301) +* ``Expand`` and ``ExpandAll`` works with hyperbolic functions (`Sinh`, `Cosh`, `Tanh`, `Coth`) * ``FileNames`` returns a sorted list (#1250). * ``FindRoot`` now receives several optional parameters like ``Method`` and ``MaxIterations``. * ``FixedPoint`` now supports the ``SameTest`` option. @@ -39,11 +42,10 @@ Enhancements * ``StringTake`` now accepts form containing a list of strings and specification (#1297). * ``Table`` [*expr*, *n*] is supported. * ``ToString`` accepts an optional *form* parameter. -* ``ToExpression`` handles multi-line string input +* ``ToExpression`` handles multi-line string input. * The implementation of Streams was redone. - Bug fixes +++++++++ diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 0bc47de52d..3c3cfe46d0 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -8,6 +8,7 @@ """ from mathics.version import __version__ # noqa used in loading to check consistency. + import sympy import mpmath from functools import lru_cache diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index c74064ecdf..87b6b3cc79 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -721,11 +721,17 @@ def boxes_to_tex(self, leaves, **options) -> str: class PatternError(Exception): def __init__(self, name, tag, *args): super().__init__() + self.name = name + self.tag = tag + self.args = args class PatternArgumentError(PatternError): def __init__(self, name, count, expected): super().__init__(None, None) + self.name = name + self.count = count + self.expected = expected class PatternObject(InstanceableBuiltin, Pattern): diff --git a/mathics/builtin/inference.py b/mathics/builtin/inference.py index 5dda95db1c..94c296203d 100644 --- a/mathics/builtin/inference.py +++ b/mathics/builtin/inference.py @@ -1,7 +1,9 @@ +# -*- coding: utf-8 -*- + from mathics.version import __version__ # noqa used in loading to check consistency. + from mathics.core.expression import ( Expression, - Symbol, SymbolTrue, SymbolFalse, ) @@ -14,8 +16,103 @@ # TODO: Extend these rules? +def debug_logical_expr(pref, expr, evaluation): + pass + # return + # print(pref , expr) #expr.format(evaluation,"OutputForm").boxes_to_text(evaluation=evaluation)) + + +logical_algebraic_rules_spec = { + # Inequality rules + "Unequal[a_, b_]": "Not[Equal[a, b]]", + "Greater[a_, b_]": "Less[b, a]", + "GreaterEqual[a_, b_]": "Not[Less[a, b]]", + "LessEqual[a_, b_]": "Not[Less[b, a]]", + "PositiveQ[a_]": "Less[0, a]", + "NegativeQ[a_]": "Less[a, 0]", + # Logical basic reductions + "Or[q_, Not[q_]]": "True", + "Or[q_,]": "q", + "Or[q_, q_]": "q", + "Or[pred1___, q_, pred2___, q_, pred3___]": "Or[pred1, q, pred2, pred3]", + # TODO: Logical operations should sort the leaves... + "Or[Not[q_], q_]": "True", + "Or[pred1___, q_, pred2___, Not[q_], pred3___]": "Or[pred1, pred2, pred3]", + "Or[pred1___, Not[q_], pred2___, q_, pred3___]": "Or[pred1, pred2, pred3]", + "And[q_,q_]": "q", + "And[q_, Not[q_]]": "False", + "And[Not[q_],q_]": "False", + "And[pred1___, q_, pred2___, Not[q_], pred3___]": "False", + "And[pred1___, Not[q_], pred2___, q_, pred3___]": "False", + # Logical reductions involving equalities + "Or[pred1___, a_==b_, pred2___ , b_==a_, pred3___]": "Or[pred1, a==b, pred2, pred3]", + "And[pred1___, a_==b_, pred2___ , b_==a_, pred3___]": "And[pred1, a==b, pred2, pred3]", + "Or[pred1___, a_==b_, pred2___ , Not[b_==a_], pred3___]": "Or[pred1, pred2, pred3]", + "And[pred1___, a_==b_, pred2___ , Not[b_==a_], pred3___]": "False", + "Xor[q_, Not[q_]]": "True", + "Xor[a_==b_, Not[b_==a_]]": "True", + # Logical reductions involving inequalities + "Or[a_b)", + "Or[b_==a_, a_b)", + "And[a_=b", + "Not[a_==b_]": "a!=b", +} + -def get_assumptions_list(evaluation): +logical_algebraic_rules = None +remove_not_rules = None + + +def ensure_logical_algebraic_rules(): + global logical_algebraic_rules + global remove_not_rules + if logical_algebraic_rules is None: + logical_algebraic_rules = [] + for pattern, replace in logical_algebraic_rules_spec.items(): + pattern = parse_builtin_rule(pattern, SystemDefinitions()) + logical_algebraic_rules.append( + Rule(pattern, parse_builtin_rule(replace), system=True) + ) + remove_not_rules = [] + for pattern, replace in remove_not_rules_spec.items(): + pattern = parse_builtin_rule(pattern, SystemDefinitions()) + remove_not_rules.append( + Rule(pattern, parse_builtin_rule(replace), system=True) + ) + return + + +def remove_nots_when_unnecesary(pred, evaluation): + global remove_not_rules + cc = True + while cc: + pred, cc = pred.apply_rules(remove_not_rules, evaluation) + debug_logical_expr("-> ", pred, evaluation) + if pred.is_true() or pred == SymbolFalse: + return pred + return pred + + +def get_assumptions_list(evaluation): assumptions = None assumptions_def = evaluation.definitions.get_definition( "System`$Assumptions", only_if_exists=True @@ -35,23 +132,251 @@ def get_assumptions_list(evaluation): return assumptions +def remove_duplicated_assumptions(assumptions_list, evaluation): + if len(assumptions_list) == 0: + return assumptions_list + assumptions_list = sorted(assumptions_list) + unique_assumptions = [assumptions_list[0]] + for i, assumption in enumerate(assumptions_list): + if not (assumption == unique_assumptions[-1]): + unique_assumptions.append(assumption) + return unique_assumptions -def evaluate_predicate(pred, evaluation): - if pred.has_form(("List", "Sequence"), None): - return Expression(pred._head, *[evaluate_predicate(subp, evaluation) for subp in pred._leaves] ) - assumptions = get_assumptions_list(evaluation) +def logical_expand_assumptions(assumptions_list, evaluation): + new_assumptions_list = [] + changed = False + for assumption in assumptions_list: + if assumption.is_symbol(): + if assumption.is_true(): + changed = True + continue + if assumption == SymbolFalse: + evaluation.message("Assumption", "faas") + changed = True + continue + if assumption.is_numeric(): + evaluation.message("Assumption", "baas") + changed = True + continue + new_assumptions_list.append(assumption) + continue + if assumption.has_form("And", None): + changed = True + for leaf in assumption.leaves: + new_assumptions_list.append(leaf) + continue + if assumption.has_form("Not", 1): + sentence = assumption._leaves[0] + if sentence.has_form("Or", None): + changed = True + for leaf in sentence._leaves: + new_assumptions_list.append(Expression("Not", leaf)) + continue + if sentence.has_form("And", None): + leaves = (Expression("Not", leaf) for leaf in sentence._leaves) + new_assumptions_list.append(Expression("Or", *leaves)) + continue + if sentence.has_form("Implies", 2): + changed = True + new_assumptions_list.append(sentence._leaves[0]) + new_assumptions_list.append(Expression("Not", sentence._leaves[1])) + if assumption.has_form("Nor", None): + changed = True + for leaf in assumption.leaves: + new_assumptions_list.append(Expression("Not", leaf)) + continue + else: + new_assumptions_list.append(assumption) + + if changed: + new_assumptions_list = remove_duplicated_assumptions( + new_assumptions_list, evaluation + ) + + return new_assumptions_list, changed + + +def algebraic_expand_assumptions(assumptions_list, evaluation): + global logical_algebraic_rules + ensure_logical_algebraic_rules() + new_assumptions_list = [] + changed = False + # First apply standard rules of reduction. + # These rules are generated the first time that are used. + for assumption in assumptions_list: + assumption, applied = assumption.apply_rules( + logical_algebraic_rules, evaluation + ) + changed = changed or applied + new_assumptions_list.append(assumption) + if changed: + return new_assumptions_list, changed + # If not changed, let's try with the next set of rules + for assumption in assumptions_list: + if assumption.has_form("Not", 1): + nas, local_change = algebraic_expand_assumptions( + [assumption._leaves[0]], evaluation + ) + if local_change: + changed = local_change + for na in nas: + if na.has_form("Not", 1): + new_assumptions_list.append(na._leaves[0]) + else: + new_assumptions_list.append(Expression("Not", na)) + else: + new_assumptions_list.append(assumption) + elif assumption.has_form(("Equal", "Unequal", "Equivalent"), (3, None)): + leaves = assumption.leaves() + head = assumption.get_head() + changed = True + for i in range(len(leaves)): + for j in range(i): + new_assumptions_list.append(Expression(head, leaves[i], leaves[j])) + new_assumptions_list.append(Expression(head, leaves[j], leaves[i])) + elif assumption.has_form( + ("Less", "Greater", "LessEqual", "GreaterEqual"), (3, None) + ): + leaves = assumption.leaves() + head = assumption.get_head() + changed = True + for i in range(len(leaves)): + for j in range(i): + new_assumptions_list.append(Expression(head, leaves[i], leaves[j])) + else: + new_assumptions_list.append(assumption) + + if changed: + assumptions_list = remove_duplicated_assumptions( + new_assumptions_list, evaluation + ) + new_assumptions_list = [] + for assumption in assumptions_list: + assumption, applied = assumption.apply_rules( + logical_algebraic_rules, evaluation + ) + new_assumptions_list.append(assumption) + return new_assumptions_list, changed + + +def get_assumption_rules_dispatch(evaluation): + # TODO: cache the generated rules... + assumptions_list = get_assumptions_list(evaluation) + if assumptions_list is None: + return None + + # check for consistency: + consistent_assumptions = Expression("And", *assumptions_list) + val_consistent_assumptions = consistent_assumptions.evaluate(evaluation) + if val_consistent_assumptions == SymbolFalse: + evaluation.message("Inconsistent assumptions") + + # Expands Logically + assumptions_list, cont = logical_expand_assumptions(assumptions_list, evaluation) + while cont: + assumptions_list, cont = logical_expand_assumptions( + assumptions_list, evaluation + ) + + # Expands algebraically + assumptions_list, cont = algebraic_expand_assumptions(assumptions_list, evaluation) + while cont: + assumptions_list, cont = algebraic_expand_assumptions( + assumptions_list, evaluation + ) assumption_rules = [] - for assumption in assumptions: - true_state = True - while assumption.has_form("Not",1): - true_state = False - assumption = assumption._leaves[0] - if true_state: - assumption_rules.append(Rule(assumption, SymbolTrue)) + for pat in assumptions_list: + value = True + while pat.has_form("Not", 1): + value = not value + pat = pat._leaves[0] + + if value: + symbol_value = SymbolTrue + symbol_negate_value = SymbolFalse + else: + symbol_value = SymbolFalse + symbol_negate_value = SymbolTrue + + if pat.has_form("Equal", 2): + if value: + lhs, rhs = pat._leaves + if lhs.is_numeric(): + assumption_rules.append(Rule(rhs, lhs)) + else: + assumption_rules.append(Rule(lhs, rhs)) + else: + assumption_rules.append(Rule(pat, SymbolFalse)) + symm_pat = Expression(pat._head, pat._leaves[1], pat._leaves[0]) + assumption_rules.append(Rule(symm_pat, SymbolFalse)) + elif pat.has_form("Equivalent", 2): + assumption_rules.append(Rule(pat, symbol_value)) + symm_pat = Expression(pat._head, pat._leaves[1], pat._leaves[0]) + assumption_rules.append(Rule(symm_pat, symbol_value)) + elif pat.has_form("Less", 2): + if value: + assumption_rules.append(Rule(pat, SymbolTrue)) + assumption_rules.append( + Rule( + Expression(pat._head, pat._leaves[1], pat._leaves[0]), + SymbolFalse, + ) + ) + for head in ("Equal", "Equivalent"): + assumption_rules.append( + Rule( + Expression(head, pat._leaves[0], pat._leaves[1]), + SymbolFalse, + ) + ) + assumption_rules.append( + Rule( + Expression(head, pat._leaves[1], pat._leaves[0]), + SymbolFalse, + ) + ) + else: + assumption_rules.append(Rule(pat, SymbolFalse)) else: - assumption_rules.append(Rule(assumption, SymbolFalse)) - - pred, changed = pred.apply_rules(assumption_rules, evaluation) + assumption_rules.append(Rule(pat, symbol_value)) + # TODO: expand the pred and assumptions into an standard, + # atomized form, and then apply the rules... + if len(assumption_rules) == 0: + return None + return assumption_rules + + +def evaluate_predicate(pred, evaluation): + global logical_algebraic_rules + global remove_not_rules + + if pred.has_form(("List", "Sequence"), None): + return Expression( + pred._head, *[evaluate_predicate(subp, evaluation) for subp in pred._leaves] + ) + + debug_logical_expr("reducing ", pred, evaluation) + ensure_logical_algebraic_rules() pred = pred.evaluate(evaluation) + debug_logical_expr("-> ", pred, evaluation) + cc = True + while cc: + pred, cc = pred.apply_rules(logical_algebraic_rules, evaluation) + debug_logical_expr("-> ", pred, evaluation) + if pred.is_true() or pred == SymbolFalse: + return pred + + assumption_rules = get_assumption_rules_dispatch(evaluation) + if assumption_rules is None: + return remove_nots_when_unnecesary(pred, evaluation).evaluate(evaluation) + + if assumption_rules is not None: + debug_logical_expr(" Now, using the assumptions over ", pred, evaluation) + changed = True + while changed: + pred, changed = pred.apply_rules(assumption_rules, evaluation) + debug_logical_expr(" -> ", pred, evaluation) + + pred = remove_nots_when_unnecesary(pred, evaluation).evaluate(evaluation) return pred diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index eff42843c1..d5cdea6896 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -13,6 +13,7 @@ Integer, Integer0, Integer1, + RationalOneHalf, Number, Symbol, SymbolFalse, @@ -21,6 +22,7 @@ SymbolTrue, ) from mathics.core.convert import from_sympy, sympy_symbol_prefix +from mathics.core.rules import Pattern from mathics.builtin.scoping import dynamic_scoping from mathics.builtin.inference import evaluate_predicate @@ -68,47 +70,102 @@ def _expand(expr): if kwargs["modulus"] is not None and kwargs["modulus"] <= 0: return Integer0 + target_pat = kwargs.get("pattern", None) + if target_pat: + evaluation = kwargs["evaluation"] # A special case for trigonometric functions if "trig" in kwargs and kwargs["trig"]: - if expr.has_form("Sin", 1): + if expr.has_form( + ("Sin", "Cos", "Tan", "Cot", "Sinh", "Cosh", "Tanh", "Coth"), 1 + ): + head = expr.get_head() theta = expr.leaves[0] + if (target_pat is not None) and theta.is_free(target_pat, evaluation): + return expr + if deep: + theta = _expand(theta) if theta.has_form("Plus", 2, None): x, y = theta.leaves[0], Expression("Plus", *theta.leaves[1:]) + if head == Symbol("Sin"): + a = Expression( + "Times", + _expand(Expression("Sin", x)), + _expand(Expression("Cos", y)), + ) - a = Expression( - "Times", - _expand(Expression("Sin", x)), - _expand(Expression("Cos", y)), - ) - - b = Expression( - "Times", - _expand(Expression("Cos", x)), - _expand(Expression("Sin", y)), - ) + b = Expression( + "Times", + _expand(Expression("Cos", x)), + _expand(Expression("Sin", y)), + ) + return _expand(Expression("Plus", a, b)) + elif head == Symbol("Cos"): + a = Expression( + "Times", + _expand(Expression("Cos", x)), + _expand(Expression("Cos", y)), + ) - return Expression("Plus", a, b) + b = Expression( + "Times", + _expand(Expression("Sin", x)), + _expand(Expression("Sin", y)), + ) - elif expr.has_form("Cos", 1): - theta = expr.leaves[0] + return _expand(Expression("Plus", a, -b)) + elif head == Symbol("Sinh"): + a = Expression( + "Times", + _expand(Expression("Sinh", x)), + _expand(Expression("Cosh", y)), + ) - if theta.has_form("Plus", 2, None): - x, y = theta.leaves[0], Expression("Plus", *theta.leaves[1:]) + b = Expression( + "Times", + _expand(Expression("Cosh", x)), + _expand(Expression("Sinh", y)), + ) - a = Expression( - "Times", - _expand(Expression("Cos", x)), - _expand(Expression("Cos", y)), - ) + return _expand(Expression("Plus", a, b)) + elif head == Symbol("Cosh"): + a = Expression( + "Times", + _expand(Expression("Cosh", x)), + _expand(Expression("Cosh", y)), + ) - b = Expression( - "Times", - _expand(Expression("Sin", x)), - _expand(Expression("Sin", y)), - ) + b = Expression( + "Times", + _expand(Expression("Sinh", x)), + _expand(Expression("Sinh", y)), + ) - return Expression("Plus", a, -b) + return _expand(Expression("Plus", a, b)) + elif head == Symbol("Tan"): + a = _expand(Expression("Sin", theta)) + b = Expression( + "Power", _expand(Expression("Cos", theta)), Integer(-1) + ) + return _expand(Expression("Times", a, b)) + elif head == Symbol("Cot"): + a = _expand(Expression("Cos", theta)) + b = Expression( + "Power", _expand(Expression("Sin", theta)), Integer(-1) + ) + return _expand(Expression("Times", a, b)) + elif head == Symbol("Tanh"): + a = _expand(Expression("Sinh", theta)) + b = Expression( + "Power", _expand(Expression("Cosh", theta)), Integer(-1) + ) + return _expand(Expression("Times", a, b)) + elif head == Symbol("Coth"): + a = _expand(Expression("Times", "Cosh", theta)) + b = Expression( + "Power", _expand(Expression("Sinh", theta)), Integer(-1) + ) + return _expand(Expression(a, b)) sub_exprs = [] @@ -128,6 +185,9 @@ def convert_sympy(expr): leaves = expr.get_leaves() if isinstance(expr, Integer): return sympy.Integer(expr.get_int_value()) + if target_pat is not None and not isinstance(expr, Number): + if expr.is_free(target_pat, evaluation): + return store_sub_expr(expr) if expr.has_form("Power", 2): # sympy won't expand `(a + b) / x` to `a / x + b / x` if denom is False # if denom is False we store negative powers to prevent this. @@ -154,14 +214,19 @@ def unconvert_subexprs(expr): ) sympy_expr = convert_sympy(expr) - if deep: # thread over everything for (i, sub_expr,) in enumerate(sub_exprs): if not sub_expr.is_atom(): head = _expand(sub_expr.head) # also expand head leaves = sub_expr.get_leaves() - leaves = [_expand(leaf) for leaf in leaves] + if target_pat: + leaves = [ + leaf if leaf.is_free(target_pat, evaluation) else _expand(leaf) + for leaf in leaves + ] + else: + leaves = [_expand(leaf) for leaf in leaves] sub_exprs[i] = Expression(head, *leaves) else: # thread over Lists etc. @@ -170,7 +235,15 @@ def unconvert_subexprs(expr): for head in threaded_heads: if sub_expr.has_form(head, None): leaves = sub_expr.get_leaves() - leaves = [_expand(leaf) for leaf in leaves] + if target_pat: + leaves = [ + leaf + if leaf.is_free(target_pat, evaluation) + else _expand(leaf) + for leaf in leaves + ] + else: + leaves = [_expand(leaf) for leaf in leaves] sub_exprs[i] = Expression(head, *leaves) break @@ -197,7 +270,6 @@ def unconvert_subexprs(expr): sympy_expr = sympy_expr.expand(**hints) result = from_sympy(sympy_expr) result = unconvert_subexprs(result) - return result @@ -721,6 +793,8 @@ class Expand(_Expand):
'Expand[$expr$]'
expands out positive integer powers and products of sums in $expr$, as well as trigonometric identities. +
Expand[$expr$, $target$] +
just expands those parts involving $target$. >> Expand[(x + y) ^ 3] @@ -743,11 +817,17 @@ class Expand(_Expand): 'Expand' expands trigonometric identities >> Expand[Sin[x + y], Trig -> True] = Cos[x] Sin[y] + Cos[y] Sin[x] + >> Expand[Tanh[x + y], Trig -> True] + = Cosh[x] Sinh[y] / (Cosh[x] Cosh[y] + Sinh[x] Sinh[y]) + Cosh[y] Sinh[x] / (Cosh[x] Cosh[y] + Sinh[x] Sinh[y]) 'Expand' does not change any other expression. >> Expand[Sin[x (1 + y)]] = Sin[x (1 + y)] + Using the second argument, the expression only + expands those subexpressions containing $pat$: + >> Expand[(x+a)^2+(y+a)^2+(x+y)(x+a), y] + = a ^ 2 + 2 a y + x (a + x) + y (a + x) + y ^ 2 + (a + x) ^ 2 'Expand' also works in Galois fields >> Expand[(1 + a)^12, Modulus -> 3] = 1 + a ^ 3 + a ^ 9 + a ^ 12 @@ -767,11 +847,28 @@ class Expand(_Expand): #> (y^2)^(1/2)/(2x+2y)//Expand = Sqrt[y ^ 2] / (2 x + 2 y) - ## This caused a program crash! + #> 2(3+2x)^2/(5+x^2+3x)^3 // Expand = 24 x / (5 + 3 x + x ^ 2) ^ 3 + 8 x ^ 2 / (5 + 3 x + x ^ 2) ^ 3 + 18 / (5 + 3 x + x ^ 2) ^ 3 """ + def apply_patt(self, expr, target, evaluation, options): + "Expand[expr_, target_, OptionsPattern[Expand]]" + + if target.get_head_name() in ("System`Rule", "System`DelayedRule"): + optname = target.leaves[0].get_name() + options[optname] = target.leaves[1] + target = None + + kwargs = self.convert_options(options, evaluation) + if kwargs is None: + return + + if target: + kwargs["pattern"] = Pattern.create(target) + kwargs["evaluation"] = evaluation + return expand(expr, True, False, **kwargs) + def apply(self, expr, evaluation, options): "Expand[expr_, OptionsPattern[Expand]]" @@ -816,6 +913,8 @@ class ExpandAll(_Expand):
'ExpandAll[$expr$]'
expands out negative integer powers and products of sums in $expr$. +
'ExpandAll[$expr$, $target$]' +
just expands those parts involving $target$.
>> ExpandAll[(a + b) ^ 2 / (c + d)^2] @@ -825,6 +924,12 @@ class ExpandAll(_Expand): >> ExpandAll[(a + Sin[x (1 + y)])^2] = 2 a Sin[x + x y] + a ^ 2 + Sin[x + x y] ^ 2 + >> ExpandAll[Sin[(x+y)^2]] + = Sin[x ^ 2 + 2 x y + y ^ 2] + + >> ExpandAll[Sin[(x+y)^2], Trig->True] + = -Sin[x ^ 2] Sin[2 x y] Sin[y ^ 2] + Cos[x ^ 2] Cos[2 x y] Sin[y ^ 2] + Cos[x ^ 2] Cos[y ^ 2] Sin[2 x y] + Cos[2 x y] Cos[y ^ 2] Sin[x ^ 2] + 'ExpandAll' also expands heads >> ExpandAll[((1 + x)(1 + y))[x]] = (1 + x + y + x y)[x] @@ -832,8 +937,25 @@ class ExpandAll(_Expand): 'ExpandAll' can also work in finite fields >> ExpandAll[(1 + a) ^ 6 / (x + y)^3, Modulus -> 3] = (1 + 2 a ^ 3 + a ^ 6) / (x ^ 3 + y ^ 3) + """ + def apply_patt(self, expr, target, evaluation, options): + "ExpandAll[expr_, target_, OptionsPattern[Expand]]" + if target.get_head_name() in ("System`Rule", "System`DelayedRule"): + optname = target.leaves[0].get_name() + options[optname] = target.leaves[1] + target = None + + kwargs = self.convert_options(options, evaluation) + if kwargs is None: + return + + if target: + kwargs["pattern"] = Pattern.create(target) + kwargs["evaluation"] = evaluation + return expand(expr, numer=True, denom=True, deep=True, **kwargs) + def apply(self, expr, evaluation, options): "ExpandAll[expr_, OptionsPattern[ExpandAll]]" @@ -1483,3 +1605,359 @@ def apply(self, expr, form, h, evaluation): return Expression( "List", *[Expression(h, *[i for i in s]) for s in exponents] ) + + +class _CoefficientHandler(Builtin): + def coeff_power_internal(self, expr, var_exprs, filt, evaluation, form="expr"): + from mathics.builtin.patterns import match + + if len(var_exprs) == 0: + if form == "expr": + return expr + else: + return [([], expr)] + if len(var_exprs) == 1: + target_pat = Pattern.create(var_exprs[0]) + var_pats = [target_pat] + else: + target_pat = Pattern.create(Expression("Alternatives", *var_exprs)) + var_pats = [Pattern.create(var) for var in var_exprs] + + ####### Auxiliar functions ######### + def key_powers(lst): + key = Expression("Plus", *lst) + key = key.evaluate(evaluation) + if key.is_numeric(): + return key.to_python() + return 0 + + def powers_list(pf): + powers = [Integer0 for i, p in enumerate(var_pats)] + if pf is None: + return powers + if pf.is_symbol(): + for i, pat in enumerate(var_pats): + if match(pf, pat, evaluation): + powers[i] = Integer(1) + return powers + if pf.has_form("Sqrt", 1): + for i, pat in enumerate(var_pats): + if match(pf._leaves[0], pat, evaluation): + powers[i] = RationalOneHalf + return powers + if pf.has_form("Power", 2): + for i, pat in enumerate(var_pats): + matchval = match(pf._leaves[0], pat, evaluation) + if matchval: + powers[i] = pf._leaves[1] + return powers + if pf.has_form("Times", None): + contrib = [powers_list(factor) for factor in pf._leaves] + for i in range(len(var_pats)): + powers[i] = Expression("Plus", *[c[i] for c in contrib]).evaluate( + evaluation + ) + return powers + return powers + + def split_coeff_pow(term): + """ + This function factorizes term in a coefficent free + of powers of the target variables, and a factor with + that powers. + """ + coeffs = [] + powers = [] + # First, split factors on those which are powers of the variables + # and the rest. + if term.is_free(target_pat, evaluation): + coeffs.append(term) + elif ( + term.is_symbol() + or term.has_form("Power", 2) + or term.has_form("Sqrt", 1) + ): + powers.append(term) + elif term.has_form("Times", None): + for factor in term.leaves: + if factor.is_free(target_pat, evaluation): + coeffs.append(factor) + elif match(factor, target_pat, evaluation): + powers.append(factor) + elif ( + factor.has_form("Power", 2) or factor.has_form("Sqrt", 1) + ) and match(factor._leaves[0], target_pat, evaluation): + powers.append(factor) + else: + coeffs.append(factor) + else: + coeffs.append(term) + # Now, rebuild both factors + if len(coeffs) == 0: + coeffs = None + elif len(coeffs) == 1: + coeffs = coeffs[0] + else: + coeffs = Expression("Times", *coeffs) + if len(powers) == 0: + powers = None + elif len(powers) == 1: + powers = powers[0] + else: + powers = Expression("Times", *sorted(powers)) + return coeffs, powers + + ################# The actual begin #################### + expr = expand( + expr, + numer=True, + denom=False, + deep=False, + trig=False, + modulus=None, + target_pat=target_pat, + ) + + if expr.is_free(target_pat, evaluation): + if filt: + expr = Expression(filt, expr).evaluate(evaluation) + if form == "expr": + return expr + else: + return [(powers_list(None), expr)] + elif ( + expr.is_symbol() + or match(expr, target_pat, evaluation) + or expr.has_form("Power", 2) + or expr.has_form("Sqrt", 1) + ): + coeff = ( + Expression(filt, Integer1).evaluate(evaluation) if filt else Integer1 + ) + if form == "expr": + if coeff is Integer1: + return expr + else: + return Expression("Times", coeff, expr) + else: + if not coeff.is_free(target_pat, evaluation): + return [] + return [(powers_list(expr), coeff)] + elif expr.has_form("Times", None): + coeff, powers = split_coeff_pow(expr) + if coeff is None: + coeff = Integer1 + else: + if form != "expr" and not coeff.is_free(target_pat, evaluation): + return [] + if filt: + coeff = Expression(filt, coeff).evaluate(evaluation) + + if form == "expr": + if powers is None: + return coeff + else: + if coeff is Integer1: + return powers + else: + return Expression("Times", coeff, powers) + else: + pl = powers_list(powers) + return [(pl, coeff)] + elif expr.has_form("Plus", None): + coeff_dict = {} + powers_dict = {} + powers_order = {} + for term in expr._leaves: + coeff, powers = split_coeff_pow(term) + if ( + form != "expr" + and coeff is not None + and not coeff.is_free(target_pat, evaluation) + ): + return [] + pl = powers_list(powers) + key = str(pl) + if not key in powers_dict: + if form == "expr": + powers_dict[key] = powers + else: + # TODO: check if pl is a monomial... + powers_dict[key] = pl + coeff_dict[key] = [] + powers_order[key] = key_powers(pl) + + coeff_dict[key].append(Integer1 if coeff is None else coeff) + + terms = [] + for key in sorted( + coeff_dict, key=lambda kv: powers_order[kv], reverse=False + ): + val = coeff_dict[key] + if len(val) == 0: + continue + elif len(val) == 1: + coeff = val[0] + else: + coeff = Expression("Plus", *val) + if filt: + coeff = Expression(filt, coeff).evaluate(evaluation) + + powerfactor = powers_dict[key] + if form == "expr": + if powerfactor: + terms.append(Expression("Times", coeff, powerfactor)) + else: + terms.append(coeff) + else: + terms.append([powerfactor, coeff]) + if form == "expr": + return Expression("Plus", *terms) + else: + return terms + else: + # expr is not a polynomial. + if form == "expr": + if filt: + expr = Expression(filt, expr).evaluate(evaluation) + return expr + else: + return [] + + +class CoefficientArrays(_CoefficientHandler): + """ +
+
'CoefficientArrays[$polys$, $vars$]' +
returns a list of arrays of coefficients of the variables $vars$ in the polynomial $poly$. +
+ + >> CoefficientArrays[1 + x^3, x] + = {1, {0}, {{0}}, {{{1}}}} + >> CoefficientArrays[1 + x y+ x^3, {x, y}] + = {1, {0, 0}, {{0, 1}, {0, 0}}, {{{1, 0}, {0, 0}}, {{0, 0}, {0, 0}}}} + >> CoefficientArrays[{1 + x^2, x y}, {x, y}] + = {{1, 0}, {{0, 0}, {0, 0}}, {{{1, 0}, {0, 0}}, {{0, 1}, {0, 0}}}} + >> CoefficientArrays[(x+y+Sin[z])^3, {x,y}] + = {Sin[z] ^ 3, {3 Sin[z] ^ 2, 3 Sin[z] ^ 2}, {{3 Sin[z], 6 Sin[z]}, {0, 3 Sin[z]}}, {{{1, 3}, {0, 3}}, {{0, 0}, {0, 1}}}} + >> CoefficientArrays[(x + y + Sin[z])^3, {x, z}] + : (x + y + Sin[z]) ^ 3 is not a polynomial in {x, z} + = CoefficientArrays[(x + y + Sin[z]) ^ 3, {x, z}] + """ + + options = { + "Symmetric": "False", + } + messages = { + "poly": "`1` is not a polynomial in `2`", + } + + def apply_list(self, polys, varlist, evaluation, options): + "%(name)s[polys_, varlist_, OptionsPattern[]]" + from mathics.builtin.lists import walk_parts + + if polys.has_form("List", None): + list_polys = polys.leaves + else: + list_polys = [polys] + + if varlist.is_symbol(): + var_exprs = [varlist] + elif varlist.has_form("List", None): + var_exprs = varlist.get_leaves() + else: + var_exprs = [varlist] + + coeffs = [ + self.coeff_power_internal(pol, var_exprs, None, evaluation, "coeffs") + for pol in list_polys + ] + + dim1 = len(coeffs) + dim2 = len(var_exprs) + arrays = [] + if dim1 == 1: + arrays.append(Integer(0)) + for i, component in enumerate(coeffs): + if len(component) == 0: + evaluation.message("CoefficientArrays", "poly", polys, varlist) + return + for idxcoeff in component: + idx, coeff = idxcoeff + order = Expression("Plus", *idx).evaluate(evaluation).get_int_value() + if order is None: + evaluation.message("CoefficientArrays", "poly", polys, varlist) + return + while len(arrays) <= order: + cur_ord = len(arrays) + range2 = Expression(SymbolList, Integer(dim2)) + its2 = [range2 for k in range(cur_ord)] + # TODO: Use SparseArray... + # This constructs a tensor or range cur_ord+1 + if dim1 > 1: + newtable = Expression( + "Table", + Integer(0), + Expression(SymbolList, Integer(dim1)), + *its2 + ) + else: + newtable = Expression("Table", Integer(0), *its2) + arrays.append(newtable.evaluate(evaluation)) + curr_array = arrays[order] + arrayidx = [ + Integer(n + 1) + for n, j in enumerate(idx) + for q in range(j.get_int_value()) + ] + if dim1 > 1: + arrayidx = [Integer(i + 1)] + arrayidx + if dim1 == 1 and order == 0: + arrays[0] = coeff + else: + arrays[order] = walk_parts( + [curr_array], arrayidx, evaluation, coeff + ) + return Expression("List", *arrays) + + +class Collect(_CoefficientHandler): + """ +
+
'Collect[$expr$, $x$]' +
Expands $expr$ and collect together terms having the same power of $x$. +
'Collect[$expr$, {$x_1$, $x_2$, ...}]' +
Expands $expr$ and collect together terms having the same powers of + $x_1$, $x_2$, .... +
'Collect[$expr$, {$x_1$, $x_2$, ...}, $filter$]' +
After collect the terms, applies $filter$ to each coefficient. +
+ + >> Collect[(x+y)^3, y] + = x ^ 3 + 3 x ^ 2 y + 3 x y ^ 2 + y ^ 3 + >> Collect[2 Sin[x z] (x+2 y^2 + Sin[y] x), y] + = 2 x Sin[x z] + 2 x Sin[x z] Sin[y] + 4 y ^ 2 Sin[x z] + >> Collect[3 x y+2 Sin[x z] (x+2 y^2 + x) + (x+y)^3, y] + = 4 x Sin[x z] + x ^ 3 + y (3 x + 3 x ^ 2) + y ^ 2 (3 x + 4 Sin[x z]) + y ^ 3 + >> Collect[3 x y+2 Sin[x z] (x+2 y^2 + x) + (x+y)^3, {x,y}] + = 4 x Sin[x z] + x ^ 3 + 3 x y + 3 x ^ 2 y + 4 y ^ 2 Sin[x z] + 3 x y ^ 2 + y ^ 3 + >> Collect[3 x y+2 Sin[x z] (x+2 y^2 + x) + (x+y)^3, {x,y}, h] + = x h[4 Sin[x z]] + x ^ 3 h[1] + x y h[3] + x ^ 2 y h[3] + y ^ 2 h[4 Sin[x z]] + x y ^ 2 h[3] + y ^ 3 h[1] + """ + + rules = { + "Collect[expr_, varlst_]": "Collect[expr, varlst, Identity]", + } + + def apply_var_filter(self, expr, varlst, filt, evaluation): + """Collect[expr_, varlst_, filt_]""" + if filt == Symbol("Identity"): + filt = None + if varlst.is_symbol(): + var_exprs = [varlst] + elif varlst.has_form("List", None): + var_exprs = varlst.get_leaves() + else: + var_exprs = [varlst] + + return self.coeff_power_internal(expr, var_exprs, filt, evaluation, "expr") diff --git a/mathics/builtin/patterns.py b/mathics/builtin/patterns.py index 92e7a8e76f..8388894fee 100644 --- a/mathics/builtin/patterns.py +++ b/mathics/builtin/patterns.py @@ -34,18 +34,22 @@ from mathics.version import __version__ # noqa used in loading to check consistency. -from mathics.builtin.base import Builtin, BinaryOperator, PostfixOperator +from mathics.builtin.base import Builtin, BinaryOperator, PostfixOperator, AtomBuiltin from mathics.builtin.base import PatternObject, PatternError from mathics.builtin.lists import python_levelspec, InvalidLevelspecError from mathics.core.expression import ( + Atom, + String, Symbol, Expression, Number, Integer, Rational, Real, + SymbolFalse, SymbolList, + SymbolTrue, ) from mathics.core.rules import Rule from mathics.core.pattern import Pattern, StopGenerator @@ -101,13 +105,16 @@ class RuleDelayed(BinaryOperator): def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): - if rules_expr.has_form("Dispatch", None): - rules_expr = rules_expr.leaves[0] + if isinstance(rules_expr, Dispatch): + return rules_expr.rules, False + elif rules_expr.has_form("Dispatch", None): + return Dispatch(rules_expr._leaves, evaluation) + if rules_expr.has_form("List", None): rules = rules_expr.leaves else: rules = [rules_expr] - any_lists = any(item.has_form("List", None) for item in rules) + any_lists = any(item.has_form(("List", "Dispatch"), None) for item in rules) if any_lists: all_lists = all(item.has_form("List", None) for item in rules) if all_lists: @@ -285,10 +292,8 @@ def apply(self, expr, rules, evaluation): "ReplaceAll[expr_, rules_]" try: rules, ret = create_rules(rules, expr, "ReplaceAll", evaluation) - if ret: return rules - result, applied = expr.apply_rules(rules, evaluation) return result except PatternError: @@ -451,6 +456,9 @@ class PatternTest(BinaryOperator, PatternObject): = True >> MatchQ[-3, _Integer?(#>0&)] = False + >> MatchQ[3, Pattern[3]] + : First element in pattern Pattern[3] is not a valid pattern name. + = False """ operator = "?" @@ -630,7 +638,10 @@ class _StopGeneratorMatchQ(StopGenerator): class Matcher(object): def __init__(self, form): - self.form = Pattern.create(form) + if isinstance(form, Pattern): + self.form = form + else: + self.form = Pattern.create(form) def match(self, expr, evaluation): def yield_func(vars, rest): @@ -660,6 +671,9 @@ class MatchQ(Builtin): = False >> MatchQ[_Integer][123] = True + >> MatchQ[3, Pattern[3]] + : First element in pattern Pattern[3] is not a valid pattern name. + = False """ rules = {"MatchQ[form_][expr_]": "MatchQ[expr, form]"} @@ -667,9 +681,13 @@ class MatchQ(Builtin): def apply(self, expr, form, evaluation): "MatchQ[expr_, form_]" - if match(expr, form, evaluation): - return Symbol("True") - return Symbol("False") + try: + if match(expr, form, evaluation): + return SymbolTrue + return SymbolFalse + except PatternError as e: + evaluation.message(e.name, e.tag, *(e.args)) + return SymbolFalse class Verbatim(PatternObject): @@ -801,10 +819,13 @@ class Pattern_(PatternObject): } def init(self, expr): - super(Pattern_, self).init(expr) - self.varname = expr.leaves[0].get_name() - if self.varname is None: + if len(expr.leaves) != 2: + self.error("patvar", expr) + varname = expr.leaves[0].get_name() + if varname is None or varname == "": self.error("patvar", expr) + super(Pattern_, self).init(expr) + self.varname = varname self.pattern = Pattern.create(expr.leaves[1]) def __repr__(self): @@ -1450,7 +1471,33 @@ def yield_match(vars, rest): ) -class Dispatch(Builtin): +class Dispatch(Atom): + def __init__(self, rulelist, evaluation): + self.src = Expression(SymbolList, *rulelist) + self.rules = [Rule(rule._leaves[0], rule._leaves[1]) for rule in rulelist] + self._leaves = None + self._head = Symbol("Dispatch") + + def get_sort_key(self): + return self.src.get_sort_key() + + def get_atom_name(self): + return "System`Dispatch" + + def __repr__(self): + return "dispatch" + + def atom_to_boxes(self, f, evaluation): + leaves = self.src.format(evaluation, f.get_name()) + return Expression( + "RowBox", + Expression( + SymbolList, String("Dispatch"), String("["), leaves, String("]") + ), + ) + + +class DispatchAtom(AtomBuiltin): """
'Dispatch[$rulelist$]' @@ -1458,10 +1505,23 @@ class Dispatch(Builtin): In the future, it should return an optimized DispatchRules atom, containing an optimized set of rules.
- + >> rules = {{a_,b_}->a^b, {1,2}->3., F[x_]->x^2}; + >> F[2] /. rules + = 4 + >> dispatchrules = Dispatch[rules] + = Dispatch[{{a_, b_} -> a ^ b, {1, 2} -> 3., F[x_] -> x ^ 2}] + >> F[2] /. dispatchrules + = 4 """ - def apply_stub(self, rules, evaluation): + messages = { + "invrpl": "`1` is not a valid rule or list of rules.", + } + + def __repr__(self): + return "dispatchatom" + + def apply_create(self, rules, evaluation): """Dispatch[rules_List]""" # TODO: # The next step would be to enlarge this method, in order to @@ -1471,4 +1531,43 @@ def apply_stub(self, rules, evaluation): # compiled patters, and modify Replace and ReplaceAll to handle this # kind of objects. # - return rules + if isinstance(rules, Dispatch): + return rules + if rules.is_symbol(): + rules = rules.evaluate(evaluation) + + if rules.has_form("List", None): + rules = rules._leaves + else: + rules = [rules] + + all_list = all(rule.has_form("List", None) for rule in rules) + if all_list: + leaves = [self.apply_create(rule, evaluation) for rule in rules] + return Expression(SymbolList, *leaves) + flatten_list = [] + for rule in rules: + if rule.is_symbol(): + rule = rule.evaluate(evaluation) + if rule.has_form("List", None): + flatten_list.extend(rule._leaves) + elif rule.has_form(("Rule", "RuleDelayed"), 2): + flatten_list.append(rule) + elif isinstance(rule, Dispatch): + flatten_list.extend(rule.src._leaves) + else: + # WMA does not raise this message: just leave it unevaluated, + # and raise an error when the dispatch rule is used. + evaluation.message("Dispatch", "invrpl", rule) + return + try: + return Dispatch(flatten_list, evaluation) + except: + return + + def apply_normal(self, dispatch, evaluation): + """Normal[dispatch_Dispatch]""" + if isinstance(dispatch, Dispatch): + return dispatch.src + else: + return dispatch._leaves[0] diff --git a/mathics/builtin/system.py b/mathics/builtin/system.py index 6254755261..fcdbd234e7 100644 --- a/mathics/builtin/system.py +++ b/mathics/builtin/system.py @@ -451,7 +451,7 @@ class VersionNumber(Predefined): """ name = "$VersionNumber" - value = 6.0 + value = 10.0 def evaluate(self, evaluation) -> Real: # Make this be whatever the latest Mathematica release is, @@ -509,14 +509,18 @@ def apply_0(self, evaluation) -> Integer: # Partially borrowed from https://code.activestate.com/recipes/577504/ from itertools import chain from sys import getsizeof + definitions = evaluation.definitions seen = set() default_size = getsizeof(0) - handlers = {tuple: iter, - list: iter, - dict: (lambda d: chain.from_iterable(d.items())), - set: iter, - frozenset: iter,} + handlers = { + tuple: iter, + list: iter, + dict: (lambda d: chain.from_iterable(d.items())), + set: iter, + frozenset: iter, + } + def sizeof(obj): if id(obj) in seen: return 0 @@ -531,10 +535,6 @@ def sizeof(obj): return Integer(sizeof(definitions)) - - - - class Share(Builtin): """
diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 623c54bab3..4d62bb021b 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -2263,7 +2263,6 @@ def __neg__(self) -> "Integer": def is_zero(self) -> bool: return self.value == 0 - Integer0 = Integer(0) Integer1 = Integer(1) @@ -2362,6 +2361,9 @@ def is_zero(self) -> bool: ) # (implicit) and not (self.denominator().is_zero) +RationalOneHalf = Rational(1, 2) + + class Real(Number): def __new__(cls, value, p=None) -> "Real": if isinstance(value, str): diff --git a/test/test_assumptions.py b/test/test_assumptions.py new file mode 100644 index 0000000000..78cb2e20fc --- /dev/null +++ b/test/test_assumptions.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from .helper import check_evaluation +import pytest + +from mathics_scanner.errors import IncompleteSyntaxError + +list_test_assumptions_integrate = [ + ( + "Integrate[x^n, {x, 0, 1}]", + "Piecewise[{{1 / (1 + n), 1 + Re[n] > 0 && n > -Infinity && n < Infinity && n != -1}}, Infinity]", + "This is so complicated due the sympy result is wrong...", + ), + ( + "Assuming[0 < n < 1, Integrate[x^n, {x, 0, 1}]]", + "Piecewise[{{1 / (1 + n), 1 + Re[n] > 0 && n > -Infinity && n < Infinity && n != -1}}, Infinity]", + "", + ), + ( + "Assuming[0 < Re[n] + 1, Integrate[x^n, {x, 0, 1}]]", + "Piecewise[{{1 / (1 + n), n > -Infinity && n < Infinity && n != -1}}, Infinity]", + "", + ), + ("Assuming[n == 1, Integrate[x^n, {x, 0, 1}]]", "1 / 2", ""), + ("Assuming[n == 2, Integrate[x^n, {x, 0, 1}]]", "1 / 3", ""), + ("Assuming[n == -1, Integrate[x^n, {x, 0, 1}]]", "Infinity", ""), + # ("Assuming[12, n>=3], Integrate[x^n, {x, 0, 1}]]", "x^(n+1)/(n+1)", ""), +] + +list_test_assumptions_simplify = [ + ("Simplify[a==b || a!=b]", "True", "",), + ("Simplify[a==b && a!=b]", "False", "",), + ("Simplify[a<=b && a>b]", "False", "",), + ("Simplify[a==b, ! a!=b]", "True", "",), + ("Simplify[a==b, a!=b]", "False", "",), + ("Simplify[a > b, {a==4}]", "b < 4", "",), + ("Simplify[And[a>b, bb", "",), + ("Simplify[Or[a>b, ab, bb", "",), + ("Simplify[a>b, {b<=a}]", "a>b", "",), +] + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "message"), list_test_assumptions_integrate, +) +@pytest.mark.xfail +def test_assumptions_integrate(str_expr, str_expected, message): + check_evaluation(str_expr, str_expected) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "message"), list_test_assumptions_simplify, +) +@pytest.mark.xfail +def test_assumptions_simplify(str_expr, str_expected, message): + check_evaluation(str_expr, str_expected)