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 ...@@ -29,6 +29,7 @@ OP_MOD = 7
# N-ary (functions) # N-ary (functions)
OP_INT = 8 OP_INT = 8
OP_EXPAND = 9 OP_EXPAND = 9
OP_COMMA = 10
TYPE_MAP = { TYPE_MAP = {
...@@ -47,10 +48,50 @@ OP_MAP = { ...@@ -47,10 +48,50 @@ OP_MAP = {
'mod': OP_MOD, 'mod': OP_MOD,
'int': OP_INT, 'int': OP_INT,
'expand': OP_EXPAND, 'expand': OP_EXPAND,
',': OP_COMMA,
} }
class ExpressionBase(object): 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): def is_leaf(self):
return self.type != TYPE_OPERATOR return self.type != TYPE_OPERATOR
...@@ -94,26 +135,38 @@ class ExpressionNode(Node, ExpressionBase): ...@@ -94,26 +135,38 @@ class ExpressionNode(Node, ExpressionBase):
node.parent = self.parent node.parent = self.parent
self.parent = None self.parent = None
def get_polynome(self): def extract_polynome_properties(self):
""" """
Identifier nodes of all polynomes, tuple format is: Extract polynome properties into tuple format: (coefficient, root,
(root, exponent, coefficient, literal_exponent) 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 "get_polynome" -> "extract_polynome".
# TODO: change retval of c * r ^ e to (c, r, e). # 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 # rule: r ^ e -> (1, r, e)
# 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
if self.is_power(): if self.is_power():
return (self[0], self[1], ExpressionLeaf(1), True) return (ExpressionLeaf(1), self[0], self[1])
if self.op != OP_MUL: if self.op != OP_MUL:
return return
# rule: 3 * 7 ^ e | 'a' * 'b' ^ e
# expression: c * r ^ e ; tree: # expression: c * r ^ e ; tree:
# #
# * # *
...@@ -125,16 +178,25 @@ class ExpressionNode(Node, ExpressionBase): ...@@ -125,16 +178,25 @@ class ExpressionNode(Node, ExpressionBase):
# rule: c * r ^ e | (r ^ e) * c # rule: c * r ^ e | (r ^ e) * c
for i, j in [(0, 1), (1, 0)]: for i, j in [(0, 1), (1, 0)]:
if self[j].is_power(): 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 # 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): def get_scope(self):
"""""" """"""
scope = [] scope = []
#op = OP_ADD | OP_SUB if self.op & (OP_ADD | OP_SUB) else self.op #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: for child in self:
if not child.is_leaf() and child.op == self.op: if not child.is_leaf() and child.op == self.op:
scope += child.get_scope() scope += child.get_scope()
...@@ -150,13 +212,14 @@ class ExpressionLeaf(Leaf, ExpressionBase): ...@@ -150,13 +212,14 @@ class ExpressionLeaf(Leaf, ExpressionBase):
self.type = TYPE_MAP[type(args[0])] self.type = TYPE_MAP[type(args[0])]
def get_polynome(self): def extract_polynome_properties(self):
""" """
Identifier nodes of all polynomes, tuple format is: An expression leaf will return the polynome tuple (1, r, 1), where r is
(identifier, exponent, coefficient, literal_exponent) the leaf itself. See also the method extract_polynome_properties in
ExpressionBase.
""" """
# a = 1 * a ^ 1 # rule: 1 * r ^ 1 -> (1, r, 1)
return (self, ExpressionLeaf(1), ExpressionLeaf(1), False) return (ExpressionLeaf(1), self, ExpressionLeaf(1))
def replace(self, node): def replace(self, node):
if not hasattr(self, 'parent'): if not hasattr(self, 'parent'):
......
...@@ -19,24 +19,24 @@ sys.path.insert(1, EXTERNAL_MODS) ...@@ -19,24 +19,24 @@ sys.path.insert(1, EXTERNAL_MODS)
from pybison import BisonParser, BisonSyntaxError from pybison import BisonParser, BisonSyntaxError
from graph_drawing.graph import generate_graph from graph_drawing.graph import generate_graph
from node import TYPE_OPERATOR from node import TYPE_OPERATOR, OP_COMMA
from rules import RULES from rules import RULES
from possibilities import filter_duplicates from possibilities import filter_duplicates
## Check for n-ary operator in child nodes # Check for n-ary operator in child nodes
#def combine(op, op_type, *nodes): def combine(op, op_type, *nodes):
# # At least return the operator. # At least return the operator.
# res = [op] res = [op]
#
# for n in nodes: for n in nodes:
# # Merge the children for all nodes which have the same operator. # Merge the children for all nodes which have the same operator.
# if n.type == TYPE_OPERATOR and n.op == op_type: if n.type == TYPE_OPERATOR and n.op == op_type:
# res += n.nodes res += n.nodes
# else: else:
# res.append(n) res.append(n)
#
# return res return res
class Parser(BisonParser): class Parser(BisonParser):
...@@ -62,6 +62,7 @@ class Parser(BisonParser): ...@@ -62,6 +62,7 @@ class Parser(BisonParser):
# precedences # precedences
# ------------------------------ # ------------------------------
precedences = ( precedences = (
('left', ('COMMA', )),
('left', ('MINUS', 'PLUS')), ('left', ('MINUS', 'PLUS')),
('left', ('TIMES', 'DIVIDE')), ('left', ('TIMES', 'DIVIDE')),
('left', ('NEG', )), ('left', ('NEG', )),
...@@ -228,6 +229,7 @@ class Parser(BisonParser): ...@@ -228,6 +229,7 @@ class Parser(BisonParser):
| LPAREN exp RPAREN | LPAREN exp RPAREN
| unary | unary
| binary | binary
| nary
""" """
# | concat # | concat
...@@ -242,12 +244,9 @@ class Parser(BisonParser): ...@@ -242,12 +244,9 @@ class Parser(BisonParser):
if option == 2: # rule: LPAREN exp RPAREN if option == 2: # rule: LPAREN exp RPAREN
return values[1] return values[1]
if option in [3, 4]: # rule: unary | binary if option in [3, 4, 5]: # rule: unary | binary | nary
return values[0] return values[0]
#if option in [3, 4, 5]: # rule: unary | binary | concat
# return values[0]
raise BisonSyntaxError('Unsupported option %d in target "%s".' raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover % (option, target)) # pragma: nocover
...@@ -277,6 +276,17 @@ class Parser(BisonParser): ...@@ -277,6 +276,17 @@ class Parser(BisonParser):
raise BisonSyntaxError('Unsupported option %d in target "%s".' raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover % (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): #def on_concat(self, option, target, names, values):
# """ # """
# concat : exp IDENTIFIER %prec TIMES # concat : exp IDENTIFIER %prec TIMES
......
from ..node import OP_ADD, OP_MUL from ..node import OP_ADD, OP_MUL
from .poly import match_combine_factors, match_expand from .poly import match_combine_polynomes, match_expand
RULES = { RULES = {
OP_ADD: [match_combine_factors], OP_ADD: [match_combine_polynomes],
OP_MUL: [match_expand], OP_MUL: [match_expand],
} }
from itertools import combinations from itertools import combinations
from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, \ from ..node import ExpressionNode as Node, TYPE_OPERATOR, OP_ADD, OP_MUL
TYPE_OPERATOR, OP_ADD, OP_MUL
from ..possibilities import Possibility as P from ..possibilities import Possibility as P
from .utils import nary_node from .utils import nary_node
...@@ -55,7 +54,7 @@ def expand_single(root, args): ...@@ -55,7 +54,7 @@ def expand_single(root, args):
return nary_node('*', scope) return nary_node('*', scope)
def match_combine_factors(node): def match_combine_polynomes(node, verbose=False):
""" """
n + exp + m -> exp + (n + m) n + exp + m -> exp + (n + m)
k0 * v ^ n + exp + k1 * v ^ n -> exp + (k0 + k1) * v ^ n k0 * v ^ n + exp + k1 * v ^ n -> exp + (k0 + k1) * v ^ n
...@@ -75,10 +74,13 @@ def match_combine_factors(node): ...@@ -75,10 +74,13 @@ def match_combine_factors(node):
# (root, exponent, coefficient, literal_coefficient) # (root, exponent, coefficient, literal_coefficient)
polys = [] polys = []
if verbose:
print 'match combine factors:', node print 'match combine factors:', node
for n in node.get_scope(): for n in node.get_scope():
polynome = n.get_polynome() polynome = n.extract_polynome_properties()
if verbose:
print 'n:', n, 'polynome:', polynome print 'n:', n, 'polynome:', polynome
if polynome: if polynome:
...@@ -87,14 +89,13 @@ def match_combine_factors(node): ...@@ -87,14 +89,13 @@ def match_combine_factors(node):
# Each combination of powers of the same value and polynome can be added # Each combination of powers of the same value and polynome can be added
if len(polys) >= 2: if len(polys) >= 2:
for left, right in combinations(polys, 2): for left, right in combinations(polys, 2):
r0, e0, c0 = left[1][:3] c0, r0, e0 = left[1]
r1, e1, c1 = right[1][:3] c1, r1, e1 = right[1]
if (r0.is_numeric() and r1.is_numeric() and e0 == e1 == Leaf(1)) \ # Both numeric root and same exponent -> combine coefficients and
or (r0 == r1 and e0 == e1): # roots, or: same root and exponent -> combine coefficients.
# Both numeric and same exponent -> combine coefficients and if ((r0.is_numeric() and r1.is_numeric()) or r0 == r1) \
# roots, or: and e0 == e1:
# Same identifier and exponent -> combine coefficients
p.append(P(node, combine_polynomes, (left, right))) p.append(P(node, combine_polynomes, (left, right)))
return p return p
...@@ -102,7 +103,7 @@ def match_combine_factors(node): ...@@ -102,7 +103,7 @@ def match_combine_factors(node):
def combine_polynomes(root, args): 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: Example:
c * a ^ b + d * a ^ b -> (c + d) * a ^ b c * a ^ b + d * a ^ b -> (c + d) * a ^ b
...@@ -113,17 +114,13 @@ def combine_polynomes(root, args): ...@@ -113,17 +114,13 @@ def combine_polynomes(root, args):
# TODO: verify that the two commented expression below are invalid and the # TODO: verify that the two commented expression below are invalid and the
# following two expressions are right. # following two expressions are right.
r0, e0, c0 = pl c0, r0, e0 = pl
r1, e1, c1 = pr 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)
if r0.is_numeric() and r1.is_numeric() and e0 == e1 == 1: #else:
new_root = Leaf(r0.value + r1.value) # new_root = r0
else:
new_root = r0
if pl[3] or pr[3]: if pl[3] or pr[3]:
# literal a ^ 1 -> a ^ 1 # literal a ^ 1 -> a ^ 1
...@@ -136,6 +133,8 @@ def combine_polynomes(root, args): ...@@ -136,6 +133,8 @@ def combine_polynomes(root, args):
# a, b and c are from 'left', d is from 'right'. # a, b and c are from 'left', d is from 'right'.
replacement = Node('*', Node('+', pl[2], pr[2]), power) replacement = Node('*', Node('+', pl[2], pr[2]), power)
scope = root.get_scope()
# Replace the left node with the new expression # Replace the left node with the new expression
scope[scope.index(nl)] = replacement scope[scope.index(nl)] = replacement
......
...@@ -51,23 +51,27 @@ class TestNode(unittest.TestCase): ...@@ -51,23 +51,27 @@ class TestNode(unittest.TestCase):
self.assertTrue(L(1.5).is_numeric()) self.assertTrue(L(1.5).is_numeric())
self.assertFalse(L('a').is_numeric()) self.assertFalse(L('a').is_numeric())
def test_get_polynome_identifier(self): def test_extract_polynome_properties_identifier(self):
self.assertEqual(L('a').get_polynome(), (L('a'), L(1), L(1), False)) self.assertEqual(L('a').extract_polynome_properties(),
(L(1), L('a'), L(1)))
def test_get_polynome_None(self): def test_extract_polynome_properties_None(self):
self.assertIsNone(N('+').get_polynome()) 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)) 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))) 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'))) 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): def test_get_scope_binary(self):
plus = N('+', *self.l[:2]) plus = N('+', *self.l[:2])
......
import unittest import unittest
from src.node import ExpressionNode as N, ExpressionLeaf as L 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.possibilities import Possibility as P
from src.parser import Parser from src.parser import Parser
from tests.parser import ParserWrapper from tests.parser import ParserWrapper
...@@ -13,10 +13,10 @@ def tree(exp, **kwargs): ...@@ -13,10 +13,10 @@ def tree(exp, **kwargs):
class TestRulesPoly(unittest.TestCase): 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) # l0, l1, l2 = L(1), L(2), L(2)
# plus = N('+', N('+', l0, l1), l2) # 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)), # self.assertEqualPos(p, [P(plus, combine_polynomes, (l0, l1)),
# P(plus, combine_polynomes, (l0, l2)), # P(plus, combine_polynomes, (l0, l2)),
# P(plus, combine_polynomes, (l1, l2))]) # P(plus, combine_polynomes, (l1, l2))])
...@@ -32,63 +32,59 @@ class TestRulesPoly(unittest.TestCase): ...@@ -32,63 +32,59 @@ class TestRulesPoly(unittest.TestCase):
self.assertEqual(p, e) self.assertEqual(p, e)
def test_basic(self): def test_numbers(self):
l1, l2 = root = tree('1+2') l1, l2 = root = tree('1+2')
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((l1, (l1, l1, l1, False)), [P(root, combine_polynomes, ((l1, (l1, l1, l1)),
(l2, (l2, l1, l1, False))))]) (l2, (l1, l2, l1))))])
def test_identifiers_basic(self):
l1, l2 = tree('1,2')
a1, a2 = root = tree('a+a') a1, a2 = root = tree('a+a')
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (a1, l1, l1, False)), [P(root, combine_polynomes, ((a1, (l1, a1, l1)),
(a2, (a2, l1, l1, False))))]) (a2, (l1, a2, l1))))])
def test_identifiers_normal(self):
l1, l2 = tree('1,2')
a1, a2 = root = tree('a+2a') a1, a2 = root = tree('a+2a')
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (a1, l1, l1, False)), [P(root, combine_polynomes, ((a1, (l1, a1, l1)),
(a2, (a2[1], l1, l2, False))))]) (a2, (l2, a2[1], l1))))])
def test_identifiers_reverse(self):
l1, l2, la = tree('1,2,a')
a1, a2 = root = tree('a+a*2') a1, a2 = root = tree('a+a*2')
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (a1, l1, l1, False)), [P(root, combine_polynomes, ((a1, (l1, a1, l1)),
(a2, (a2[1], l1, l2, False))))]) (a2, (l2, la, l1))))])
def test_identifiers_exponent(self):
l1, l2 = tree('1,2')
a1, a2 = root = tree('a2+a2') a1, a2 = root = tree('a2+a2')
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((a1, (a1[0], l2, l1, True)), [P(root, combine_polynomes, ((a1, (l1, a1[0], l2)),
(a2, (a2[0], l2, l1, True))))]) (a2, (l1, a2[0], l2))))])
def test_basic_subexpressions(self): def test_basic_subexpressions(self):
return # TODO: test this!! a_b, c, d, l1, l5, l7 = tree('a+b,c,d,1,5,7')
a_b = tree('a+b')
c, d = tree('c+d')
l1 = tree('1')
l5, l7 = tree('5+7')
left, right = root = tree('(a+b)^d + (a+b)^d') left, right = root = tree('(a+b)^d + (a+b)^d')
self.assertEqual(left, right) self.assertEqual(left, right)
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((left, (a_b, d, l1, True)), [P(root, combine_polynomes, ((left, (l1, a_b, d)),
(right, (a_b, d, l1, True))))]) (right, (l1, a_b, d))))])
left, right = root = tree('5(a+b)^d + 7(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" self.assertEqualPos(match_combine_polynomes(root),
# handler=combine_polynomes args=((<src.node.ExpressionNode object at [P(root, combine_polynomes, ((left, (l5, a_b, d)),
# 0x9fb2e0c>, (<src.node.ExpressionNode object at 0x9fb2c2c>, (right, (l7, a_b, d))))])
# '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))))])
left, right = root = tree('c(a+b)^d + c(a+b)^d') left, right = root = tree('c(a+b)^d + c(a+b)^d')
self.assertEqual(left, right) self.assertEqual(left, right)
self.assertEqualPos(match_combine_factors(root), self.assertEqualPos(match_combine_polynomes(root),
[P(root, combine_polynomes, ((left, (left[0], c, d, True)), [P(root, combine_polynomes, ((left, (c, a_b, d)),
(right, (right[0], c, d, True))))]) (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