Wednesday, September 29, 2010

CAS: The Interface

I wrote in the tome-like overview of CAS that I would begin the technical posts with one about the parser. It makes more sense, however, to write first about how one uses CAS. After all, the interface is what unifies the rest of the program!




As always, I can only describe my program, here. To understand what CAS does, and how it does it, the best course of action would be to read the code and run the program; indeed, let this be my standing recommendation, for any software! That said, documentation is important, and so I offer the following.

Recall that CAS is implemented as a (collection of) Python module(s): one imports it to a Python instance and interacts with it at the REPL. I will give some insight into the structure of this module, and then give some sample input of a CAS session.

The module cas contains just one class (Expr), instances of which represent symbolic expressions. To initialize a new Expr, one must supply Expr's __init__ with a string containing a symbolic expression in the usual notation (note that exponentiation in CAS is represented by a '^'). Each operation that can be performed upon an Expr instance is represented by a method. Below is an abstract depiction of Expr.

Expr(expr_string)
    d(var)
      Differentiate the expression with respect to var; returns a new
      Expr.
    integrate(var)
      Find the indefinite integral (with respect to var) of the
      expression; returns a new Expr.
    plot(title=title, labels=labels, range=range)
      Plot the expression, using title as the title of the plot, labelling the
      axes with the strings contained in the tuple labels, and setting the
      axes' ranges according to the tuples given in range; returns None.
    __str__()
      Returns a string containing a traditional, infix representation of the
      expression.

As an example of how to use CAS, consider the following input to the REPL:

from cas import *

f = Expr('x^2+sin(x)')

print(f)
print('Derivative of %s: %s' % (f, f.d('x')))
print('Integral of %s: %s' % (f, f.integrate('x')))

f.plot(title='A function', labels=('x', 'f(x)'), range=((0,3.14),(0,10)))

Now, this is all one needs to know to get started playing with CAS. However, I would like to write more about the internals of Expr.plot and Expr.__str__  (Expr.integrate and Expr.d will get their own moments of glory in later posts).

Plotting
I did not want to write my own plotting engine. Being already familiar with Gnuplot, I decided that the quick-and-dirty way to bestow CAS with plotting facilities would be to pipe commands to a separate Gnuplot process. As an aside, it is not strictly true that CAS pipes instructions to Gnuplot. For various arcane reasons, Gnuplot did not want to play nicely with Python's subprocess functionalities; finally, I found that if I wrote the commands to a temporary file and then spawned Gnuplot (with the file as input) I could get Gnuplot to display the plot. This is not a very nice way to interoperate with Gnuplot; perhaps a more elegant solution exists?

Note also that Gnuplot uses a different convention for exponentiation: its operator is '**', whereas CAS's is '^'. It was easy, though, to substitute one for the other in the output driver, which brings me to the matter of Expr.__str__.

Pretty Printing
All the operations in CAS are performed on, and return, expression trees. An end-user is unlikely to want to see them (although one can, by evaluating the tree_repr attribute of an Expr). Thus the need for a pretty-printer to output expressions in the familiar infix syntax. Python provides a nice place to put this method -- any object's __str__ method is called whenever the object is used in the context of a string (e.g., when passing it to a print statement, or when concatenating it with a string). Thus, every Expr object has a __str__ method that converts its tree_repr into a string, and returns the result -- that  is how one can type 'print(f)' as above and display f in infix syntax.

I must pause here and warn any potential reader of the code that (as of 29 Sep) the machinery of Expr.__str__ is quite ugly. I intend to clean it up in the future, but I will let it be for now, for it works.

__str__ itself does not directly convert trees to strings. It calls the private method __format,  passing it the gnuplot_mode flag, which if true, signals __format to substitute '**' for '^' in the output. __format, quite briefly, calls a sub-function (converter) that takes the tree_repr and solves a classic problem in data structures, converting it into a list (really a Python-list, which is a sort of extensible array). There is a twist: while an expression tree requires no parentheses, an infix representation does. In order to guarantee proper parenthesization, converter needed to know about functions, negations, and operator precedence. The last case required the most effort, and proper parenthesization of operator expressions was only accomplished in an ugly fashion. It is, indeed, responsible for the ugliness of this part of the program. I feel that an elegant solution exists, however, and I intend to implement it -- eventually!

No comments:

Post a Comment