Skip to content

Commit c23b36c

Browse files
committed
RPN First-order formula & Prenex Normal Form reduction.
1 parent 96ecacb commit c23b36c

File tree

2 files changed

+204
-19
lines changed

2 files changed

+204
-19
lines changed

src/asp_graph.py

+35-12
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import kivy.uix.label as label
1212
import kivy.animation as anim
1313

14-
from normalization import LIT
14+
from normalization import LIT, OP
1515
from name_manager import NameManager
1616

1717
# Enum class for drawing modes
@@ -373,13 +373,12 @@ def get_first_order_formula(self, quantified_vars=set()):
373373
conjunction = eqlist
374374
disjuntion = []
375375
for ch in self.children:
376+
if isinstance(ch, NexusWidget):
377+
continue
378+
subformula = ch.get_first_order_formula(quantified_vars.union(new_quants))
376379
if isinstance(ch, SquareWidget):
377-
subformula = ch.get_first_order_formula(quantified_vars.union(new_quants))
378380
disjuntion.append(subformula)
379-
elif isinstance(ch, NexusWidget):
380-
continue
381381
else:
382-
subformula = ch.get_first_order_formula(quantified_vars.union(new_quants))
383382
conjunction.append(subformula)
384383
if conjunction == []:
385384
conjunction.append('True')
@@ -400,40 +399,64 @@ def get_first_order_formula(self, quantified_vars=set()):
400399
def get_formula(self):
401400
return self.get_first_order_formula()
402401

403-
def get_formula_RPN(self):
402+
def get_formula_RPN(self, quantified_vars=set()):
403+
404+
def apply_quantifiers(string, quants, qtype):
405+
for q in quants:
406+
string = q + ' ' + string + ' ' + qtype
407+
return string
408+
404409
s = ''
405410
if isinstance(self, AtomWidget):
406-
s = self.text
411+
s = self.get_as_text()
407412
else:
413+
# Get quantifiers and equalities at this level
414+
new_quants, new_eqs = self.get_quantifiers_and_equalities()
415+
new_quants = new_quants.difference(quantified_vars)
416+
eqlist = [eq[0] + '=' + eq[1] for eq in new_eqs]
417+
existentials, universals = [], []
418+
for q in new_quants:
419+
if self.get_quantifier_type(q).startswith('Exists'):
420+
existentials.append(q)
421+
else:
422+
universals.append(q)
423+
408424
if isinstance(self, EllipseWidget):
409425
squares = []
410-
rest = []
426+
rest = eqlist
411427
for ch in self.children:
428+
if isinstance(ch, NexusWidget):
429+
continue
412430
if isinstance(ch, SquareWidget):
413-
squares.append(ch.get_formula_RPN())
431+
squares.append(ch.get_formula_RPN(quantified_vars.union(new_quants)))
414432
else:
415-
rest.append(ch.get_formula_RPN())
433+
rest.append(ch.get_formula_RPN(quantified_vars.union(new_quants)))
416434
if (len(rest) == 0):
417435
s += LIT.TRUE
418436
if len(rest) > 0:
419437
s += rest.pop()
420438
while rest <> []:
421439
s += ' ' + rest.pop() + ' &'
440+
s = apply_quantifiers(s, existentials, OP.EXISTS)
422441
if (len(squares) == 0):
423442
s += ' ' + LIT.FALSE
424443
if len(squares) > 0:
425444
s += ' ' + squares.pop()
426445
while squares <> []:
427446
s += ' ' + squares.pop() + ' |'
428447
s += ' >'
448+
s = apply_quantifiers(s, universals, OP.FORALL)
429449
else:
430-
l = []
450+
l = eqlist
431451
for ch in self.children:
432-
l.append(ch.get_formula_RPN())
452+
if isinstance(ch, NexusWidget):
453+
continue
454+
l.append(ch.get_formula_RPN(quantified_vars.union(new_quants)))
433455
if len(l) > 0:
434456
s += l.pop()
435457
while l <> []:
436458
s += ' ' + l.pop() + ' &'
459+
s = apply_quantifiers(s, existentials, OP.EXISTS)
437460
return s
438461

439462
class Segment:

src/normalization.py

+169-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# -*- coding: utf-8 -*-
22

33
"""NORMALIZATION MODULE
4+
45
Transforms HT propositional formulas into logic programs with the form:
56
p1 & p2 & ... & pN -> q1 | q2 | ... | qM
7+
8+
Also transforms HT first order formulas into Prenex Normal Form.
69
"""
710

811
import string
@@ -14,6 +17,8 @@ class OP:
1417
IMPLIES = '>'
1518
AND = '&'
1619
OR = '|'
20+
EXISTS = '/E'
21+
FORALL = '/F'
1722

1823
class LIT:
1924
"""Enum class for true & false values"""
@@ -26,6 +31,15 @@ def __init__(self, val, left=None, right=None):
2631
self.l = left
2732
self.r = right
2833

34+
def __repr__(self):
35+
s = ''
36+
if self.l is not None:
37+
s += self.l.__repr__() + ' '
38+
if self.r is not None:
39+
s += self.r.__repr__() + ' '
40+
s += self.val
41+
return s
42+
2943
def __eq__(self, node):
3044
v = False
3145
l = False
@@ -48,9 +62,16 @@ def __eq__(self, node):
4862
v = True
4963
return (v and l and r)
5064

65+
def is_quantifier(self):
66+
if (self.val == OP.EXISTS) or (self.val == OP.FORALL):
67+
return True
68+
else:
69+
return False
70+
5171
def is_literal(self):
5272
if ((self.val == OP.NOT) or (self.val == OP.IMPLIES)
53-
or (self.val == OP.AND) or (self.val == OP.OR)):
73+
or (self.val == OP.AND) or (self.val == OP.OR)
74+
or (self.val == OP.EXISTS) or (self.val == OP.FORALL)):
5475
return False
5576
else:
5677
return True
@@ -71,6 +92,9 @@ def get_string(self):
7192
string += self.r.get_string()
7293
return string
7394

95+
class MalformedFormulaError(Exception):
96+
pass
97+
7498
class Formula:
7599

76100
separator = ' '
@@ -90,12 +114,82 @@ def build_tree(self, string):
90114
op2 = stack.pop()
91115
n.r = op1
92116
n.l = op2
117+
if (s == OP.EXISTS) or (s == OP.FORALL):
118+
op1 = stack.pop()
119+
op2 = stack.pop()
120+
n.r = op1
121+
n.l = op2
122+
if not n.l.is_literal():
123+
# Quantifiers always have their bound variable in self.l
124+
raise MalformedFormulaError(string)
93125
stack.append(n)
94126
return stack.pop()
95127

96128
def show(self):
97129
self.root.print_tree(0)
98130

131+
132+
#### First-order functions
133+
134+
def prenex(node):
135+
"""Converts a formula into Prenex Normal Form
136+
137+
Arguments:
138+
node: The root node of the formula tree
139+
Returns:
140+
The root node of the formula in PNF
141+
"""
142+
newnode = node
143+
if node.val == OP.NOT:
144+
# Rule 0.0
145+
if node.r.val == OP.EXISTS:
146+
newnode = Node(OP.FORALL, left=node.r.l)
147+
newnode.r = Node(OP.NOT, right=node.r.r)
148+
# Rule 0.1
149+
elif node.r.val == OP.FORALL:
150+
newnode = Node(OP.EXISTS, left=node.r.l)
151+
newnode.r = Node(OP.NOT, right=node.r.r)
152+
153+
# Rules 1 & 2
154+
elif (node.val == OP.AND) or (node.val == OP.OR):
155+
if node.l.is_quantifier():
156+
newnode = Node(node.l.val, left=node.l.l, right=Node(node.val))
157+
newnode.r.l = node.l.r
158+
newnode.r.r = node.r
159+
if node.r.is_quantifier():
160+
newnode = Node(node.r.val, left=node.r.l, right=Node(node.val))
161+
newnode.r.l = node.l
162+
newnode.r.r = node.r.r
163+
164+
elif node.val == OP.IMPLIES:
165+
# Rule 3
166+
if node.r.is_quantifier():
167+
newnode = Node(node.r.val, left=node.r.l, right=Node(OP.IMPLIES))
168+
newnode.r.l = node.l
169+
newnode.r.r = node.r.r
170+
# Rule 4.0
171+
if node.l.val == OP.EXISTS:
172+
newnode = Node(OP.FORALL, left=node.l.l, right=Node(OP.IMPLIES))
173+
newnode.r.l = node.l.r
174+
newnode.r.r = node.r
175+
# Rule 4.1
176+
elif node.l.val == OP.FORALL:
177+
newnode = Node(OP.EXISTS, left=node.l.l, right=Node(OP.IMPLIES))
178+
newnode.r.l = node.l.r
179+
newnode.r.r = node.r
180+
181+
# Recursive call
182+
if not newnode.is_literal():
183+
if newnode.val == OP.NOT:
184+
newnode.r = prenex(newnode.r)
185+
else:
186+
newnode.l = prenex(newnode.l)
187+
newnode.r = prenex(newnode.r)
188+
return newnode
189+
190+
191+
#### Propositional-only functions
192+
99193
def nnf(node):
100194
"""Converts a formula into Negation Normal Form
101195
@@ -104,41 +198,47 @@ def nnf(node):
104198
Returns:
105199
The root node of the formula in NNF
106200
"""
107-
108201
newnode = node
109202
if node.val == OP.NOT:
110203
if node.r.is_literal():
204+
# Rule 1
111205
if node.r.val == LIT.TRUE:
112206
newnode = Node(LIT.FALSE)
207+
# Rule 2
113208
elif node.r.val == LIT.FALSE:
114209
newnode = Node(LIT.TRUE)
115210
return newnode
116211
else:
117212
if node.r.val == OP.NOT:
213+
# Rule 3
118214
if node.r.r.val == OP.NOT:
119215
newnode = node.r.r
120216
else:
121217
newnode.r = nnf(node.r)
122218
return newnode
219+
# Rule 4
123220
elif node.r.val == OP.AND:
124221
newnode = Node(OP.OR)
125222
newnode.l = Node(OP.NOT)
126223
newnode.l.r = node.r.l
127224
newnode.r = Node(OP.NOT)
128225
newnode.r.r = node.r.r
226+
# Rule 5
129227
elif node.r.val == OP.OR:
130228
newnode = Node(OP.AND)
131229
newnode.l = Node(OP.NOT)
132230
newnode.l.r = node.r.l
133231
newnode.r = Node(OP.NOT)
134232
newnode.r.r = node.r.r
233+
# Rule 6
135234
elif node.r.val == OP.IMPLIES:
136235
newnode = Node(OP.AND)
137236
newnode.l = Node(OP.NOT)
138237
newnode.l.r = Node(OP.NOT)
139238
newnode.l.r.r = node.r.l
140239
newnode.r = Node(OP.NOT)
141240
newnode.r.r = node.r.r
241+
# Recursive call
142242
if not newnode.is_literal():
143243
if newnode.val == OP.NOT:
144244
newnode = nnf(newnode)
@@ -298,10 +398,10 @@ def apply_substitution(f, side):
298398
for rule in substitution_rules[side]:
299399
applicable, result = rule(f)
300400
if applicable:
301-
for i in result:
302-
for j in i:
303-
for h in j:
304-
print h.get_string()
401+
# for i in result:
402+
# for j in i:
403+
# for h in j:
404+
# print h.get_string()
305405
return result
306406
return []
307407

@@ -598,10 +698,72 @@ def test_paper_example(self):
598698
'-p & q > '}
599699
self.assertEqual(normalization(f), s)
600700

701+
class PrenexTest(unittest.TestCase):
702+
703+
r0_1 = Formula('x p(x) q(x) & /E -')
704+
r0_2 = Formula('x p(x) /F -')
705+
706+
r1_1 = Formula('x s(x) r(x) & /E p &')
707+
r1_2 = Formula('p x s(x) r(x) & /F &')
708+
709+
r2_1 = Formula('p x s(x) r(x) & /E |')
710+
r2_2 = Formula('x s(x) r(x) & /F p |')
711+
712+
r3_1 = Formula('p x q(x) /E >')
713+
r3_2 = Formula('p x q(x) r(x) | /F >')
714+
715+
r4_1 = Formula('x p(x) /E q >')
716+
r4_2 = Formula('x q(x) r(x) & /F p >')
717+
718+
nested1 = Formula('p x y q(x) /E /F >')
719+
720+
def test_r0(self):
721+
s1 = 'x p(x) q(x) & - /F'
722+
s2 = 'x p(x) - /E'
723+
self.assertEqual(str(prenex(self.r0_1.root)), s1)
724+
self.assertEqual(str(prenex(self.r0_2.root)), s2)
725+
726+
def test_r1(self):
727+
s1 = 'x s(x) r(x) & p & /E'
728+
s2 = 'x p s(x) r(x) & & /F'
729+
self.assertEqual(str(prenex(self.r1_1.root)), s1)
730+
self.assertEqual(str(prenex(self.r1_2.root)), s2)
731+
732+
def test_r2(self):
733+
s1 = 'x p s(x) r(x) & | /E'
734+
s2 = 'x s(x) r(x) & p | /F'
735+
self.assertEqual(str(prenex(self.r2_1.root)), s1)
736+
self.assertEqual(str(prenex(self.r2_2.root)), s2)
737+
738+
def test_r3(self):
739+
s1 = 'x p q(x) > /E'
740+
s2 = 'x p q(x) r(x) | > /F'
741+
self.assertEqual(str(prenex(self.r3_1.root)), s1)
742+
self.assertEqual(str(prenex(self.r3_2.root)), s2)
743+
744+
def test_r4(self):
745+
s1 = 'x p(x) q > /F'
746+
s2 = 'x q(x) r(x) & p > /E'
747+
self.assertEqual(str(prenex(self.r4_1.root)), s1)
748+
self.assertEqual(str(prenex(self.r4_2.root)), s2)
749+
750+
def test_mixed(self):
751+
pass
752+
753+
def test_nested(self):
754+
s1 = 'x y p q(x) > /E /F'
755+
self.assertEqual(str(prenex(self.nested1.root)), s1)
756+
757+
def test_malformed_formula(self):
758+
def build_malformed():
759+
Formula('p x y & s(x) r(x) & /F &')
760+
self.assertRaises(MalformedFormulaError, build_malformed)
761+
762+
601763
if __name__ == '__main__':
602764

603765
#TODO: adapt for subsumed+taut checking
604-
#unittest.main()
766+
unittest.main()
605767

606768
f = NormTest.constraint
607769
f.show()

0 commit comments

Comments
 (0)