# A clever horse
Theory and applications

## Christian Lawson-Perfect

    from parsimonious.grammar import Grammar
    from parsimonious.nodes import NodeVisitor,rule
    import math

    from parsimonious.grammar import Grammar

    grammar = Grammar("""
            Expression
             = ("clever hans "/"") question

            question
             = question_what / question_love

            question_what = ("what is"/"what's"/"can you tell me"/"") " "? terms
            question_love = "i love you" space "hans"?

            terms
             = op_add_subtract

            op_add_subtract
             =  op_add_subtract_combo
             /  op_multiply_divide

            op_add_subtract_combo = op_multiply_divide (space (add/subtract) space op_multiply_divide)+

            op_multiply_divide
             =  op_multiply_divide_combo
             /  op_power

            op_multiply_divide_combo = op_power (space (multiply/divide) space op_power)+

            op_power
             = op_power_combo
             / atom

            op_power_combo = (atom space power space)+ atom

            atom
             =  atom_unary
             /  atom_square_root
             /  atom_cube_root
             /  atom_root
             /  atom_function
             /  number

            atom_unary = number space unaryop
            atom_square_root = (("the "? "square root" " of"?)/"root"/"route"/"√") space number
            atom_cube_root = (("the "? "cube root" " of"?)) space number
            atom_root  = "the "? ordinal space "root of" space number
            atom_function = (gcd/lcm) space terms space "and" space terms

            unaryop
                = "squared"
                / "cubed"
                / "factorial"

            binaryop
                = add
                / multiply
                / subtract
                / divide
                / power

            add = ("+"/"add"/"plus")
            multiply = ("×"/"x"/"times"/"multiplied by")
            subtract = ("-"/"minus"/"take away"/"takeaway"/"take-away"/"subtract")
            divide = ("÷"/"divided by"/"over")
            power = ("^"/"to the power of"/"to the")

            gcd = "the "? ("greatest common factor of"/"greatest common divisor of"/"gcd of"/"gcf of"/("biggest number that divides" " both"?))
            lcm = "the "? ("least common multiple of"/"lcm of"/("smallest multiple of" " both"?))

            space = (" "*)

            number
             = digit / number_word

            number_word
             = "one"
             / ("two"/"to"/"too")
             / "three"
             / ("four"/"for")
             / "five"
             / "six"
             / "seven"
             / ("eight"/"ate")
             / "nine"
             / "zero"

            ordinal
             = ordinal_digit / ordinal_word

            ordinal_digit
             = digit ("st"/"nd"/"rd"/"th")

            ordinal_word
             = "first"
             / "second"
             / "third"
             / "fourth"
             / "fifth"
             / "sixth"
             / "seventh"
             / "eighth"
             / "ninth"
             / "tenth"

            digit = ~r"[0-9]+"

    """)
    ops = {
        '+': lambda a,b:a+b,
        '-': lambda a,b:a-b,
        '*': lambda a,b:a*b,
        '/': lambda a,b:a/b,
        '^': lambda a,b:a**b,
        'squared': lambda a:a*a,
    }

    unary_ops = {
        'squared': lambda a:a*a,
        'cubed': lambda a:a*a*a,
        'factorial': math.factorial,
    }

    def gcd(a,b):
        a,b = int(a),int(b)
        if a<1 or b<1:
            raise Exception("gcd of non-positive numbers")
        if a<b:
            return gcd(b,a)
        while b!=0:
            a,b = b,a%b
        return a

    def lcm(a,b):
        return a*b//gcd(a,b)

    function_ops = {
        'gcd': gcd,
        'lcm': lcm
    }

    class HansVisitor(NodeVisitor):
        grammar = grammar

        def visit_space(self,*args):
            pass

        def visit_digit(self,node,visited_children):
            return int(node.text)

        def visit_number_word(self,node,visited_children):
            d = {
                'one': 1,
                'two': 2, 'to': 2, 'too': 2,
                'three': 3,
                'four': 4, 'for': 4,
                'five': 5,
                'six': 6,
                'seven': 7,
                'eight': 8, 'ate': 8,
                'nine': 9,
                'zero': 0
            }
            return d[node.text]

        def visit_number(self,node,visited_children):
            return visited_children[0]

        visit_ordinal = NodeVisitor.lift_child

        def visit_ordinal_digit(self,node,visited_children):
            return visited_children[0]

        def visit_ordinal_word(self,node,visited_children):
            d = {
                'first': 1,
                'second': 2,
                'third': 3,
                'fourth': 4,
                'fifth': 5,
                'sixth': 6,
                'seventh': 7,
                'eighth': 8,
                'ninth': 9,
                'tenth': 10,
            }
            return d[node.text]

        def visit_add(self,*args):
            return '+'

        def visit_multiply(self,*args):
            return '*'

        def visit_subtract(self,*args):
            return '-'

        def visit_divide(self,*args):
            return '/'

        def visit_power(self,*args):
            return '^'

        def generic_visit(self,node,visited_children):
            if not node.expr_name:
                return visited_children
            return 'g '+node.expr_name

        visit_op_add_subtract = NodeVisitor.lift_child
        visit_op_multiply_divide = NodeVisitor.lift_child
        visit_op_power = NodeVisitor.lift_child
        visit_atom = NodeVisitor.lift_child

        def visit_op_add_subtract_combo(self,node,visited_children):
            a = visited_children[0]
            for bit in visited_children[1]:
                _,op,_,b = bit
                op = op[0]
                a = ops[op](a,b)
            return a

        visit_op_multiply_divide_combo = visit_op_add_subtract_combo

        def visit_op_power_combo(self,node,visited_children):
            a = visited_children[1]
            for bit in visited_children[0][::-1]:
                b,_,op,_ = bit
                op = op[0]
                a = ops[op](b,a)
            return a

        def visit_op_all_squared(self,node,visited_children):
            return ('squared',None)

        def visit_unaryop(self,node,visited_children):
            return node.text

        def visit_atom_unary(self,node,visited_children):
            n,_,op = visited_children
            return unary_ops[op](n)

        def visit_atom_square_root(self,node,visited_children):
            n = visited_children[-1]
            return math.sqrt(n)

        def visit_atom_cube_root(self,node,visited_children):
            n = visited_children[-1]
            return math.pow(n,1/3)

        def visit_atom_root(self,node,visited_children):
            _,r,_,_,_,n = visited_children
            return math.pow(n,1/r)

        def visit_gcd(self,node,visited_children):
            return 'gcd'
        def visit_lcm(self,node,visited_children):
            return 'lcm'

        def visit_atom_function(self,node,visited_children):
            op,_,a,_,_,_,b = visited_children
            op = op[0]
            return function_ops[op](a,b)

        def visit_question_what(self,node,visited_children):
            return ('what',visited_children[-1])

        def visit_question_love(self,*args):
            return ('love',True)

        def visit_question(self,node,visited_children):
            return visited_children[0]

        def visit_Expression(self,node,visited_children):
            return visited_children[-1]

    if __name__ == '__main__':
        import sys
        text = sys.argv[1]
        visitor = HansVisitor()
        result = visitor.parse(text)
        print(': ',result)

    from grammar import HansVisitor
    from parsimonious.exceptions import ParseError
    import speech_recognition as sr

    from gpiozero import LED, Button
    from time import sleep

    class Horse(object):

        visitor = HansVisitor()

        neigh_switch = LED(27)
        clop_switch = LED(17)

        listen_button = Button(22)

        def __init__(self, listen_continuously=True):
            self.recognizer = sr.Recognizer()
            self.mic = sr.Microphone(device_index=2,sample_rate=44100,chunk_size=128)
            with self.mic as source:
                self.recognizer.adjust_for_ambient_noise(source)

            if listen_continuously:
                self.start_listening()
            else:
                self.wait_for_button()

        def start_listening(self):
            print("Listening continuously")
            def handle(recognizer,audio):
                self.handle_phrase(recognizer,audio)
            self.stop_listening = self.recognizer.listen_in_background(self.mic, handle)

        def wait_for_button(self):
            print("Listening when button pressed")
            def do_listen():
                print("Listening...")
                with self.mic as source:
                    audio = self.recognizer.listen(self.mic)
                self.handle_phrase(self.recognizer,audio)
            self.listen_button.when_pressed = do_listen

        def neigh(self):
            self.neigh_switch.on()
            sleep(2)
            self.neigh_switch.off()

        def clop(self,n):
            self.clop_switch.on()
            sleep(n/2)
            self.clop_switch.off()

        def handle_phrase(self,recognizer,audio):
            print("GOT")
            try:
                text = self.recognizer.recognize_google(audio).strip().lower()
                print("Google Speech Recognition thinks you said " + text)
                try:
                    question,result = self.visitor.parse(text)
                    if question == 'love':
                        print("Hans loves you too!")
                        self.neigh()
                    else:
                        result = int(result)
                        print("It's {}".format(result))
                        if result<=0 or result>100:
                            self.neigh()
                        else:
                            self.clop(result)

                except ParseError:
                    print("Neigh!")
                    self.neigh()

            except sr.UnknownValueError:
                print("Google Speech Recognition could not understand audio")
                return
            except sr.RequestError as e:
                print("Could not request results from Google Speech Recognition service; {0}".format(e))

    hans = Horse(listen_continuously=False)

    while True:
        sleep(0.1)

## christianp.github.io/clever-hans