moving to traits

Hi All, I've started playing around with traits in what I

    > think is the logical first step -- the _transforms
    > extension module.

I've been wondering about this myself. It is not a necessary step,
but may be desirable. Not necessary because we could use traits for
things like artist properties (line widths, edge colors and the like)
where the GUI editor features will be useful to many, and keep the
transforms implementation traits free. But it may be desirable
because the transform framework was designed to solve many of the
problems traits solves and is currently a bit hairy -- traits may
offer us a chance to clean the transforms implementation and
interface in a way that is user extensible.

traits and mpl transforms take different approaches to solving the
problem of keeping the transform updated with respect to things like
changes in window size and view limits, and if we were to refactor the
transforms architecture to use traits I think a total rewrite would be
appropriate. I wouldn't focus on a class by class rewrite (Value,
Point, Interval, Bbox) because this scheme was designed to support the
LazyValue architecture which would become obsolete. Rather I would do
a rewrite that caters to the strength of traits. A sketch in this
direction is included below.

    > Before I go too far, a couple of questions arising from
    > the fact that many of the extension types (Point, Bbox,
    > ...) have C members. The biggest concern is that a simple
    > re-implementation in traits would move all this to a
    > Python level and thus presumably slow things down. Is
    > this perceived to be a significant issue?

Basically, with traits you would use the observer pattern to update
the affines when display or view limits change. The mpl approach is
to defer evaluation of the arithmetic until render time (lazy values
with arithmetic ops overloaded). Performance is a problem here --
this was originally done in python and was too slow. But with an
observer pattern, you wouldn't need the extension code since you
wouldn't be doing lazy evaluation at all.

I think the basic interface should be:

  A transformation is an (optional) nonlinear transform that takes an
  x,y pair and returns an x,y pair in cartesian coords and also
  supplies an affine transformation to map these cartesian coords to
  display space.

Transformation gurus, does this cover the spectrum?

One thing that is nice about the current implementation, which does
the nonlinear part in extension code, is that it allows us to
effectively and efficiently deal with nan even thought there isn't
consistent support for these in the python / numerix. Eg in
backend_agg draw_lines

  for (size_t i=0; i<Nx; ++i) {
    thisx = *(double *)(xa->data + i*xa->strides[0]);
    thisy = *(double *)(ya->data + i*ya->strides[0]);
    
    if (needNonlinear)
      try {
  mpltransform->nonlinear_only_api(&thisx, &thisy);
      }
      catch (...) {
  moveto = true;
  continue;
      }

Basically, when the transform throws a domain error, the point is
skipped and the moveto code is set [ C++ mavens, I know the catch(...)
thing is bad form but was added as a quick workaround with gcc 3.4.x
was giving us hell trying to catch the std::domain_error explicitly. ]

To do this at the numstar level would require an extra pass through
the data (once for the transform and once for the draw_lines) and we
would need some support for illegal values, either as nan or as a
masked array. In the case of log transforms, to take a concrete
example, preprocessing the data to screen out the non-positive elements
would probably require an additional pass still. So for performance
reasons, there is something to be said for doing the nonlinear part in
extension code. For easy extensibility though, the converse is
certainly true. Perhaps it's possible to have the best of both
worlds.

In any case, here is the start of how I would define some of the core
objects (Bbox and Affine). Value and Point would disappear as they
arose from the lazy value scheme. Interval would be easy to define
using an observer pattern on the Bbox.

from matplotlib.enthought.traits import Trait, HasTraits, Float
from matplotlib.numerix.mlab import amin, amax, rand

class Bbox(HasTraits):
    left = Float
    bottom = Float
    right = Float
    top = Float

    def __init__(self, l=0., b=0., r=1., t=1.):
        HasTraits.__init__(self)
        self.left = l
        self.bottom = b
        self.right = r
        self.top = t
        
    def width(self):
        return self.right - self.left

    def height(self):
        return self.top - self.bottom

    def update_numerix(self, x, y):
        'update the bbox to make sure it contains x and y'
        # This method is used to update the datalim; the python
        # impl. requires 4 passes through the data; the extension code
        # version requires only one loop. But we could make provide
        # an extension code helper function, eg with signature
        # minx,miny, maxx, maxy = _get_bounds(x,y)
        # if we want
        minx = amin(x)
        maxx = amax(x)
        miny = amin(y)
        maxy = amax(y)
        if minx<self.left: self.left = minx
        if maxx>self.right: self.right = maxx
        if miny<self.bottom: self.bottom = miny
        if maxy>self.top: self.top = maxy

        # the current extension code also tracks the minposx and
        # minposy for log transforms

class ProductBbox(Bbox):
    """
    Product of bounding boxes - mainly used as specialty bbox for axes
    where the bbox1 is in relative (0,1) coords, bbox2 is in display.
    The axes pixel bounds are maintained as the product of these two
    boxes
    """
    bbox1 = Bbox
    bbox2 = Bbox
    def __init__(self, bbox1, bbox2):
        Bbox.__init__(self)
        self.bbox1 = bbox1
        self.bbox2 = bbox2
        self.update()
        bbox1.on_trait_change(self.update)
        bbox2.on_trait_change(self.update)
    def update(self, *args):
        self.left = self.bbox1.left * self.bbox2.left
        self.right = self.bbox1.right * self.bbox2.right
        self.bottom = self.bbox1.bottom * self.bbox2.bottom
        self.top = self.bbox1.top * self.bbox2.top

class Affine(HasTraits):
    a = Float
    b = Float
    c = Float
    d = Float
    tx = Float
    ty = Float

    def __repr__(self):
        return ', '.join(['%1.3f'%val for val in (self.a, self.b,
                                                  self.c, self.d,
                                                  self.tx, self.ty)])
class BboxAffine(Affine):
    bbox1 = Bbox
    bbox2 = Bbox
    def __init__(self, bbox1, bbox2):
        Affine.__init__(self)
        self.bbox1 = bbox1
        self.bbox2 = bbox2
        self.update()
        bbox1.on_trait_change(self.update)
        bbox2.on_trait_change(self.update)

    def update(self, *args):
        sx = self.bbox2.width()/self.bbox1.width()
        sy = self.bbox2.height()/self.bbox1.height()

        tx = -self.bbox1.left*sx + self.bbox2.left
        ty = -self.bbox1.bottom*sy + self.bbox2.bottom
        self.a = sx
        self.b = 0.
        self.c = 0.
        self.d = sy
        self.tx = tx
        self.ty = ty

viewbox = Bbox(1, 2, 3, 3) # data coords

axfrac = Bbox(0.1, 0.1, 0.8, 0.8) # fraction
figbox = Bbox(0,0,400,400) # pixels
axbox = ProductBbox(axfrac, figbox)

axtrans = BboxAffine(viewbox, axbox)
print axtrans

# now to a figure resize
figbox.right = 600
figbox.bottom = 500

# and change the view lim
viewbox.update_numerix(5*rand(100), 5*rand(100))

# and check the affine
print axtrans