From: John H. <jdh...@ac...> - 2005-02-21 23:19:20
|
>>>>> "Andrew" == Andrew Straw <str...@as...> writes: Andrew> Hi All, I've started playing around with traits in what I Andrew> think is the logical first step -- the _transforms Andrew> 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. Andrew> Before I go too far, a couple of questions arising from Andrew> the fact that many of the extension types (Point, Bbox, Andrew> ...) have C members. The biggest concern is that a simple Andrew> re-implementation in traits would move all this to a Andrew> Python level and thus presumably slow things down. Is Andrew> 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 |