From e69397ae14474946fbbb7e6eadac2cfe98be087b Mon Sep 17 00:00:00 2001
From: Andreas Kloeckner <inform@tiker.net>
Date: Mon, 30 Apr 2012 17:41:56 -0400
Subject: [PATCH] Add Maxima interface.

---
 pymbolic/maxima.py  | 272 ++++++++++++++++++++++++++++++++++++++++++++
 test/test_maxima.py |  74 ++++++++++++
 2 files changed, 346 insertions(+)
 create mode 100644 pymbolic/maxima.py
 create mode 100644 test/test_maxima.py

diff --git a/pymbolic/maxima.py b/pymbolic/maxima.py
new file mode 100644
index 0000000..3faf898
--- /dev/null
+++ b/pymbolic/maxima.py
@@ -0,0 +1,272 @@
+# Inspired by similar code in Sage at:
+# http://trac.sagemath.org/sage_trac/browser/sage/interfaces/maxima.py
+
+import pexpect
+import re
+import pytools
+
+from pymbolic.mapper.stringifier import StringifyMapper
+from pymbolic.parser import Parser as ParserBase
+
+
+
+
+IN_PROMPT_RE = re.compile(r"\(%i([0-9]+)\) ")
+OUT_PROMPT_RE = re.compile(r"\(%o([0-9]+)\) ")
+ERROR_PROMPT_RE = re.compile(r"(Principal Value|debugmode|incorrect syntax|Maxima encountered a Lisp error)")
+ASK_RE = re.compile(r"(zero or nonzero|an integer|positive, negative, or zero|"
+        "positive or negative|positive or zero)")
+MULTI_WHITESPACE = re.compile(r"[ \r\n\t]+")
+
+
+
+
+class MaximaError(RuntimeError):
+    pass
+
+
+
+
+# {{{ stringifier
+
+class MaximaStringifyMapper(StringifyMapper):
+    def map_power(self, expr, enclosing_prec):
+        from pymbolic.mapper.stringifier import PREC_POWER
+        return self.parenthesize_if_needed(
+                self.format("%s^%s",
+                    self.rec(expr.base, PREC_POWER),
+                    self.rec(expr.exponent, PREC_POWER)),
+                enclosing_prec, PREC_POWER)
+
+    def map_constant(self, expr, enclosing_prec):
+        from pymbolic.mapper.stringifier import PREC_SUM
+        if isinstance(expr, complex):
+            result = "%r + %r*%%i" % (expr.real, expr.imag)
+        else:
+            result = repr(expr)
+
+        if not (result.startswith("(") and result.endswith(")")) \
+                and ("-" in result or "+" in result) \
+                and (enclosing_prec > PREC_SUM):
+            return self.parenthesize(result)
+        else:
+            return result
+
+
+# }}}
+
+# {{{ parser
+
+class MaximaParser(ParserBase):
+    power_sym = intern("power")
+    imag_unit = intern("imag_unit")
+
+    lex_table = [
+            (power_sym, pytools.lex.RE(r"\^")),
+            (imag_unit, pytools.lex.RE(r"%i")),
+            ] + ParserBase.lex_table
+
+    def parse_terminal(self, pstate):
+        import pymbolic.primitives as primitives
+
+        next_tag = pstate.next_tag()
+        import pymbolic.parser as p
+        if next_tag is p._int:
+            return int(pstate.next_str_and_advance())
+        elif next_tag is p._float:
+            return float(pstate.next_str_and_advance())
+        elif next_tag is self.imag_unit:
+            pstate.advance()
+            return 1j
+        elif next_tag is p._identifier:
+            return primitives.Variable(pstate.next_str_and_advance())
+        else:
+            pstate.expected("terminal")
+
+    def parse_postfix(self, pstate, min_precedence, left_exp):
+        import pymbolic.primitives as primitives
+        import pymbolic.parser as p
+
+        did_something = False
+
+        next_tag = pstate.next_tag()
+
+        if next_tag is p._openpar and p._PREC_CALL > min_precedence:
+            pstate.advance()
+            pstate.expect_not_end()
+            if next_tag is p._closepar:
+                pstate.advance()
+                left_exp = primitives.Call(left_exp, ())
+            else:
+                args = self.parse_expression(pstate)
+                if not isinstance(args, tuple):
+                    args = (args,)
+                left_exp = primitives.Call(left_exp, args)
+                pstate.expect(p._closepar)
+                pstate.advance()
+            did_something = True
+        elif next_tag is p._openbracket and p._PREC_CALL > min_precedence:
+            pstate.advance()
+            pstate.expect_not_end()
+            left_exp = primitives.Subscript(left_exp, self.parse_expression(pstate))
+            pstate.expect(p._closebracket)
+            pstate.advance()
+            did_something = True
+        elif next_tag is p._dot and p._PREC_CALL > min_precedence:
+            pstate.advance()
+            pstate.expect(p._identifier)
+            left_exp = primitives.Lookup(left_exp, pstate.next_str())
+            pstate.advance()
+            did_something = True
+        elif next_tag is p._plus and p._PREC_PLUS > min_precedence:
+            pstate.advance()
+            left_exp += self.parse_expression(pstate, p._PREC_PLUS)
+            did_something = True
+        elif next_tag is p._minus and p._PREC_PLUS > min_precedence:
+            pstate.advance()
+            left_exp -= self.parse_expression(pstate, p._PREC_PLUS)
+            did_something = True
+        elif next_tag is p._times and p._PREC_TIMES > min_precedence:
+            pstate.advance()
+            left_exp *= self.parse_expression(pstate, p._PREC_TIMES)
+            did_something = True
+        elif next_tag is p._over and p._PREC_TIMES > min_precedence:
+            pstate.advance()
+            from pymbolic.primitives import Quotient
+            left_exp = Quotient(left_exp, self.parse_expression(pstate, p._PREC_TIMES))
+            did_something = True
+        elif next_tag is self.power_sym and p._PREC_POWER > min_precedence:
+            pstate.advance()
+            left_exp **= self.parse_expression(pstate, p._PREC_POWER)
+            did_something = True
+
+        return left_exp, did_something
+
+# }}}
+
+
+
+
+# {{{ pexpect-based driver
+
+class MaximaKernel:
+    def __init__(self, executable="maxima", timeout=30):
+        self.executable = executable
+        self.timeout = timeout
+
+        self._initialize()
+
+    # {{{ internal
+
+    def _initialize(self):
+        self.child = pexpect.spawn(self.executable,
+                ["--disable-readline", "-q"],
+                timeout=self.timeout)
+        #import sys
+        #self.child.logfile = sys.stdout
+        self.current_prompt = 0
+        self._expect_prompt(IN_PROMPT_RE)
+
+        self.exec_str("display2d:false")
+        self.exec_str("keepfloat:true")
+
+    def _expect_prompt(self, prompt_re):
+        if prompt_re is IN_PROMPT_RE:
+            self.current_prompt += 1
+
+        which = self.child.expect([prompt_re, ERROR_PROMPT_RE, ASK_RE])
+        if which == 0:
+            assert int(self.child.match.group(1)) == self.current_prompt
+            return
+        if which == 1:
+            txt = self.child.before+self.child.after+self.child.readline()
+            self.restart()
+            raise MaximaError("maxima encountered an error and had to be restarted:\n%s\n%s\n%s"
+                    % (75*"-", txt.rstrip("\n"), 75*"-"))
+        elif which == 2:
+            txt = self.child.before+self.child.after+self.child.readline()
+            self.restart()
+            raise MaximaError("maxima asked a question and had to be restarted:\n%s\n%s\n%s"
+                    % (75*"-", txt.rstrip("\n"), 75*"-"))
+        else:
+            self.restart()
+            raise MaximaError("unexpected output from maxima, restarted")
+
+    # }}}
+
+    # {{{ execution control
+
+    def restart(self):
+        from signal import SIGKILL
+        self.child.kill(SIGKILL)
+        self._initialize()
+
+    def shutdown(self):
+        self.child.sendline("quit();")
+        self.child.wait()
+
+    # }}}
+
+    # {{{ string interface
+
+    def exec_str(self, s):
+        self.child.sendline(s+";")
+        self._expect_prompt(IN_PROMPT_RE)
+
+    def eval_str(self, s):
+        cmd = s+";"
+        self.child.sendline(cmd)
+        s_echo = self.child.readline()
+        assert s_echo.strip() == cmd.strip()
+
+        self._expect_prompt(OUT_PROMPT_RE)
+        self._expect_prompt(IN_PROMPT_RE)
+
+        result, _ = MULTI_WHITESPACE.subn(" ", self.child.before)
+        return result
+
+    def reset(self):
+        self.current_prompt = 0
+        self.exec_str("kill(all)")
+
+    def clean_eval_str_with_setup(self, setup_lines, s):
+        self.reset()
+        for l in setup_lines:
+            self.exec_str(l)
+
+        return self.eval_str(s)
+
+    # }}}
+
+    # {{{ expression interface
+
+    def eval_expr(self, expr):
+        input_str = MaximaStringifyMapper()(expr)
+        result_str = self.eval_str(input_str)
+        return MaximaParser()(result_str)
+
+    def clean_eval_expr_with_setup(self, assignments, expr):
+        strify = MaximaStringifyMapper()
+
+        if isinstance(expr, str):
+            input_str = expr
+        else:
+            input_str = strify(expr)
+
+        def make_setup(assignment):
+            if isinstance(assignment, str):
+                return assignment
+            else:
+                name, value = assignment
+                return"%s: %s" % (name, strify(value))
+
+        result_str = self.clean_eval_str_with_setup(
+                [make_setup(assignment) for assignment in assignments],
+                input_str)
+        return MaximaParser()(result_str)
+
+    # }}}
+
+# }}}
+
+# vim: fdm=marker
diff --git a/test/test_maxima.py b/test/test_maxima.py
new file mode 100644
index 0000000..761aeca
--- /dev/null
+++ b/test/test_maxima.py
@@ -0,0 +1,74 @@
+from pymbolic.maxima import MaximaKernel, MaximaError
+
+def test_kernel():
+    knl = MaximaKernel()
+    knl.exec_str("k:1/(sqrt((x0-(a+t*b))^2+(y0-(c+t*d))^2+(z0-(e+t*f))^2))")
+    knl.eval_str("sum(diff(k, t,deg)*t^deg,deg,0,6)")
+    assert knl.eval_str("2+2").strip() == "4"
+    knl.shutdown()
+
+def pytest_funcarg__knl(request):
+    knl = MaximaKernel()
+    request.addfinalizer(knl.shutdown)
+    return knl
+
+def test_setup(knl):
+    knl.clean_eval_str_with_setup(
+            ["k:1/(sqrt((x0-(a+t*b))^2+(y0-(c+t*d))^2+(z0-(e+t*f))^2))"],
+            "sum(diff(k, t,deg)*t^deg,deg,0,6)")
+
+def test_error(knl):
+    try:
+        knl.eval_str("))(!")
+    except MaximaError:
+        pass
+
+    try:
+        knl.eval_str("integrate(1/(x^3*(a+b*x)^(1/3)),x)")
+    except MaximaError:
+        pass
+
+def test_strict_round_trip(knl):
+    from pymbolic import parse
+    from pymbolic.primitives import Quotient
+
+    exprs = [
+            2j,
+            parse("x**y"),
+            Quotient(1, 2),
+            ]
+    for expr in exprs:
+        result = knl.eval_expr(expr)
+        round_trips_correctly = result == expr
+        if not round_trips_correctly:
+            print "ORIGINAL:"
+            print
+            print expr
+            print
+            print "POST-MAXIMA:"
+            print
+            print result
+        assert round_trips_correctly
+
+
+def test_lax_round_trip(knl):
+    from pymbolic.maxima import MaximaParser
+    k_setup = [
+            "k:1/(sqrt((x0-(a+t*b))^2+(y0-(c+t*d))^2))",
+            "result:sum(diff(k, t,deg)*t^deg,deg,0,6)",
+            ]
+    parsed = MaximaParser()(
+            knl.clean_eval_str_with_setup(k_setup, "result"))
+
+    assert knl.clean_eval_expr_with_setup(
+            k_setup + [("result2", parsed)],
+            "ratsimp(result-result2)") == 0
+
+
+if __name__ == "__main__":
+    import sys
+    if len(sys.argv) > 1:
+        exec(sys.argv[1])
+    else:
+        from py.test.cmdline import main
+        main([__file__])
-- 
GitLab