Commit 6139bd82 authored by Taddeus Kroes's avatar Taddeus Kroes

Merge branch 'master' into gonio

Conflicts:
	tests/test_rules_goniometry.py
parents ab2c06a8 da968378
......@@ -2,47 +2,9 @@
- Sort polynom by its exponents?
- No possibilities found for:
>>> a2b3 + a2b3
a ^ 2 * b ^ 3 + a ^ 2 * b ^ 3
- 2 + 3 + 4 rewrites to 5 instead of 5 + 4
-> the problem is that the 'root' of the application is actually a subtree
of the entire expression. This means that the parent of each possibility
root (or 'subtree') must me stored to be able to replace the subtree.
- MESSAGES needs to be expanded.
- rewrite match_combine_polynomes to an even more generic form:
match_combine_factors.
- "--ab + c" has no rewrite possibility. The graph of "--ab + c" is also
not valid:
-
+
╭─┴╮
* c
╭┴╮
- b
a
- The following expression gives a cycle in the possibilities:
>>> ab + ba
possibilities:
Group "ab" is multiplied by 1 and 1, combine them.
>>> (1 + 1) * ab
(1 + 1)ab
possibilities:
Combine the constants 1 and 1.
Group "1" is multiplied by 1 and 1, combine them.
Expand a(1 + 1).
Expand b(1 + 1).
- Fix division by zero caused by "0/0".
- Fix division by zero caused by "0/0": Catch exception in front-end
smvv@multivac ~/work/trs $ printf "a/0\n??" | ./main.py
Traceback (most recent call last):
......@@ -84,3 +46,10 @@ smvv@multivac ~/work/trs $ printf "0/1\n??" | ./main.py
<Possibility root="0 / 1" handler=divide_numerics args=(0, 1)>
Division of 0 by 1 reduces to 0.
Division of 0 by 1 reduces to 0.
- Fractions constant rewrite rules.
- >>> (sin x) ^ 2 + (cos x) ^ 2
sin(x) ^ 2 + cos(x) ^ 2
>>> sin(x) ^ 2 + cos(x) ^ 2
sin(x ^ 2) + cos(x ^ 2)
graph_drawing @ 21f0710c
Subproject commit 821bdb8f8408fb36ee1d92ada162589794a8c5b3
Subproject commit 21f0710c80184b6ea18827808ec40af924f0c8a8
......@@ -61,8 +61,10 @@ class Parser(BisonParser):
('left', ('COMMA', )),
('left', ('MINUS', 'PLUS')),
('left', ('TIMES', 'DIVIDE')),
('left', ('EQ', )),
('left', ('NEG', )),
('right', ('POW', )),
('right', ('SIN', 'COS', 'TAN', 'SOLVE', 'INT', 'SQRT')),
)
interactive = 0
......@@ -154,7 +156,7 @@ class Parser(BisonParser):
left, right = filter(None, match.groups())
# Filter words (otherwise they will be preprocessed as well)
if left + right in ['graph', 'raise']:
if (left + right).upper() in self.tokens:
return left + right
# If all characters on the right are numbers. e.g. "a4", the
......@@ -334,6 +336,12 @@ class Parser(BisonParser):
def on_unary(self, target, option, names, values):
"""
unary : MINUS exp %prec NEG
| SIN exp
| COS exp
| TAN exp
| INT exp
| SOLVE exp
| SQRT exp
"""
if option == 0: # rule: NEG exp
......@@ -346,6 +354,11 @@ class Parser(BisonParser):
return values[1]
if option < 7: # rule: SIN exp | COS exp | TAN exp | INT exp
if values[1].type == TYPE_OPERATOR and values[1].op == OP_COMMA:
return Node(values[0], *values[1])
return Node(*values)
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
......@@ -355,13 +368,14 @@ class Parser(BisonParser):
| exp TIMES exp
| exp DIVIDE exp
| exp POW exp
| exp EQ exp
| exp MINUS exp
"""
if 0 <= option < 4: # rule: exp {PLUS,TIMES,DIVIDES,POW} exp
if 0 <= option < 5: # rule: exp {PLUS,TIMES,DIVIDES,POW,EQ} exp
return Node(values[1], values[0], values[2])
if option == 4: # rule: exp MINUS exp
if option == 5: # rule: exp MINUS exp
node = values[2]
# Add negation to the left-most child
......@@ -436,7 +450,6 @@ class Parser(BisonParser):
[a-zA-Z] { returntoken(IDENTIFIER); }
"(" { returntoken(LPAREN); }
")" { returntoken(RPAREN); }
"," { returntoken(COMMA); }
""" + operators + r"""
"raise" { returntoken(RAISE); }
"graph" { returntoken(GRAPH); }
......
......@@ -11,13 +11,15 @@ from .fractions import match_constant_division, match_add_constant_fractions, \
match_expand_and_add_fractions
from .negation import match_negated_factor, match_negate_polynome, \
match_negated_division
from .sort import match_sort_multiplicants
RULES = {
OP_ADD: [match_add_numerics, match_add_constant_fractions,
match_combine_groups],
OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
match_expand_and_add_fractions, match_multiply_zero,
match_negated_factor, match_multiply_one],
match_negated_factor, match_multiply_one,
match_sort_multiplicants],
OP_DIV: [match_subtract_exponents, match_divide_numerics,
match_constant_division, match_negated_division],
OP_POW: [match_multiply_exponents, match_duplicate_exponent,
......
from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
OP_ADD, OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, OP_ADD, \
OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
def sin(*args):
return N('sin', *args)
def cos(*args):
return N('cos', *args)
def tan(*args):
return N('tan', *args)
def match_add_quadrants(node):
"""
sin(x) ^ 2 + cos(x) ^ 2 -> 1
......
from itertools import combinations
from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, Scope, \
OP_ADD, OP_MUL, nary_node, negate
from ..node import ExpressionLeaf as Leaf, Scope, OP_ADD, OP_MUL, nary_node, \
negate
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
......
......@@ -38,11 +38,6 @@ def add_numerics(root, args):
scope, c0, c1 = args
value = c0.actual_value() + c1.actual_value()
if value < 0:
leaf = Leaf(-value).negate()
else:
leaf = Leaf(value)
# Replace the left node with the new expression
scope.replace(c0, Leaf(abs(value)).negate(int(value < 0)))
......
from itertools import combinations
from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
OP_MUL, OP_DIV, OP_POW, OP_ADD
OP_MUL, OP_DIV, OP_POW, OP_ADD, negate
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
......@@ -12,6 +12,7 @@ def match_add_exponents(node):
a * a^q -> a^(1 + q)
a^p * a -> a^(p + 1)
a * a -> a^(1 + 1)
-a * a^q -> -a^(1 + q)
"""
assert node.is_op(OP_MUL)
......@@ -20,12 +21,12 @@ def match_add_exponents(node):
scope = Scope(node)
for n in scope:
# Order powers by their roots, e.g. a^p and a^q are put in the same
# list because of the mutual 'a'
if n.is_identifier():
s = n
s = negate(n, 0)
exponent = L(1)
elif n.is_op(OP_POW):
# Order powers by their roots, e.g. a^p and a^q are put in the same
# list because of the mutual 'a'
s, exponent = n
else: # pragma: nocover
continue
......@@ -53,8 +54,10 @@ def add_exponents(root, args):
"""
scope, n0, n1, a, p, q = args
# TODO: combine exponent negations
# Replace the left node with the new expression
scope.replace(n0, a ** (p + q))
scope.replace(n0, (a ** (p + q)).negate(n0.negated + n1.negated))
# Remove the right node
scope.remove(n1)
......
from itertools import product, combinations
from ..node import Scope, OP_ADD, OP_MUL
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
def match_sort_multiplicants(node):
"""
Sort multiplicant factors by swapping
x * 2 -> 2x
"""
assert node.is_op(OP_MUL)
p = []
scope = Scope(node)
for i, n in enumerate(scope[1:]):
left_nb = scope[i]
if n.is_numeric() and not left_nb.is_numeric():
p.append(P(node, move_constant, (scope, n, left_nb)))
return p
def move_constant(root, args):
scope, constant, destination = args
scope.replace(destination, constant * destination)
scope.remove(constant)
return scope.as_nary_node()
MESSAGES[move_constant] = \
_('Move constant {2} to the left of the multiplication {0}.')
from src.parser import Parser
from tests.parser import ParserWrapper
class ValidationNode(object):
pass
def validate(exp, result):
"""
Validate that exp =>* result.
"""
parser = ParserWrapper(Parser)
exp = parser.run([exp])
result = parser.run([result])
return validate_graph(exp, result)
def iter_preorder(exp, possibility):
"""
Traverse the possibility tree using pre-order traversal.
"""
pass
def validate_graph(exp, result):
"""
Validate that "exp" =>* "result".
"""
# TODO: Traverse the tree of possibility applications
return False
......@@ -4,10 +4,10 @@ from tests.rulestestcase import RulesTestCase as TestCase, rewrite
class TestLeidenOefenopgave(TestCase):
def test_1_1(self):
for chain in [['-5(x2 - 3x + 6)', '-5(x ^ 2 - 3x) - 5 * 6',
'-5 * x ^ 2 - 5 * -3x - 5 * 6',
'-5 * x ^ 2 - -15x - 5 * 6',
'-5 * x ^ 2 + 15x - 5 * 6',
'-5 * x ^ 2 + 15x - 30',
'-5x ^ 2 - 5 * -3x - 5 * 6',
'-5x ^ 2 - -15x - 5 * 6',
'-5x ^ 2 + 15x - 5 * 6',
'-5x ^ 2 + 15x - 30',
],
]:
self.assertRewrite(chain)
......@@ -15,11 +15,11 @@ class TestLeidenOefenopgave(TestCase):
return
for exp, solution in [
('-5(x2 - 3x + 6)', '-30 + 15 * x - 5 * x ^ 2'),
('(x+1)^2', 'x ^ 2 + 2 * x + 1'),
('(x-1)^2', 'x ^ 2 - 2 * x + 1'),
('(2x+x)*x', '3 * x ^ 2'),
('-2(6x-4)^2*x', '-72 * x^3 + 96 * x ^ 2 + 32 * x'),
('-5(x2 - 3x + 6)', '-30 + 15x - 5x ^ 2'),
('(x+1)^2', 'x ^ 2 + 2x + 1'),
('(x-1)^2', 'x ^ 2 - 2x + 1'),
('(2x+x)*x', '3x ^ 2'),
('-2(6x-4)^2*x', '-72x ^ 3 + 96x ^ 2 + 32x'),
('(4x + 5) * -(5 - 4x)', '16x^2 - 25'),
]:
self.assertEqual(str(rewrite(exp)), solution)
......@@ -36,20 +36,21 @@ class TestLeidenOefenopgave(TestCase):
'(x ^ 2 + 2x + 1 * 1)(x + 1)',
'(x ^ 2 + 2x + 1)(x + 1)',
'(x ^ 2 + 2x)x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x * x ^ 2 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'xx ^ 2 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ (1 + 2) + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + x * 2x + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + x ^ (1 + 1) * 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + x ^ 2 * 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + x ^ 2 * 2 + 1 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + x ^ 2 * 2 + x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + (2 + 1) * x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 2x + 1x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 2x + x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + (2 + 1)x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 3x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 3x + 1',
'x ^ 3 + 2xx + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + 2x ^ (1 + 1) + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + 2x ^ 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + 2x ^ 2 + 1x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 2x ^ 2 + x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + (2 + 1)x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 3x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 3x ^ 2 + 2x + 1x + 1 * 1',
'x ^ 3 + 3x ^ 2 + 2x + x + 1 * 1',
'x ^ 3 + 3x ^ 2 + (2 + 1)x + 1 * 1',
'x ^ 3 + 3x ^ 2 + 3x + 1 * 1',
'x ^ 3 + 3x ^ 2 + 3x + 1',
]
]:
self.assertRewrite(chain)
......@@ -115,7 +116,7 @@ class TestLeidenOefenopgave(TestCase):
def test_1_5(self):
self.assertRewrite(['(2x + x)x', '(2 + 1)xx', '3xx',
'3 * x ^ (1 + 1)', '3 * x ^ 2'])
'3x ^ (1 + 1)', '3x ^ 2'])
def test_1_7(self):
self.assertRewrite(['(4x + 5) * -(5 - 4x)',
......@@ -124,15 +125,15 @@ class TestLeidenOefenopgave(TestCase):
'4x * -5 + 4x * 4x + 5 * -5 + 5 * 4x',
'-20x + 4x * 4x + 5 * -5 + 5 * 4x',
'-20x + 16xx + 5 * -5 + 5 * 4x',
'-20x + 16 * x ^ (1 + 1) + 5 * -5 + 5 * 4x',
'-20x + 16 * x ^ 2 + 5 * -5 + 5 * 4x',
'-20x + 16 * x ^ 2 - 25 + 5 * 4x',
'-20x + 16 * x ^ 2 - 25 + 20x',
'(-20 + 20)x + 16 * x ^ 2 - 25',
'0x + 16 * x ^ 2 - 25',
'0 + 16 * x ^ 2 - 25',
'-25 + 16 * x ^ 2'])
# FIXME: '16 * x ^ 2 - 25'])
'-20x + 16x ^ (1 + 1) + 5 * -5 + 5 * 4x',
'-20x + 16x ^ 2 + 5 * -5 + 5 * 4x',
'-20x + 16x ^ 2 - 25 + 5 * 4x',
'-20x + 16x ^ 2 - 25 + 20x',
'(-20 + 20)x + 16x ^ 2 - 25',
'0x + 16x ^ 2 - 25',
'0 + 16x ^ 2 - 25',
'-25 + 16x ^ 2'])
# FIXME: '16x ^ 2 - 25'])
def test_2(self):
pass
......
......@@ -4,7 +4,7 @@ from tests.rulestestcase import RulesTestCase as TestCase, rewrite
class TestLeidenOefenopgaveV12(TestCase):
def test_1_e(self):
self.assertRewrite([
'-2(6x - 4) ^ 2 * x',
'-2(6x - 4) ^ 2x',
'-2(6x - 4)(6x - 4)x',
'(-2 * 6x - 2 * -4)(6x - 4)x',
'(-12x - 2 * -4)(6x - 4)x',
......@@ -12,23 +12,25 @@ class TestLeidenOefenopgaveV12(TestCase):
'(-12x + 8)(6x - 4)x',
'(-12x * 6x - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72xx - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72 * x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 + 48x + 48x + 8 * -4)x',
'(-72 * x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
'(-72 * x ^ 2 + 2 * 48x + 8 * -4)x',
'(-72 * x ^ 2 + 96x + 8 * -4)x',
'(-72 * x ^ 2 + 96x - 32)x',
'x(-72 * x ^ 2 + 96x) + x * -32',
'x * -72 * x ^ 2 + x * 96x + x * -32',
'-x * 72 * x ^ 2 + x * 96x + x * -32',
'-x * 72 * x ^ 2 + x ^ (1 + 1) * 96 + x * -32',
'-x * 72 * x ^ 2 + x ^ 2 * 96 + x * -32',
'-x * 72 * x ^ 2 + x ^ 2 * 96 - x * 32'])
# FIXME: '-x ^ (1 + 2) * 72 + x ^ 2 * 96 - x * 32',
# FIXME: '-x ^ 3 * 72 + x ^ 2 * 96 - x * 32',
# FIXME: '-72x ^ 3 + x ^ 2 * 96 - x * 32',
# FIXME: '-72x ^ 3 + 96x ^ 2 - x * 32',
# FIXME: '-72x ^ 3 + 96x ^ 2 - 32x'])
'(-72x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 + 48x + 48x + 8 * -4)x',
'(-72x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
'(-72x ^ 2 + 2 * 48x + 8 * -4)x',
'(-72x ^ 2 + 96x + 8 * -4)x',
'(-72x ^ 2 + 96x - 32)x',
'x(-72x ^ 2 + 96x) + x * -32',
'x * -72x ^ 2 + x * 96x + x * -32',
'-x * 72x ^ 2 + x * 96x + x * -32',
'72 * -xx ^ 2 + x * 96x + x * -32',
'-72xx ^ 2 + x * 96x + x * -32',
'-72x ^ (1 + 2) + x * 96x + x * -32',
'-72x ^ 3 + x * 96x + x * -32',
'-72x ^ 3 + 96xx + x * -32',
'-72x ^ 3 + 96x ^ (1 + 1) + x * -32',
'-72x ^ 3 + 96x ^ 2 + x * -32',
'-72x ^ 3 + 96x ^ 2 - x * 32',
'-72x ^ 3 + 96x ^ 2 + 32 * -x',
'-72x ^ 3 + 96x ^ 2 - 32x'])
......@@ -4,6 +4,8 @@ import unittest
from src.parser import Parser
from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
from tests.parser import ParserWrapper, run_expressions, line, graph
from tests.rulestestcase import tree
from src.rules.goniometry import sin, cos
class TestParser(unittest.TestCase):
......@@ -15,11 +17,11 @@ class TestParser(unittest.TestCase):
run_expressions(Parser, [('a', Leaf('a'))])
def test_graph(self):
assert graph(Parser, '4a') == ("""
self.assertEqual(graph(Parser, '4a'), ("""
*
╭┴╮
4 a
""").replace('\n ', '\n')[1:-1]
""").replace('\n ', '\n')[1:-1])
def test_line(self):
self.assertEqual(line(Parser, '4-a'), '4 - a')
......@@ -35,3 +37,14 @@ class TestParser(unittest.TestCase):
self.assertNotEqual(possibilities2, [])
self.assertNotEqual(possibilities1, possibilities2)
def test_functions(self):
root, x = tree('sin x, x')
self.assertEqual(root, sin(x))
self.assertEqual(tree('sin x ^ 2'), sin(x) ** 2)
self.assertEqual(tree('sin(x) ^ 2'), sin(x) ** 2)
self.assertEqual(tree('sin (x) ^ 2'), sin(x) ** 2)
self.assertEqual(tree('sin(x ^ 2)'), sin(x ** 2))
self.assertEqual(tree('sin cos x'), sin(cos(x)))
self.assertEqual(tree('sin cos x ^ 2'), sin(cos(x)) ** 2)
......@@ -6,7 +6,7 @@ from tests.rulestestcase import RulesTestCase, tree
class TestRulesGoniometry(RulesTestCase):
def test_match_add_quadrants(self):
root = tree('sin(x) ^ 2 + cos(x) ^ 2')
root = tree('sin x ^ 2 + cos x ^ 2')
possibilities = match_add_quadrants(root)
self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
......
......@@ -47,6 +47,14 @@ class TestRulesPowers(RulesTestCase):
self.assertEqualPos(possibilities,
[P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
def test_match_add_exponents_negated(self):
a, q = tree('a,q')
n0, n1 = root = (-a) * a ** q
possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities,
[P(root, add_exponents, (Scope(root), n0, n1, a, 1, q))])
def test_match_subtract_exponents_powers(self):
a, p, q = tree('a,p,q')
root = a ** p / a ** q
......
from src.rules.sort import match_sort_multiplicants, move_constant
from src.node import Scope
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
class TestRulesSort(RulesTestCase):
def test_match_sort_multiplicants(self):
x, l2 = root = tree('x * 2')
possibilities = match_sort_multiplicants(root)
self.assertEqualPos(possibilities,
[P(root, move_constant, (Scope(root), l2, x))])
def test_move_constant(self):
x, l2 = root = tree('x * 2')
self.assertEqualNodes(move_constant(root, (Scope(root), l2, x)),
l2 * x)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment