sml.py
author Eugen Sawin <sawine@me73.com>
Tue, 31 May 2011 00:58:51 +0200
changeset 26 88451ad84297
parent 25 3418ff66b9ed
permissions -rw-r--r--
Added satisfiabilty test via minisat.
     1 """
     2 Name: sml - Simple Modal Logic Lib.
     3 Description: sml provides classes for simple modal logic formulae construction and reduction. It also features a disfunctional parser for the SML syntax.
     4 Author: Eugen Sawin <sawine@informatik.uni-freiburg.de>
     5 """
     6 
     7 TMP_DIMACS = "dimacs~"
     8 TMP_SOLUTION = "solution~"
     9 
    10 def main():
    11     # edit your formulae here
    12     p = Variable("p")
    13     q = Variable("q")
    14     r = Variable("r") 
    15     s = Variable("s")
    16     formula1 = Imp(Eq(p, q), r)
    17     formula2 = Not(Or(p, q))
    18     formula3 = Not(Not(p))
    19     formula4 = Imp(Box(Imp(Diamond(p), p)), Box(Imp(p, Box(p))))
    20     formula5 = Imp(And(Or(p, q), r), Not(s))
    21     formula6 = And(Or(p, Or(q, Not(s))), And(Or(r, Not(s)), Or(Not(p), s)))
    22     formula = formula6 # choose your formula here
    23 
    24     args = parse_arguments()   
    25     if (args.formula):
    26         scanner = Scanner(TOKENS)
    27         parser = Parser(SYNTAX, scanner)
    28         (accepted, out, tree_nodes) = parser.parse(args.formula)
    29         if not accepted:
    30             print EXCUSE 
    31             return
    32         tree = SyntaxTree(out, tree_nodes) 
    33         formula = tree.root
    34    
    35     original = formula
    36     print "formula: \t\t\t%s [%i]" % (formula, formula.depth())
    37     formula = expand_imp_eq(formula)
    38     print "expanded -> and <->: \t\t%s [%i]" % (formula, formula.depth()) 
    39     formula = de_morgan(formula)
    40     print "reduced ~: \t\t\t%s [%i]" % (formula, formula.depth()) 
    41     formula = reduce_iter(formula)
    42     print "reduced iterated modalities: \t%s [%i]" % (formula, formula.depth()) 
    43     formula = reduce_iter(dist_mod((formula)))
    44     print "distributed modalities+: \t%s [%i]" % (formula, formula.depth()) 
    45     formula = mcnf(formula)
    46     print "alpha mcnf: \t\t\t%s [%i]" % (formula, formula.depth())
    47     #cnf_formula = cnf(original)
    48     #print "alpha cnf: \t\t\t%s [%i]" % (cnf_formula, cnf_formula.depth())
    49     
    50     if not args.dimacs:
    51         dimacs_file = TMP_DIMACS
    52     else:
    53         dimacs_file = args.dimacs
    54     solution = sat_test(original, dimacs_file)
    55     if solution:
    56         print "satisfiable: \t\t\t%s " % (", ".join([str(s) for s in solution]))
    57     else:
    58         print "not satisfiable"
    59     
    60 from subprocess import call, PIPE
    61 
    62 def sat_test(formula, dimacs_file):
    63     out, variables = dimacs(formula)
    64     with open(dimacs_file, "w") as f:
    65         f.write(out) 
    66     call(["minisat2", "-verb=0", dimacs_file, TMP_SOLUTION], stdout=PIPE)
    67     with open(TMP_SOLUTION) as f:
    68         solved, assignment = f.readlines()  
    69     solved = solved[0] == "S"      
    70     if solved:
    71         positive = set() 
    72         for a in assignment.split():           
    73             neg = a[0] == "-"
    74             if not neg:
    75                 positive.add(int(a))       
    76         solution = []
    77         for v, n in variables.iteritems():           
    78             if n in positive:              
    79                 solution.append(Variable(v))
    80             else:
    81                 solution.append(Not(Variable(v)))
    82         return solution
    83 
    84 def dimacs(formula):
    85     def clausify(f):        
    86         if f.id == AND:    
    87             c1 = clausify(f.values[0])   
    88             c2 = clausify(f.values[1])   
    89             if c1:
    90                 clauses.append(clausify(f.values[0]))  
    91             if c2:
    92                 clauses.append(clausify(f.values[1]))              
    93         if f.id == OR:
    94             clause = []         
    95             clause.extend(clausify(f.values[0]))
    96             clause.extend(clausify(f.values[1]))            
    97             return clause
    98         if f.id == NOT:
    99             return ["-" + clausify(f.values[0])]
   100         if f.id == VARIABLE:
   101             if f.values[0] in variables:
   102                 n = variables[f.values[0]]
   103             else:
   104                 n = len(variables) + 1
   105                 variables[f.values[0]] = n
   106             return str(n)
   107       
   108     variables = {}
   109     clauses = []
   110     clausify(formula)
   111     out = "c DIMACS CNF input file generated by sml\n"
   112     out += "c Author: Eugen Sawin <sawine@informatik.uni-freiburg.de>\n"
   113     out += "p cnf %i %i\n" % (len(variables), len(clauses))    
   114     out += " 0\n".join([" ".join(c) for c in clauses]) + " 0\n"
   115     return out, variables
   116 
   117 def values(fun, f):  
   118     return [fun(v) for v in f.values] 
   119 
   120 def expand_imp_eq(formula):  
   121     expmap = {IMP: lambda (a, b): Or(Not(a), b),
   122               EQ: lambda (a, b): And(Or(Not(a), b), Or(a, Not(b)))}   
   123     if not isinstance(formula, Operator):   
   124         return formula
   125     if formula.id in expmap:
   126         return expmap[formula.id](values(expand_imp_eq, formula))   
   127     return formula.__class__(*values(expand_imp_eq, formula))
   128 
   129 def de_morgan(formula):    
   130     negmap = {AND: lambda (a, b): Or(negate(a), negate(b)),
   131               OR: lambda (a, b): And(negate(a), negate(b)),
   132               BOX: lambda (a,): Diamond(negate(a)),
   133               DIAMOND: lambda (a,): Box(negate(a)),
   134               VARIABLE: lambda (a,): Not(Variable(a)),
   135               NOT: lambda (a,): a}    
   136     def negate(f):
   137         return negmap[f.id](f.values)
   138     if not isinstance(formula, Operator):
   139         return formula  
   140     if formula.id == NOT:
   141         return negate(formula.values[0])
   142     return formula.__class__(*values(de_morgan, formula))
   143 
   144 def reduce_iter(formula):  
   145     redset = set(((BOX, BOX), (DIAMOND, DIAMOND), (DIAMOND, BOX), (BOX, DIAMOND)))
   146     if not isinstance(formula, Operator) or not isinstance(formula.values[0], Operator):
   147         return formula
   148     ids = (formula.id, formula.values[0].id)
   149     if ids in redset:
   150         return reduce_iter(formula.values[0])
   151     return formula.__class__(*values(reduce_iter, formula))
   152 
   153 def dist_mod(formula):    
   154     distmap = {(BOX, AND): lambda (a, b): And(Box(a), Box(b)),
   155                (DIAMOND, OR): lambda (a, b): Or(Diamond(a), Diamond(b)),
   156                (BOX, OR): lambda (a, b): (isinstance(a, Box) or isinstance(b, Box))               
   157                and Or(Box(a), Box(b)) or Box(Or(a, b)),
   158                (DIAMOND, AND): lambda (a, b): (isinstance(a, Diamond) or isinstance(b, Diamond))
   159                and And(Diamond(a), Diamond(b)) or Diamond(And(a, b))}   
   160     if not isinstance(formula, Operator) or not isinstance(formula.values[0], Operator):
   161         return formula
   162     ids = (formula.id, formula.values[0].id)
   163     if ids in distmap:
   164         return distmap[ids](values(dist_mod, formula.values[0]))
   165     return formula.__class__(*values(dist_mod, formula))    
   166 
   167 def mcnf(formula):
   168     if formula.id == OR:
   169         conj_id = int(formula.values[1].id == AND) 
   170         return And(Or(formula.values[conj_id].values[0], formula.values[1 - conj_id]), 
   171                     Or(formula.values[conj_id].values[1], formula.values[1 - conj_id]));
   172     return formula
   173 
   174 def cnf(formula):      
   175     def part(f):
   176         for v in f.values:
   177             if v.id != VARIABLE:
   178                 sub_formulae.append(v)
   179                 part(v) 
   180 
   181     def conjunct(f):
   182         if len(bicon):
   183             return And(f, conjunct(bicon.pop()))
   184         return f
   185 
   186     sub_formulae = [formula]
   187     part(formula)
   188     sub_formulae.reverse() 
   189     known = {}
   190     bicon = []
   191     for s in sub_formulae:       
   192         for i, v in enumerate(s.values):
   193             if v in known:
   194                 s.values[i] = known[v]
   195         bicon.append(Eq(Variable("x" + str(len(bicon))), s))
   196         known[s] = bicon[-1].values[0]     
   197     return mcnf(dist_mod(reduce_iter(de_morgan(expand_imp_eq(conjunct(known[formula]))))))
   198 
   199 """ The Parser - Do NOT look below this line! """ 
   200 import re
   201 
   202 NOT = "~"
   203 AND = "&"
   204 OR = "|"
   205 IMP = "->"
   206 EQ = "<->"
   207 BOX = "[]"
   208 DIAMOND = "<>"
   209 VARIABLE = "VAR"
   210 LB = "("
   211 RB = ")"
   212 
   213 class Operator(object):
   214     def __init__(self, id, arity, *values):
   215         self.id = id         
   216         self.arity = arity
   217         self.values = list(values)
   218     def depth(self):       
   219         if self.arity == 0:
   220             return 0
   221         if self.id in (BOX, DIAMOND):
   222             return 1 + self.values[0].depth()
   223         if self.id == NOT:           
   224             return self.values[0].depth()       
   225         return max(self.values[0].depth(), self.values[1].depth())
   226     def __eq__(self, other):
   227         return self.id == other.id and self.values == other.values # commutative types not considered yet
   228     def __str__(self):
   229         out = self.id
   230         if self.arity == 0:
   231             out = str(self.values[0])           
   232         if self.arity == 1:
   233             out = self.id + str(self.values[0])
   234         elif self.arity  == 2:          
   235             out = "(" + str(self.values[0]) + " " + self.id + " " + str(self.values[1]) + ")"
   236         return out
   237 
   238 class Not(Operator):
   239     arity = 1
   240     def __init__(self, *values):
   241         Operator.__init__(self, NOT, Not.arity, *values)       
   242     def __call__(self):
   243         pass
   244 
   245 class And(Operator):
   246     arity = 2
   247     def __init__(self, *values):
   248         Operator.__init__(self, AND, And.arity, *values)
   249        
   250 class Or(Operator):
   251     arity = 2
   252     def __init__(self, *values):
   253         Operator.__init__(self, OR, Or.arity, *values)       
   254 
   255 class Imp(Operator):
   256     arity = 2
   257     def __init__(self, *values):
   258         Operator.__init__(self, IMP, Imp.arity, *values)
   259     
   260 class Eq(Operator):
   261     arity = 2
   262     def __init__(self, *values):
   263         Operator.__init__(self, EQ, Eq.arity, *values)
   264     
   265 class Box(Operator):
   266     arity = 1
   267     def __init__(self, *values):
   268         Operator.__init__(self, BOX, Box.arity, *values)
   269 
   270 class Diamond(Operator):
   271     arity = 1
   272     def __init__(self, *values):
   273         Operator.__init__(self, DIAMOND, Diamond.arity, *values)
   274     
   275 class Variable(Operator):
   276     arity = 0
   277     def __init__(self, *values):
   278         Operator.__init__(self, VARIABLE, Variable.arity, *values)   
   279 
   280 class Lb(Operator):
   281     arity = -1
   282     def __init__(self, *values):
   283         Operator.__init__(self, LB, Lb.arity, *values)
   284 
   285 class Rb(Operator):
   286     arity = -1
   287     def __init__(self, *values):
   288         Operator.__init__(self, RB, Rb.arity, *values)
   289 
   290 class Formula(object):
   291     def __init__(self):
   292         pass 
   293 
   294 TOKENS = {re.compile("\("): Lb,
   295           re.compile("\)"): Rb,
   296           re.compile("~"): Not,
   297           re.compile("&"): And,
   298           re.compile("\|"): Or,
   299           re.compile("->"): Imp,
   300           re.compile("<->"): Eq,
   301           re.compile("\[\]"): Box,
   302           re.compile("<>"): Diamond,
   303           re.compile("[a-z]+"): Variable}
   304          
   305 class Scanner(object):
   306     def __init__(self, tokens, source=None):
   307         self.tokens = tokens
   308         self.reset(source)
   309     def next(self):         
   310         while self.i < len(self.source):
   311             re.purge()
   312             for p, token in self.tokens.iteritems():
   313                 m = p.match(self.source[self.i:])               
   314                 if m:                  
   315                     self.i += m.end(0)
   316                     value = m.group(0)  
   317                     return (token, value)
   318             self.i += 1
   319         return (None, None)
   320     def reset(self, source):
   321         self.i = 0
   322         self.source = source
   323 
   324 S = Formula
   325 A = "A"
   326 #0 S: (Variable,),
   327 #1 S: (Not, S),
   328 #2 S: (Lb, A, Rb),
   329 #3 S: (Box, S),
   330 #4 S: (Diamond, S),
   331 #5 A: (S, And, S),
   332 #6 A: (S, Or, S),
   333 #7 A: (S, Imp, S),
   334 #8 A: (S, Eq, S)
   335 SYNTAX = ((Variable,),
   336           (Not, S),
   337           (Lb, A, Rb),
   338           (Box, S),
   339           (Diamond, S),
   340           (S, And, S),
   341           (S, Or, S),
   342           (S, Imp, S),
   343           (S, Eq, S))
   344 
   345 class Parser(object):
   346     def __init__(self, syntax, scanner):
   347         self.syntax = syntax
   348         self.scanner = scanner
   349     def parse(self, source):
   350         table = {(S, Variable): 0,
   351                  (S, Not): 1,
   352                  (S, Lb): 2,
   353                  (S, Box): 3,
   354                  (S, Diamond): 4,
   355                  (A, S, And): 5,
   356                  (A, S, Or): 6,
   357                  (A, S, Imp): 7,
   358                  (A, S, Eq): 8,
   359                  (A, Variable, And): 5,
   360                  (A, Variable, Or): 6,
   361                  (A, Variable, Imp): 7,
   362                  (A, Variable, Eq): 8,
   363                  (A, Not, Variable, And): 5,
   364                  (A, Not, Variable, Or): 6,
   365                  (A, Not, Variable, Imp): 7,
   366                  (A, Not, Variable, Eq): 8}
   367         stack = [S]
   368         tree_nodes = []
   369         tree = None
   370         out = []
   371         self.scanner.reset(source)
   372         (token, value) = self.scanner.next()
   373         (lookahead, la_value) = self.scanner.next()
   374         (lookahead2, la2_value) = self.scanner.next()
   375         accepted = True 
   376         while token and len(stack): 
   377             top = stack[-1]               
   378             if top == token:
   379                 tree_nodes.append((token, value))
   380                 stack.pop()
   381                 #tree_nodes.append((stack.pop(), value))
   382                 (token, value) = (lookahead, la_value)
   383                 (lookahead, la_value) = (lookahead2, la2_value)
   384                 #(lookahead, _) = self.scanner.next()
   385                 (lookahead2, la2_value) = self.scanner.next()
   386             else:
   387                 action = None
   388                 if (top, token) in table:
   389                     action = table[(top, token)]
   390                 elif (top, token, lookahead) in table:
   391                     action = table[(top, token, lookahead)]  
   392                 elif (top, token, lookahead, lookahead2) in table:
   393                     action = table[(top, token, lookahead, lookahead2)]    
   394                 if action is None:
   395                     accepted = False
   396                     break
   397                 
   398                 out.append(action)
   399                 stack.pop()
   400                 stack.extend(reversed(self.syntax[action]))
   401         accepted = accepted and not len(stack)
   402         return (accepted, out, tree_nodes) 
   403 
   404 class SyntaxTree(object):
   405     def __init__(self, seq, tree_nodes):
   406         seq.reverse()
   407         tree_nodes.reverse()      
   408         self.root = self.compile(seq, tree_nodes)[0]       
   409     def compile(self, seq, tree_nodes):
   410         stack = []     
   411         while len(seq):           
   412             s = seq.pop()
   413             if s == 0:
   414                 c, v = tree_nodes.pop()
   415                 while c != Variable:
   416                     c, v = tree_nodes.pop()
   417                 stack.append(Variable(v))
   418             elif s == 1:
   419                 stack.append(Not(self.compile(seq, tree_nodes)))
   420             elif s == 2:
   421                 stack.extend(self.compile(seq, tree_nodes))
   422             elif s == 3:
   423                 stack.append(Box(self.compile(seq, tree_nodes)))
   424             elif s == 4:
   425                 stack.append(Diamond(self.compile(seq, tree_nodes)))
   426             elif s == 5:
   427                 stack.append(And(self.compile(seq, tree_nodes)))
   428             elif s == 6:
   429                 stack.append(Or(self.compile(seq, tree_nodes)))
   430             elif s == 7:
   431                 stack.append(Imp(self.compile(seq, tree_nodes)))
   432             elif s == 8:
   433                 stack.append(Eq(self.compile(seq, tree_nodes)))       
   434         return stack   
   435 
   436 from argparse import ArgumentParser
   437 
   438 def parse_arguments():
   439     parser = ArgumentParser(description="Tries to parse simple modal logic syntax and reduces the formula. Edit your formula directly in the code file for best results.")
   440     parser.add_argument("-f", "--formula", help="provide your own formula here, or better don't") 
   441     parser.add_argument("-d", "--dimacs", help="output file for the CNF DIMACS output")
   442     return parser.parse_args()
   443  
   444 EXCUSE = """The formula was not accepted by me, because I do not like you. It is not personal. I seem to have some issues with formulae with inner nesting and I generally suffer from my hasty implementation. I would like to thank you for your coorporation by providing the formula within the code file itself. Thank you. ~The Parser"""
   445 
   446 if __name__ == "__main__":
   447     main()