Skip to content

Commit 3c0ea29

Browse files
authored
Merge pull request #1330 from mathics/ConditionalExpression-basic
basic implementation of ConditionalExpression, Assuming and
2 parents 3691e3d + 417f478 commit 3c0ea29

9 files changed

Lines changed: 305 additions & 73 deletions

File tree

CHANGES.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ New builtins
1010

1111
* ``Arg``
1212
* ``Dispatch``
13+
* ``FullSimplify``
1314
* ``LetterNumber`` #1298. The ``alphabet`` parameter supports only a minimal number of languages.
1415
* ``Nand`` and ``Nor`` logical functions.
1516
* ``Series``, ``O`` and ``SeriesData``
@@ -21,19 +22,23 @@ New builtins
2122
Enhancements
2223
++++++++++++
2324

25+
* a function `evaluate_predicate` allows for a basic predicate evaluation using `$Assumptions`.
2426
* ``Attributes`` accepts a string parameter.
2527
* ``ColorNegate`` for colors is supported.
2628
* ``D`` and ``Derivative`` improvements.
2729
* ``FileNames`` returns a sorted list (#1250).
2830
* ``FindRoot`` now receives several optional parameters like ``Method`` and ``MaxIterations``.
29-
* ``FixedPoint`` now supports the ``SameTest`` option.
31+
* ``FixedPoint`` now supports the ``SameTest`` option.
3032
* ``Prime`` and ``PrimePi`` now accept a list parameter and have the ``NumericFunction`` attribute.
3133
* ``ReplaceRepeated`` and ``FixedPoint`` now supports the ``MaxIteration`` option (#1260).
34+
* ``Simplify`` performs a more sophisticated set of simplifications.
35+
* ``Simplify`` accepts a second parameter that temporarily overwrites ``$Assumptions``.
3236
* ``StringTake`` now accepts form containing a list of strings and specification (#1297).
3337
* ``Table`` [*expr*, *n*] is supported.
3438
* ``ToString`` accepts an optional *form* parameter.
3539
* ``ToExpression`` handles multi-line string input
3640
* The implementation of Streams was redone.
41+
3742

3843

3944
Bug fixes

mathics/builtin/algebra.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Algebraic Manipulation
44
"""
55

6+
67
from mathics.version import __version__ # noqa used in loading to check consistency.
78

89
from mathics.builtin.base import Builtin
@@ -15,10 +16,13 @@
1516
Number,
1617
Symbol,
1718
SymbolFalse,
19+
SymbolList,
1820
SymbolNull,
1921
SymbolTrue,
2022
)
2123
from mathics.core.convert import from_sympy, sympy_symbol_prefix
24+
from mathics.builtin.scoping import dynamic_scoping
25+
from mathics.builtin.inference import evaluate_predicate
2226

2327
import sympy
2428

@@ -288,6 +292,8 @@ class Simplify(Builtin):
288292
<dl>
289293
<dt>'Simplify[$expr$]'
290294
<dd>simplifies $expr$.
295+
<dt>'Simplify[$expr$, $assump$]'
296+
<dd>simplifies $expr$ assuming $assump$ instead of $Assumptions$.
291297
</dl>
292298
293299
>> Simplify[2*Sin[x]^2 + 2*Cos[x]^2]
@@ -297,6 +303,16 @@ class Simplify(Builtin):
297303
>> Simplify[f[x]]
298304
= f[x]
299305
306+
Simplify over conditional expressions uses $Assumptions, or $assump$
307+
to evaluate the condition:
308+
# TODO: enable this once the logic for conditional expression
309+
# be restaured.
310+
# >> $Assumptions={a <= 0};
311+
# >> Simplify[ConditionalExpression[1, a > 0]]
312+
# = Undefined
313+
# >> Simplify[ConditionalExpression[1, a > 0], { a > 0 }]
314+
# = 1
315+
300316
#> Simplify[a*x^2+b*x^2]
301317
= x ^ 2 (a + b)
302318
@@ -308,11 +324,31 @@ class Simplify(Builtin):
308324
rules = {
309325
"Simplify[list_List]": "Simplify /@ list",
310326
"Simplify[rule_Rule]": "Simplify /@ rule",
311-
"Simplify[eq_Equal]": "Simplify /@ eq",
327+
"Simplify[list_List, assum_]": "Simplify[#1, assum]& /@ list",
328+
"Simplify[rule_Rule, assum_]": "Simplify[#1, assum]& /@ rule",
329+
"Simplify[0^a_, assum_]": "ConditionalExpression[0,Simplify[a>0]]",
330+
"Simplify[b_^a_, assum_]": "ConditionalExpression[b,Simplify[{Or[a>0, b!=0]}]]",
312331
}
313332

333+
def apply_assuming(self, expr, assumptions, evaluation):
334+
"%(name)s[expr_, assumptions_]"
335+
assumptions = assumptions.evaluate(evaluation)
336+
return dynamic_scoping(
337+
lambda ev: self.apply(expr, ev),
338+
{"System`$Assumptions": assumptions},
339+
evaluation,
340+
)
341+
314342
def apply(self, expr, evaluation):
315-
"Simplify[expr_]"
343+
"%(name)s[expr_]"
344+
# Check first if we are dealing with a logic expression...
345+
expr = evaluate_predicate(expr, evaluation)
346+
if expr.is_atom():
347+
return expr
348+
# else, use sympy:
349+
leaves = [self.apply(leaf, evaluation) for leaf in expr._leaves]
350+
head = self.apply(expr.get_head(), evaluation)
351+
expr = Expression(head, *leaves)
316352

317353
sympy_expr = expr.to_sympy()
318354
if sympy_expr is None:
@@ -321,6 +357,31 @@ def apply(self, expr, evaluation):
321357
return from_sympy(sympy_result)
322358

323359

360+
class FullSimplify(Simplify):
361+
"""
362+
<dl>
363+
<dt>'FullSimplify[$expr$]'
364+
<dd>simplifies $expr$ using an extended set of simplification rules.
365+
<dt>'FullSimplify[$expr$, $assump$]'
366+
<dd>simplifies $expr$ assuming $assump$ instead of $Assumptions$.
367+
</dl>
368+
TODO: implement the extension. By now, this does the same than Simplify...
369+
370+
>> FullSimplify[2*Sin[x]^2 + 2*Cos[x]^2]
371+
= 2
372+
373+
"""
374+
375+
rules = {
376+
"FullSimplify[list_List]": "FullSimplify /@ list",
377+
"FullSimplify[rule_Rule]": "FullSimplify /@ rule",
378+
"FullSimplify[eq_Equal]": "FullSimplify /@ eq",
379+
"FullSimplify[list_List, assum_]": "FullSimplify[#1, assum]& /@ list",
380+
"FullSimplify[rule_Rule, assum_]": "FullSimplify[#1, assum]& /@ rule",
381+
"FullSimplify[eq_Equal, assum_]": "FullSimplify[#1, assum]& /@ eq",
382+
}
383+
384+
324385
class Together(Builtin):
325386
"""
326387
<dl>
@@ -1264,7 +1325,7 @@ def apply(self, expr, form, evaluation):
12641325
if expr == Integer0:
12651326
return Expression("List")
12661327
elif e_null and f_null:
1267-
return Expression("List", Integer0)
1328+
return Expression(SymbolList, Integer0)
12681329
elif e_null and not f_null:
12691330
return Expression("List", SymbolNull)
12701331
elif f_null:

mathics/builtin/arithmetic.py

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@
4242
SymbolFalse,
4343
SymbolUndefined,
4444
SymbolSequence,
45+
SymbolList,
4546
from_mpmath,
4647
from_python,
4748
)
4849
from mathics.core.numbers import min_prec, dps, SpecialValueError
4950

5051
from mathics.builtin.lists import _IterationFunction
51-
from mathics.core.convert import from_sympy, SympyExpression
52+
from mathics.core.convert import from_sympy, SympyExpression, sympy_symbol_prefix
53+
from mathics.builtin.scoping import dynamic_scoping
54+
from mathics.builtin.inference import get_assumptions_list, evaluate_predicate
5255

5356

5457
@lru_cache(maxsize=1024)
@@ -741,7 +744,7 @@ def apply(self, items, evaluation):
741744
elif number.sameQ(Integer(-1)) and leaves and leaves[0].has_form("Plus", None):
742745
leaves[0] = Expression(
743746
leaves[0].get_head(),
744-
*[Expression("Times", Integer(-1), leaf) for leaf in leaves[0].leaves]
747+
*[Expression("Times", Integer(-1), leaf) for leaf in leaves[0].leaves],
745748
)
746749
number = None
747750

@@ -2222,7 +2225,7 @@ class Piecewise(SympyFunction):
22222225
## Piecewise({{0, Or[x < 0, x > 0]}}, Indeterminate).
22232226
22242227
>> Integrate[Piecewise[{{1, x <= 0}, {-1, x > 0}}], x]
2225-
= Piecewise[{{x, x <= 0}, {-x, True}}]
2228+
= Piecewise[{{x, x <= 0}}, -x]
22262229
22272230
>> Integrate[Piecewise[{{1, x <= 0}, {-1, x > 0}}], {x, -1, 2}]
22282231
= -1
@@ -2244,15 +2247,18 @@ class Piecewise(SympyFunction):
22442247

22452248
def apply(self, items, evaluation):
22462249
"%(name)s[items__]"
2247-
result = self.to_sympy(Expression("Piecewise", *items.get_sequence()))
2250+
result = self.to_sympy(Expression("Piecewise", *items.get_sequence()),
2251+
evaluation=evaluation
2252+
)
22482253
if result is None:
22492254
return
22502255
if not isinstance(result, sympy.Piecewise):
2251-
return from_sympy(result)
2256+
result = from_sympy(result)
2257+
return result
22522258

22532259
def to_sympy(self, expr, **kwargs):
22542260
leaves = expr.leaves
2255-
2261+
evaluation = kwargs.get("evaluation", None)
22562262
if len(leaves) not in (1, 2):
22572263
return
22582264

@@ -2263,6 +2269,8 @@ def to_sympy(self, expr, **kwargs):
22632269
if len(case.leaves) != 2:
22642270
return
22652271
then, cond = case.leaves
2272+
if evaluation:
2273+
cond = evaluate_predicate(cond, evaluation)
22662274

22672275
sympy_cond = None
22682276
if isinstance(cond, Symbol):
@@ -2320,11 +2328,11 @@ def apply(self, expr, evaluation):
23202328

23212329
class Assumptions(Predefined):
23222330
"""
2323-
<dl>
2324-
<dt>'$Assumptions'
2325-
<dd>is the default setting for the Assumptions option used in such
2326-
functions as Simplify, Refine, and Integrate.
2327-
</dl>
2331+
<dl>
2332+
<dt>'$Assumptions'
2333+
<dd>is the default setting for the Assumptions option used in such
2334+
functions as Simplify, Refine, and Integrate.
2335+
</dl>
23282336
"""
23292337

23302338
name = "$Assumptions"
@@ -2338,84 +2346,113 @@ class Assuming(Builtin):
23382346
"""
23392347
<dl>
23402348
<dt>'Assuming[$cond$, $expr$]'
2341-
<dd>Evaluates $expr$ assuming the conditions $cond$
2349+
<dd>Evaluates $expr$ assuming the conditions $cond$.
23422350
</dl>
23432351
>> $Assumptions = { x > 0 }
23442352
= {x > 0}
2345-
>> Assuming[y>0, $Assumptions]
2346-
= {x > 0, y > 0}
2353+
>> Assuming[y>0, ConditionalExpression[y x^2, y>0]//Simplify]
2354+
= x ^ 2 y
2355+
>> Assuming[Not[y>0], ConditionalExpression[y x^2, y>0]//Simplify]
2356+
= Undefined
2357+
>> ConditionalExpression[y x ^ 2, y > 0]//Simplify
2358+
= ConditionalExpression[x ^ 2 y, y > 0]
23472359
"""
23482360

23492361
attributes = ("HoldRest",)
23502362

2351-
def apply_assuming(self, cond, expr, evaluation):
2352-
"Assuming[cond_, expr_]"
2353-
cond = cond.evaluate(evaluation)
2354-
if cond.is_true():
2363+
def apply_assuming(self, assumptions, expr, evaluation):
2364+
"Assuming[assumptions_, expr_]"
2365+
assumptions = assumptions.evaluate(evaluation)
2366+
if assumptions.is_true():
23552367
cond = []
2356-
elif cond.is_symbol() or not cond.has_form("List", None):
2357-
cond = [cond]
2368+
elif assumptions.is_symbol() or not assumptions.has_form("List", None):
2369+
cond = [assumptions]
23582370
else:
2359-
cond = cond.leaves
2360-
assumptions = evaluation.definitions.get_definition(
2361-
"System`$Assumptions", only_if_exists=True
2371+
cond = assumptions._leaves
2372+
cond = tuple(cond) + get_assumptions_list(evaluation)
2373+
list_cond = Expression("List", *cond)
2374+
# TODO: reduce the list of predicates
2375+
return dynamic_scoping(
2376+
lambda ev: expr.evaluate(ev), {"System`$Assumptions": list_cond}, evaluation
23622377
)
23632378

2364-
if assumptions:
2365-
assumptions = assumptions.ownvalues
2366-
if len(assumptions) > 0:
2367-
assumptions = assumptions[0].replace
2368-
else:
2369-
assumptions = None
2370-
if assumptions:
2371-
if assumptions.is_symbol() or not assumptions.has_form("List", None):
2372-
assumptions = [assumptions]
2373-
else:
2374-
assumptions = assumptions.leaves
2375-
cond = assumptions + tuple(cond)
2376-
Expression(
2377-
"Set", Symbol("System`$Assumptions"), Expression("List", *cond)
2378-
).evaluate(evaluation)
2379-
ret = expr.evaluate(evaluation)
2380-
if assumptions:
2381-
Expression(
2382-
"Set", Symbol("System`$Assumptions"), Expression("List", *assumptions)
2383-
).evaluate(evaluation)
2384-
else:
2385-
Expression(
2386-
"Set", Symbol("System`$Assumptions"), Expression("List", SymbolTrue)
2387-
).evaluate(evaluation)
2388-
return ret
2389-
23902379

23912380
class ConditionalExpression(Builtin):
23922381
"""
23932382
<dl>
2394-
<dt>'ConditionalExpression[$expr$, $cond$]'
2395-
<dd>returns $expr$ if $cond$ evaluates to $True$, $Undefined$ if
2396-
$cond$ evaluates to $False$.
2383+
<dt>'ConditionalExpression[$expr$, $cond$]'
2384+
<dd>returns $expr$ if $cond$ evaluates to $True$, $Undefined$ if $cond$ evaluates to $False$.
23972385
</dl>
23982386
2387+
>> ConditionalExpression[x^2, True]
2388+
= x ^ 2
2389+
2390+
>> ConditionalExpression[x^2, False]
2391+
= Undefined
2392+
23992393
>> f = ConditionalExpression[x^2, x>0]
24002394
= ConditionalExpression[x ^ 2, x > 0]
24012395
>> f /. x -> 2
24022396
= 4
24032397
>> f /. x -> -2
24042398
= Undefined
2399+
'ConditionalExpression' uses assumptions to evaluate the condition:
2400+
>> $Assumptions = x > 0;
2401+
>> ConditionalExpression[x ^ 2, x>0]//Simplify
2402+
= x ^ 2
2403+
>> $Assumptions = True;
2404+
# >> ConditionalExpression[ConditionalExpression[s,x>a], x<b]
2405+
# = ConditionalExpression[s, And[x>a, x<b]]
24052406
"""
24062407

2408+
sympy_name = "Piecewise"
2409+
24072410
rules = {
24082411
"ConditionalExpression[expr_, True]": "expr",
24092412
"ConditionalExpression[expr_, False]": "Undefined",
2413+
"ConditionalExpression[ConditionalExpression[expr_, cond1_], cond2_]": "ConditionalExpression[expr, And@@Flatten[{cond1, cond2}]]",
2414+
"ConditionalExpression[expr1_, cond_] + expr2_": "ConditionalExpression[expr1+expr2, cond]",
2415+
"ConditionalExpression[expr1_, cond_] expr2_": "ConditionalExpression[expr1 expr2, cond]",
2416+
"ConditionalExpression[expr1_, cond_]^expr2_": "ConditionalExpression[expr1^expr2, cond]",
2417+
"expr1_ ^ ConditionalExpression[expr2_, cond_]": "ConditionalExpression[expr1^expr2, cond]",
24102418
}
24112419

24122420
def apply_generic(self, expr, cond, evaluation):
24132421
"ConditionalExpression[expr_, cond_]"
2414-
cond = cond.evaluate(evaluation)
2422+
# What we need here is a way to evaluate
2423+
# cond as a predicate, using assumptions.
2424+
# Let's delegate this to the And (and Or) symbols...
2425+
if not cond.is_atom() and cond._head == SymbolList:
2426+
cond = Expression("System`And", *(cond._leaves))
2427+
else:
2428+
cond = Expression("System`And", cond)
24152429
if cond is None:
24162430
return
24172431
if cond.is_true():
24182432
return expr
24192433
if cond == SymbolFalse:
24202434
return SymbolUndefined
24212435
return
2436+
2437+
def to_sympy(self, expr, **kwargs):
2438+
leaves = expr.leaves
2439+
if len(leaves) != 2:
2440+
return
2441+
expr, cond = leaves
2442+
2443+
sympy_cond = None
2444+
if isinstance(cond, Symbol):
2445+
if cond == SymbolTrue:
2446+
sympy_cond = True
2447+
elif cond == SymbolFalse:
2448+
sympy_cond = False
2449+
if sympy_cond is None:
2450+
sympy_cond = cond.to_sympy(**kwargs)
2451+
if not (sympy_cond.is_Relational or sympy_cond.is_Boolean):
2452+
return
2453+
2454+
sympy_cases = (
2455+
(expr.to_sympy(**kwargs), sympy_cond),
2456+
(sympy.Symbol(sympy_symbol_prefix + "System`Undefined"), True),
2457+
)
2458+
return sympy.Piecewise(*sympy_cases)

0 commit comments

Comments
 (0)