Implemented node comparison and normalisation.

 - Node comparison using the __lt__ operator is implemented.
 - Node normalisation is implemented in extract_polynome_properties.
 - Enabled the remaining polynome test cases (all unit tests succeed).
 - Comma can be used to generate multiple expression leafs and nodes at once.
parent 083c7661
......@@ -29,6 +29,7 @@ OP_MOD = 7
# N-ary (functions)
OP_INT = 8
OP_EXPAND = 9
OP_COMMA = 10
TYPE_MAP = {
......@@ -47,10 +48,50 @@ OP_MAP = {
'mod': OP_MOD,
'int': OP_INT,
'expand': OP_EXPAND,
',': OP_COMMA,
}
class ExpressionBase(object):
def __lt__(self, other):
"""
Comparison between this expression{node,leaf} and another
expression{node,leaf}. This comparison will return True if this
instance has less value than the other expression{node,leaf}.
Otherwise, False is returned.
The comparison is based on the following conditions:
1. Both are leafs. String comparison of the value is used.
2. This is a leaf and other is a node. This leaf has less value, thus
True is returned.
3. This is a node and other is a leaf. This leaf has more value, thus
False is returned.
4. Both are nodes. Compare the polynome properties of the nodes. True
is returned if this node's root property is less than other's root
property, or this node's exponent property is less than other's
exponent property, or this node's coefficient property is less than
other's coefficient property. Otherwise, False is returned.
"""
if self.is_leaf():
if other.is_leaf():
# Both are leafs, string compare the value.
return str(self.value) < str(other.value)
# Self is a leaf, thus has less value than an expression node.
return True
if other.is_leaf():
# Self is an expression node, and the other is a leaf. Thus, other
# is greater than self.
return False
# Both are nodes, compare the polynome properties.
s_coeff, s_root, s_exp = self.extract_polynome_properties()
o_coeff, o_root, o_exp = other.extract_polynome_properties()
return s_root < o_root or s_exp < or_exp or s_coeff < or_coeff
def is_leaf(self):
return self.type != TYPE_OPERATOR
......@@ -94,26 +135,38 @@ class ExpressionNode(Node, ExpressionBase):
node.parent = self.parent
self.parent = None
def get_polynome(self):
def extract_polynome_properties(self):
"""
Identifier nodes of all polynomes, tuple format is:
(root, exponent, coefficient, literal_exponent)
Extract polynome properties into tuple format: (coefficient, root,
exponent). Thus: c * r ^ e will be extracted into the tuple (c, r, e).
This function will normalize the expression before extracting the
properties. Therefore, the expression r ^ e * c results the same tuple
(c, r, e) as the expression c * r ^ e.
>>> from src.node import ExpressionNode as N, ExpressionLeaf as L
>>> c, r, e = L('c'), L('r'), L('e')
>>> n1 = N('*', c, N('^', r, e))
>>> n1.extract_polynome()
(c, r, e)
>>> n2 = N('*', N('^', r, e), c)
>>> n2.extract_polynome()
(c, r, e)
"""
# TODO: change "get_polynome" -> "extract_polynome".
# TODO: change retval of c * r ^ e to (c, r, e).
# was: (root, exponent, coefficient, literal_exponent)
# TODO: normalize c * r and r * c -> c * r. Otherwise, the tuple will
# not match if the order of the expression is different. Example:
# r ^ e * c == c * r ^ e
# without normalization, those expressions will not match.
# rule: r ^ e
# rule: r ^ e -> (1, r, e)
if self.is_power():
return (self[0], self[1], ExpressionLeaf(1), True)
return (ExpressionLeaf(1), self[0], self[1])
if self.op != OP_MUL:
return
# rule: 3 * 7 ^ e | 'a' * 'b' ^ e
# expression: c * r ^ e ; tree:
#
# *
......@@ -125,16 +178,25 @@ class ExpressionNode(Node, ExpressionBase):
# rule: c * r ^ e | (r ^ e) * c
for i, j in [(0, 1), (1, 0)]:
if self[j].is_power():
return (self[j][0], self[j][1], self[i], True)
return (self[i], self[j][0], self[j][1])
# Normalize c * r and r * c -> c * r. Otherwise, the tuple will not
# match if the order of the expression is different. Example:
# r ^ e * c == c * r ^ e
# without normalization, those expressions will not match.
#
# rule: c * r | r * c
return (self[0], ExpressionLeaf(1), self[1], False)
if self[0] < self[1]:
return (self[0], self[1], ExpressionLeaf(1))
return (self[1], self[0], ExpressionLeaf(1))
def get_scope(self):
""""""
scope = []
#op = OP_ADD | OP_SUB if self.op & (OP_ADD | OP_SUB) else self.op
# TODO: what to do with OP_SUB and OP_ADD in get_scope?
for child in self:
if not child.is_leaf() and child.op == self.op:
scope += child.get_scope()
......@@ -150,13 +212,14 @@ class ExpressionLeaf(Leaf, ExpressionBase):
self.type = TYPE_MAP[type(args[0])]
def get_polynome(self):
def extract_polynome_properties(self):
"""
Identifier nodes of all polynomes, tuple format is:
(identifier, exponent, coefficient, literal_exponent)
An expression leaf will return the polynome tuple (1, r, 1), where r is
the leaf itself. See also the method extract_polynome_properties in
ExpressionBase.
"""
# a = 1 * a ^ 1
return (self, ExpressionLeaf(1), ExpressionLeaf(1), False)
# rule: 1 * r ^ 1 -> (1, r, 1)
return (ExpressionLeaf(1), self, ExpressionLeaf(1))
def replace(self, node):
if not hasattr(self, 'parent'):
......
......@@ -19,24 +19,24 @@ sys.path.insert(1, EXTERNAL_MODS)
from pybison import BisonParser, BisonSyntaxError
from graph_drawing.graph import generate_graph
from node import TYPE_OPERATOR
from node import TYPE_OPERATOR, OP_COMMA
from rules import RULES
from possibilities import filter_duplicates
## Check for n-ary operator in child nodes
#def combine(op, op_type, *nodes):
# # At least return the operator.
# res = [op]
#
# for n in nodes:
# # Merge the children for all nodes which have the same operator.
# if n.type == TYPE_OPERATOR and n.op == op_type:
# res += n.nodes
# else:
# res.append(n)
#
# return res
# Check for n-ary operator in child nodes
def combine(op, op_type, *nodes):
# At least return the operator.
res = [op]
for n in nodes:
# Merge the children for all nodes which have the same operator.
if n.type == TYPE_OPERATOR and n.op == op_type:
res += n.nodes
else:
res.append(n)
return res
class Parser(BisonParser):
......@@ -62,6 +62,7 @@ class Parser(BisonParser):
# precedences
# ------------------------------
precedences = (
('left', ('COMMA', )),
('left', ('MINUS', 'PLUS')),
('left', ('TIMES', 'DIVIDE')),
('left', ('NEG', )),
......@@ -228,6 +229,7 @@ class Parser(BisonParser):
| LPAREN exp RPAREN
| unary
| binary
| nary
"""
# | concat
......@@ -242,12 +244,9 @@ class Parser(BisonParser):
if option == 2: # rule: LPAREN exp RPAREN
return values[1]
if option in [3, 4]: # rule: unary | binary
if option in [3, 4, 5]: # rule: unary | binary | nary
return values[0]
#if option in [3, 4, 5]: # rule: unary | binary | concat
# return values[0]
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
......@@ -277,6 +276,17 @@ class Parser(BisonParser):
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
def on_nary(self, target, option, names, values):
"""
nary : exp COMMA exp
"""
if option == 0: # rule: exp COMMA exp
return Node(*combine(',', OP_COMMA, values[0], values[2]))
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
#def on_concat(self, option, target, names, values):
# """
# concat : exp IDENTIFIER %prec TIMES
......
from ..node import OP_ADD, OP_MUL
from .poly import match_combine_factors, match_expand
from .poly import match_combine_polynomes, match_expand
RULES = {
OP_ADD: [match_combine_factors],
OP_ADD: [match_combine_polynomes],
OP_MUL: [match_expand],
}
from itertools import combinations
from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, \
TYPE_OPERATOR, OP_ADD, OP_MUL
from ..node import ExpressionNode as Node, TYPE_OPERATOR, OP_ADD, OP_MUL
from ..possibilities import Possibility as P
from .utils import nary_node
......@@ -55,7 +54,7 @@ def expand_single(root, args):
return nary_node('*', scope)
def match_combine_factors(node):
def match_combine_polynomes(node, verbose=False):
"""
n + exp + m -> exp + (n + m)
k0 * v ^ n + exp + k1 * v ^ n -> exp + (k0 + k1) * v ^ n
......@@ -75,10 +74,13 @@ def match_combine_factors(node):
# (root, exponent, coefficient, literal_coefficient)
polys = []
if verbose:
print 'match combine factors:', node
for n in node.get_scope():
polynome = n.get_polynome()
polynome = n.extract_polynome_properties()
if verbose:
print 'n:', n, 'polynome:', polynome
if polynome:
......@@ -87,14 +89,13 @@ def match_combine_factors(node):
# Each combination of powers of the same value and polynome can be added
if len(polys) >= 2:
for left, right in combinations(polys, 2):
r0, e0, c0 = left[1][:3]
r1, e1, c1 = right[1][:3]
if (r0.is_numeric() and r1.is_numeric() and e0 == e1 == Leaf(1)) \
or (r0 == r1 and e0 == e1):
# Both numeric and same exponent -> combine coefficients and
# roots, or:
# Same identifier and exponent -> combine coefficients
c0, r0, e0 = left[1]
c1, r1, e1 = right[1]
# Both numeric root and same exponent -> combine coefficients and
# roots, or: same root and exponent -> combine coefficients.
if ((r0.is_numeric() and r1.is_numeric()) or r0 == r1) \
and e0 == e1:
p.append(P(node, combine_polynomes, (left, right)))
return p
......@@ -102,7 +103,7 @@ def match_combine_factors(node):
def combine_polynomes(root, args):
"""
Combine two identifier multiplications of any polynome in an n-ary plus.
Combine two multiplications of any polynome in an n-ary plus.
Example:
c * a ^ b + d * a ^ b -> (c + d) * a ^ b
......@@ -113,17 +114,13 @@ def combine_polynomes(root, args):
# TODO: verify that the two commented expression below are invalid and the
# following two expressions are right.
r0, e0, c0 = pl
r1, e1, c1 = pr
#c0, r0, e0 = pl
#c1, r1, e1 = pr
c0, r0, e0 = pl
c1, r1, e1 = pr
scope = root.get_scope()
if r0.is_numeric() and r1.is_numeric() and e0 == e1 == 1:
new_root = Leaf(r0.value + r1.value)
else:
new_root = r0
#if r0.is_numeric() and r1.is_numeric() and e0 == e1 == 1:
# new_root = Leaf(r0.value + r1.value)
#else:
# new_root = r0
if pl[3] or pr[3]:
# literal a ^ 1 -> a ^ 1
......@@ -136,6 +133,8 @@ def combine_polynomes(root, args):
# a, b and c are from 'left', d is from 'right'.
replacement = Node('*', Node('+', pl[2], pr[2]), power)
scope = root.get_scope()
# Replace the left node with the new expression
scope[scope.index(nl)] = replacement
......
......@@ -51,23 +51,27 @@ class TestNode(unittest.TestCase):
self.assertTrue(L(1.5).is_numeric())
self.assertFalse(L('a').is_numeric())
def test_get_polynome_identifier(self):
self.assertEqual(L('a').get_polynome(), (L('a'), L(1), L(1), False))
def test_extract_polynome_properties_identifier(self):
self.assertEqual(L('a').extract_polynome_properties(),
(L(1), L('a'), L(1)))
def test_get_polynome_None(self):
self.assertIsNone(N('+').get_polynome())
def test_extract_polynome_properties_None(self):
self.assertIsNone(N('+').extract_polynome_properties())
def test_get_polynome_power(self):
def test_extract_polynome_properties_power(self):
power = N('^', L('a'), L(2))
self.assertEqual(power.get_polynome(), (L('a'), L(2), L(1), True))
self.assertEqual(power.extract_polynome_properties(),
(L(1), L('a'), L(2)))
def test_get_polynome_coefficient_exponent_int(self):
def test_extract_polynome_properties_coefficient_exponent_int(self):
times = N('*', L(3), N('^', L('a'), L(2)))
self.assertEqual(times.get_polynome(), (L('a'), L(2), L(3), True))
self.assertEqual(times.extract_polynome_properties(),
(L(3), L('a'), L(2)))
def test_get_polynome_coefficient_exponent_id(self):
def test_extract_polynome_properties_coefficient_exponent_id(self):
times = N('*', L(3), N('^', L('a'), L('b')))
self.assertEqual(times.get_polynome(), (L('a'), L('b'), L(3), True))
self.assertEqual(times.extract_polynome_properties(),
(L(3), L('a'), L('b')))
def test_get_scope_binary(self):
plus = N('+', *self.l[:2])
......
import unittest
from src.node import ExpressionNode as N, ExpressionLeaf as L
from src.rules.poly import match_combine_factors, combine_polynomes
from src.rules.poly import match_combine_polynomes, combine_polynomes
from src.possibilities import Possibility as P
from src.parser import Parser
from tests.parser import ParserWrapper
......@@ -13,10 +13,10 @@ def tree(exp, **kwargs):
class TestRulesPoly(unittest.TestCase):
#def test_match_combine_factors_numeric_combinations(self):
#def test_match_combine_polynomes_numeric_combinations(self):
# l0, l1, l2 = L(1), L(2), L(2)
# plus = N('+', N('+', l0, l1), l2)
# p = match_combine_factors(plus)
# p = match_combine_polynomes(plus)
# self.assertEqualPos(p, [P(plus, combine_polynomes, (l0, l1)),
# P(plus, combine_polynomes, (l0, l2)),
# P(plus, combine_polynomes, (l1, l2))])
......@@ -32,63 +32,59 @@ class TestRulesPoly(unittest.TestCase):
self.assertEqual(p, e)
def test_basic(self):
def test_numbers(self):
l1, l2 = root = tree('1+2')
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((l1, (l1, l1, l1, False)),
(l2, (l2, l1, l1, False))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((l1, (l1, l1, l1)),
(l2, (l1, l2, l1))))])
def test_identifiers_basic(self):
l1, l2 = tree('1,2')
a1, a2 = root = tree('a+a')
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((a1, (a1, l1, l1, False)),
(a2, (a2, l1, l1, False))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (l1, a1, l1)),
(a2, (l1, a2, l1))))])
def test_identifiers_normal(self):
l1, l2 = tree('1,2')
a1, a2 = root = tree('a+2a')
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((a1, (a1, l1, l1, False)),
(a2, (a2[1], l1, l2, False))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (l1, a1, l1)),
(a2, (l2, a2[1], l1))))])
def test_identifiers_reverse(self):
l1, l2, la = tree('1,2,a')
a1, a2 = root = tree('a+a*2')
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((a1, (a1, l1, l1, False)),
(a2, (a2[1], l1, l2, False))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (l1, a1, l1)),
(a2, (l2, la, l1))))])
def test_identifiers_exponent(self):
l1, l2 = tree('1,2')
a1, a2 = root = tree('a2+a2')
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((a1, (a1[0], l2, l1, True)),
(a2, (a2[0], l2, l1, True))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (l1, a1[0], l2)),
(a2, (l1, a2[0], l2))))])
def test_basic_subexpressions(self):
return # TODO: test this!!
a_b = tree('a+b')
c, d = tree('c+d')
l1 = tree('1')
l5, l7 = tree('5+7')
a_b, c, d, l1, l5, l7 = tree('a+b,c,d,1,5,7')
left, right = root = tree('(a+b)^d + (a+b)^d')
self.assertEqual(left, right)
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((left, (a_b, d, l1, True)),
(right, (a_b, d, l1, True))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((left, (l1, a_b, d)),
(right, (l1, a_b, d))))])
left, right = root = tree('5(a+b)^d + 7(a+b)^d')
#<Possibility root="5 * (a + b) ^ d + 7 * (a + b) ^ d"
# handler=combine_polynomes args=((<src.node.ExpressionNode object at
# 0x9fb2e0c>, (<src.node.ExpressionNode object at 0x9fb2c2c>,
# 'd', 5, True)), (<src.node.ExpressionNode object at
# 0x9fb438c>, (<src.node.ExpressionNode object at
# 0x9fb2f0c>, 'd', 7, True)))>
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((left, (a_b, d, l5, True)),
(right, (a_b, d, l7, True))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((left, (l5, a_b, d)),
(right, (l7, a_b, d))))])
left, right = root = tree('c(a+b)^d + c(a+b)^d')
self.assertEqual(left, right)
self.assertEqualPos(match_combine_factors(root),
[P(root, combine_polynomes, ((left, (left[0], c, d, True)),
(right, (right[0], c, d, True))))])
self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((left, (c, a_b, d)),
(right, (c, a_b, d))))])
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