0

I posted this question under an alter yesterday not realising my account was still active after 9 months, sorry for the double post, i've fixed an error in my example pointed out by jellybean and i'll elaborate further on the context of the problem.

I'm trying to process a first order logic formula represented as nested lists and strings in python so that that its in disjunctive normal form,

i.e

[
    '&', 
    ['|', 'a', 'b'], 
    ['|', 'c', 'd']
]

turns into

[
    '|',
    [
        '|', 
        ['&', 'a', 'c'], 
        ['&', 'b', 'c']
    ], 
    [
        '|', 
        ['&', 'a', 'd'], 
        ['&', 'b', 'd']
    ]
]`

where | is or and & is and.

currently im using a recursive implementation which does several passes over a formula until it can't find any nested 'or' symbols inside a list argument for 'ands'. Its being used to process a set of nested formulas represented as strings and lists for universal computational tree logic so it will not only have |s and &s but temporal operators.

This is my implementation, performDNF(form) is the entry point. Right now it performs a single pass over the formula with dnfDistributivity() which works for smaller inputs but when you use larger inputs the while loop checking function (checkDistributivity()) finds no |s inside of &s and terminates. Help anyone, this is driving me mad.

def dnfDistributivity(self, form):
    if isinstance(form, type([])):
        if len(form) == 3:
            if form[0] == '&':
                if form[1][0] == '|':
                    form = [
                               '|', 
                               ['&', form[2], form[1][1]], 
                               ['&', form[2], form[1][2]]
                           ]
                elif form[2][0] == '|':
                    form = [
                                '|', 
                                ['&', form[1], form[2][1]], 
                                ['&', form[1], form[2][2]]
                           ]
            form[1] = self.dnfDistributivity(form[1])
            form[2] = self.dnfDistributivity(form[2])
        elif len(form) == 2:
            form[1] = self.dnfDistributivity(form[1])
    return form

def checkDistributivity(self, form, result = 0):
    if isinstance(form, type([])):
        if len(form) == 3:
            if form[0] == '&':
                print "found &"
                if isinstance(form[1], type([])):
                    if form[1][0] == '|':
                        return 1
                elif isinstance(form[2], type([])):
                    if form[2][0] == '|':
                        return 1
                else:
                    result = self.checkDistributivity(form[1], result)
                    print result
                    if result != 1:
                        result = self.checkDistributivity(form[2], result)
                        print result
        elif len(form) == 2:
            result = self.checkDistributivity(form[1], result)
            print result
    return result

def performDNF(self, form):
    while self.checkDistributivity(form):
        form = self.dnfDistributivity(self.dnfDistributivity(form))
    return form
nosklo
  • 217,122
  • 57
  • 293
  • 297
FlyingToaster
  • 19
  • 1
  • 3
  • The previous post FlyingToaster is referring to is http://stackoverflow.com/questions/1787576/python-nested-lists-and-recursion-problem. Although these two posting collected so far in excess of 70 views, there's been but one generic response. – mjv Nov 25 '09 at 02:11
  • Why go top down? If you put the recursion at the top, won't the "or" bits bubble up? – wisty Nov 25 '09 at 03:20
  • Its looks like regex to me "[ab][cd]" == "ac|bc|ad|bd", May be I dont know that logic at all though. – YOU Nov 25 '09 at 03:45
  • I gave the bottom up approach a try too but similarly to the top down it partially processed the formula in dnfDistributivity() and checkDistributivity() still misses that its only partially processed. As for the regex thats correct except for what i'm using the lists for requires a fairly rich language, i've been doing this in conjunction with lex/yacc but I need to process formulas after they've been parsed, i'm just not sure why checkDistributivity wouldn't be able to see that the last returned formula still contains a disjunctive operation nested with a conjunctive operation. – FlyingToaster Nov 25 '09 at 04:48
  • You should strive to post short, focussed questions rather than this kind of "please do my homework" question. Thank you. – Antoine P. Dec 29 '09 at 23:24

2 Answers2

3

First, two general remarks about your code:

  • Use return True instead of return 1.
  • Use isinstance(form, list) instead of isinstance(form, type([])).

Second, some other observations:

  • I assume you also want to get rid of double negations. Currently your code doesn't do that.
  • Likewise, you'll need to apply one of DeMorgan's laws to push negations to the leaves.

Aside from that, I think the readability of this code can be improved greatly. I'll give an alternative implementation which I believe to be correct. Let me know whether the code below works for you; I didn't go crazy with creating expressions, so I may have missed an edge case. Lastly, I will focus only on the regular propositional connectives. It should be clear how to apply transformations involving CTL-specific connectives.

  1. Create a class Op which represents an operator (connective):

    class Op(list):
        def __init__(self, *args):
            super().__init__(args)
    

    The arguments to __init__ are the operands. This code uses super as defined in PEP 3135 and works only in Python 3.x In Python 2.x, you'll have to use super as defined in PEP 367:

    class Op(list):
        def __init__(self, *args):
            super(Op, self).__init__(args)
    
  2. Create simple subclasses of Op for each operator. For debugging purposes you may want to implement a custom __str__ method:

    class Neg(Op):
        def __str__(self):
            return '!(%s)' % tuple(self)
    class And(Op):
        def __str__(self):
            return '(%s) & (%s)' % tuple(self)
    class Or(Op):
        def __str__(self):
            return '(%s) | (%s)' % tuple(self)
    class AX(Op):
        def __str__(self):
            return 'AX (%s)' % tuple(self)
    ...
    

    Now the formula !(a & b) can be created as Neg(And('a', 'b')).

  3. Create very simple functions which apply a certain transformation once. This will keep the implementation clean. Annotate these functions which some information on how they should be applied. A preorder function should be applied from top to bottom: first transform the root of the expression tree, then recurse. A postorder function should be applied to an expression after it has been recursively applied to subexpressions. Use isinstance to check the type of connectives.

    1. We start easy: the function removeDoubleNeg removes double negations:

      @expressionTransformation('postorder')
      def removeDoubleNeg(expr):
          if isinstance(expr, Neg) and isinstance(expr[0], Neg):
              return expr[0][0]
      
    2. Next, let's define one of DeMorgan's laws:

      @expressionTransformation('preorder')
      def deMorgan(expr):
          if isinstance(expr, Neg) and isinstance(expr[0], And):
              return Or(Neg(expr[0][0]), Neg(expr[0][1]))
      
    3. And now the function which this question is all about:

      @expressionTransformation('preorder', 'postorder')
      def distribute(expr):
          if isinstance(expr, And):
              if isinstance(expr[0], Or):
                  return Or(And(expr[0][0], expr[1]), And(expr[0][1], expr[1]))
              if isinstance(expr[1], Or):
                  return Or(And(expr[0], expr[1][0]), And(expr[0], expr[1][1]))
      

      Wow! That's a lot less code!

  4. Okay, so how does this work? Observe that any naive implementation of an expression transformation f will involve boilerplate code:

    1. Test whether the argument is a connective (as opposed to a constant or variable).
    2. Attempt to apply f to the root of the expression tree.
    3. Recurse.
    4. Return the result.

    Depending on f, step 1 and 2 may need to be reversed (postorder instead of preorder). Still, every implementation of f will look alike. You will want to avoid boilerplate code, especially if you plan to define many more transformations. It is the lack of this boilerplate that made the functions defined in the previous step so concise (and thus easy to debug!). The decorators returned by the function expressionTransformation solved this problem. Its implementation is as follows:

    from functools import wraps
    def expressionTransformation(*args):
        def wrap(f):
            @wraps(f)
            def recursiveTransformation(expr):
                if not isinstance(expr, Op):
                    return expr
                if 'postorder' in args:
                    expr[:] = map(recursiveTransformation, expr)
                res = f(expr)
                expr = expr if res is None else res 
                if 'preorder' in args:
                    expr[:] = map(recursiveTransformation, expr)
                return expr
            return recursiveTransformation
        return wrap
    

    What happens here is the following:

    1. The function expressionTransformation returns a decorator (named wrap) which receives the transformation function f as its argument.
    2. wrap returns a recursive function recursiveTransformation which applies f to its argument expr only if this argument is a connective.
    3. Depending on the arguments args supplied to expressionTransformation, f will be applied before or after (or before and after) applying f to the subexpressions.
    4. The assumption is made that f may return None if no transformation is made.

    The function functools.wraps is used to copy certain properties of f, such as its name, to recursiveTransformation. This functionality is non-essential.

    (Note that there are more efficient way to create preorder and postorder transformations than using the tests 'postorder' in args and 'preorder' in args over and over, but I chose this for clarity.)

  5. That's all. We can now easily combine these functions (note that this function should not be decorated):

    def toDNF(expr):
        return distribute(removeDoubleNeg(deMorgan(expr)))
    
  6. You can test the code with statements like these:

    toDNF(AX(And(Or('a', 'b'), And(Or('c', 'd'), Or('e', 'f')))))
    toDNF(Neg(And(Or(Neg(Neg('a')), 'b'), And(Or('c', 'd'), Or('e', 'f')))))
    
Stephan202
  • 59,965
  • 13
  • 127
  • 133
  • wow, that is some beautiful code, give me some time and I will analyse that approach but i'm very interested in what you've done from first glance. Also you are correct about the absence removal of double negatives and DeMorgans law, I had them as other methods of the overall class and removed them from the scope of the problem to simplify the issue. – FlyingToaster Nov 25 '09 at 22:47
  • ok, i've had some time to check it out and wrap my head around whats going on, great approach if you ask me, i've come across decorators before but had trouble wrapping my mind around the concept. After doing a bit of "sharpening my saw" I understand it. Cheers :) – FlyingToaster Nov 30 '09 at 11:34
  • @FlyingToaster: glad you like it! If this sufficiently answers your question, you can upvote the answer and mark it as accepted. Otherwise, feel free to point out what's missing :) – Stephan202 Nov 30 '09 at 11:56
0

You have:

    elif len(form) == 2:
        result = self.checkDistributivity(form[1], result)
        print result

Shouldn't that be:

    elif len(form) == 2:
        result_1 = self.checkDistributivity(form[1], result)
        result_2 = self.checkDistributivity(form[2], result) 
        if result_1 or result_2:
            return 1
wisty
  • 6,981
  • 1
  • 30
  • 29
  • ah, when the formula has 2 items in the list its considered a unary operation, the first list item would be the token (in the case of ctl this would be cases like ['AG', 'a'] or ['AF', 'c'] where AG and AF are temporal operators. You only need to process the 2nd argument, or index value 1. – FlyingToaster Nov 25 '09 at 04:26