Index: sansutil/calcthread.py
===================================================================
--- sansutil/calcthread.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/calcthread.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,301 @@
+# This program is public domain
+
+## \file
+#  \brief Abstract class for defining calculation threads.
+#
+
+import thread, traceback
+
+import sys
+
+if sys.platform.count("darwin")>0:
+    import time
+    stime = time.time()
+    def clock():
+        return time.time()-stime
+    def sleep(t):
+        return time.sleep(t)    
+else:
+    from time import clock
+    from time import sleep
+
+class CalcThread:
+    """Threaded calculation class.  Inherit from here and specialize
+    the compute() method to perform the appropriate operations for the
+    class.
+
+    If you specialize the __init__ method be sure to call
+    CalcThread.__init__, passing it the keyword arguments for
+    yieldtime, worktime, update and complete.
+    
+    When defining the compute() method you need to include code which
+    allows the GUI to run.  They are as follows:
+        self.isquit()          call frequently to check for interrupts
+        self.update(kw=...)    call when the GUI could be updated
+        self.complete(kw=...)  call before exiting compute()
+    The update() and complete() calls accept field=value keyword
+    arguments which are passed to the called function.  complete()
+    should be called before exiting the GUI function.  A KeyboardInterrupt
+    event is triggered if the GUI signals that the computation should
+    be halted.
+
+    The following documentation should be included in the description
+    of the derived class.
+
+    The user of this class will call the following:
+
+        thread = Work(...,kw=...)  prepare the work thread.
+        thread.queue(...,kw=...)   queue a work unit
+        thread.requeue(...,kw=...) replace work unit on the end of queue
+        thread.reset(...,kw=...)   reset the queue to the given work unit
+        thread.stop()              clear the queue and halt
+        thread.interrupt()         halt the current work unit but continue
+        thread.ready(delay=0.)     request an update signal after delay
+        thread.isrunning()         returns true if compute() is running
+
+    Use queue() when all work must be done.  Use requeue() when intermediate
+    work items don't need to be done (e.g., in response to a mouse move
+    event).  Use reset() when the current item doesn't need to be completed
+    before the new event (e.g., in response to a mouse release event).  Use
+    stop() to halt the current and pending computations (e.g., in response to
+    a stop button).
+    
+    The methods queue(), requeue() and reset() are proxies for the compute()
+    method in the subclass.  Look there for a description of the arguments.
+    The compute() method can be called directly to run the computation in
+    the main thread, but it should not be called if isrunning() returns true.
+
+    The constructor accepts additional keywords yieldtime=0.01 and
+    worktime=0.01 which determine the cooperative multitasking
+    behaviour.  Yield time is the duration of the sleep period
+    required to give other processes a chance to run.  Work time
+    is the duration between sleep periods.
+
+    Notifying the GUI thread of work in progress and work complete
+    is done with updatefn=updatefn and completefn=completefn arguments
+    to the constructor.  Details of the parameters to the functions
+    depend on the particular calculation class, but they will all
+    be passed as keyword arguments.  Details of how the functions
+    should be implemented vary from framework to framework.
+
+    For wx, something like the following is needed:
+
+        import wx, wx.lib.newevent
+        (CalcCompleteEvent, EVT_CALC_COMPLETE) = wx.lib.newevent.NewEvent()
+
+        # methods in the main window class of your application
+        def __init__():
+            ...
+            # Prepare the calculation in the GUI thread.
+            self.work = Work(completefn=self.CalcComplete)
+            self.Bind(EVT_CALC_COMPLETE, self.OnCalcComplete)
+            ...
+            # Bind work queue to a menu event.
+            self.Bind(wx.EVT_MENU, self.OnCalcStart, id=idCALCSTART)
+            ...
+
+        def OnCalcStart(self,event):
+            # Start the work thread from the GUI thread.
+            self.work.queue(...work unit parameters...)
+
+        def CalcComplete(self,**kwargs):
+            # Generate CalcComplete event in the calculation thread.
+            # kwargs contains field1, field2, etc. as defined by
+            # the Work thread class.
+            event = CalcCompleteEvent(**kwargs)
+            wx.PostEvent(self, event)
+
+        def OnCalcComplete(self,event):
+            # Process CalcComplete event in GUI thread.
+            # Use values from event.field1, event.field2 etc. as
+            # defined by the Work thread class to show the results.
+            ...
+    """
+
+    def __init__(self,
+                 completefn = None,
+                 updatefn   = None,
+                 yieldtime  = 0.01,
+                 worktime   = 0.01
+                 ):
+        """Prepare the calculator"""
+        self.yieldtime     = yieldtime
+        self.worktime      = worktime
+        self.completefn    = completefn
+        self.updatefn      = updatefn
+        self._interrupting = False
+        self._running      = False
+        self._queue        = []
+        self._lock         = thread.allocate_lock()
+        self._delay        = 1e6
+
+    def queue(self,*args,**kwargs):
+        """Add a work unit to the end of the queue.  See the compute()
+        method for details of the arguments to the work unit."""
+        self._lock.acquire()
+        self._queue.append((args,kwargs))
+        # Cannot do start_new_thread call within the lock
+        self._lock.release()
+        if not self._running:
+            self._time_for_update = clock()+1e6
+            thread.start_new_thread(self._run,())
+
+    def requeue(self,*args,**kwargs):
+        """Replace the work unit on the end of the queue.  See the compute()
+        method for details of the arguments to the work unit."""
+        self._lock.acquire()
+        self._queue = self._queue[:-1]
+        self._lock.release()
+        self.queue(*args,**kwargs)
+
+    def reset(self,*args,**kwargs):
+        """Clear the queue and start a new work unit.  See the compute()
+        method for details of the arguments to the work unit."""
+        self.stop()
+        self.queue(*args,**kwargs)
+
+    def stop(self):
+        """Clear the queue and stop the thread.  New items may be
+        queued after stop.  To stop just the current work item, and
+        continue the rest of the queue call the interrupt method"""
+        self._lock.acquire()
+        self._interrupting = True
+        self._queue = []
+        self._lock.release()
+
+    def interrupt(self):
+        """Stop the current work item.  To clear the work queue as
+        well call the stop() method."""
+        self._lock.acquire()
+        self._interrupting = True
+        self._lock.release()
+
+    def isrunning(self): return self._running
+
+    def ready(self, delay=0.):
+        """Ready for another update after delay=t seconds.  Call
+        this for threads which can show intermediate results from
+        long calculations."""
+        self._delay = delay
+        self._lock.acquire()
+        self._time_for_update = clock() + delay
+        # print "setting _time_for_update to ",self._time_for_update
+        self._lock.release()
+
+    def isquit(self):
+        """Check for interrupts.  Should be called frequently to
+        provide user responsiveness.  Also yields to other running
+        threads, which is required for good performance on OS X."""
+
+        # Only called from within the running thread so no need to lock
+        if self._running and self.yieldtime>0 and clock()>self._time_for_nap:
+            # print "sharing"
+            sleep(self.yieldtime)
+            self._time_for_nap = clock() + self.worktime
+        if self._interrupting: raise KeyboardInterrupt
+
+    def update(self,**kwargs):
+
+        """Update GUI with the lastest results from the current work unit."""
+        if self.updatefn != None and clock() > self._time_for_update:
+            self._lock.acquire()
+            self._time_for_update = clock() + self._delay
+            self._lock.release()
+            self._time_for_update += 1e6  # No more updates
+            
+            self.updatefn(**kwargs)
+            sleep(self.yieldtime)
+            if self._interrupting: raise KeyboardInterrupt
+        else:
+            self.isquit()
+        return
+
+    def complete(self,**kwargs):
+        """Update the GUI with the completed results from a work unit."""
+        if self.completefn != None:
+            self.completefn(**kwargs)
+            sleep(self.yieldtime)
+        return
+
+    def compute(self,*args,**kwargs):
+        """Perform a work unit.  The subclass will provide details of
+        the arguments."""
+        raise NotImplemented, "Calculation thread needs compute method"
+
+    def _run(self):
+        """Internal function to manage the thread."""
+        # The code for condition wait in the threading package is
+        # implemented using polling.  I'll accept for now that the
+        # authors of this code are clever enough that polling is
+        # difficult to avoid.  Rather than polling, I will exit the
+        # thread when the queue is empty and start a new thread when
+        # there is more work to be done.
+        while 1:
+            self._lock.acquire()
+            # print "lock aquired"
+            self._time_for_nap = clock()+self.worktime
+            self._running = True
+            if self._queue == []: break
+            self._interrupting = False
+            args,kwargs = self._queue[0]
+            self._queue = self._queue[1:]
+            self._lock.release()
+            # print "lock released"
+            try:
+                self.compute(*args,**kwargs)
+            except KeyboardInterrupt:
+                pass
+            except:
+                traceback.print_exc()
+                #print 'CalcThread exception',
+        self._running = False
+
+# ======================================================================
+# Demonstration of calcthread in action
+class CalcDemo(CalcThread):
+    """Example of a calculation thread."""
+    def compute(self,n):
+        total = 0.
+        for i in range(n):
+            self.update(i=i)
+            for j in range(n):
+                self.isquit()
+                total += j
+        self.complete(total=total)
+
+class CalcCommandline:
+    def __init__(self, n=20000):
+        print thread.get_ident()
+        self.starttime = clock()
+        self.done = False
+        self.work = CalcDemo(completefn=self.complete,
+                             updatefn=self.update, yieldtime=0.001)
+        self.work2 = CalcDemo(completefn=self.complete,
+                             updatefn=self.update)
+        self.work3 = CalcDemo(completefn=self.complete,
+                             updatefn=self.update)
+        self.work.queue(n)
+        self.work2.queue(n)
+        self.work3.queue(n)
+        print "Expect updates from Main every second and from thread every 2.5 seconds"
+        print ""
+        self.work.ready(.5)
+        while not self.done:
+            sleep(1)
+            print "Main thread %d at %.2f"%(thread.get_ident(),clock()-self.starttime)
+
+    def update(self,i=0):
+        print "Update i=%d from thread %d at %.2f"%(i,thread.get_ident(),clock()-self.starttime)
+        self.work.ready(2.5)
+
+    def complete(self,total=0.0):
+        print "Complete total=%g from thread %d at %.2f"%(total,thread.get_ident(),clock()-self.starttime)
+        self.done = True
+
+if __name__ == "__main__":
+    CalcCommandline()
+
+# version
+__id__ = "$Id: calcthread.py 249 2007-06-15 17:03:01Z ziwen $"
+
+# End of file
Index: sansutil/err1d.py
===================================================================
--- sansutil/err1d.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/err1d.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,146 @@
+# This program is public domain
+
+"""
+Error propogation algorithms for simple arithmetic
+
+Warning: like the underlying numpy library, the inplace operations
+may return values of the wrong type if some of the arguments are
+integers, so be sure to create them with floating point inputs.
+"""
+
+
+from __future__ import division  # Get true division
+import numpy
+
+def div(X,varX, Y,varY):
+    """Division with error propagation"""
+    # Direct algorithm:
+    #   Z = X/Y
+    #   varZ = (varX/X**2 + varY/Y**2) * Z**2
+    #        = (varX + varY * Z**2) / Y**2
+    # Indirect algorithm to minimize intermediates
+    Z = X/Y      # truediv => Z is a float
+    varZ = Z**2  # Z is a float => varZ is a float
+    varZ *= varY
+    varZ += varX
+    T = Y**2     # Doesn't matter if T is float or int
+    varZ /= T
+    return Z, varZ
+
+def mul(X,varX, Y,varY):
+    """Multiplication with error propagation"""
+    # Direct algorithm:
+    Z = X * Y
+    varZ = Y**2 * varX + X**2 * varY
+    # Indirect algorithm won't ensure floating point results
+    #   varZ = Y**2
+    #   varZ *= varX
+    #   Z = X**2   # Using Z to hold the temporary
+    #   Z *= varY
+    #   varZ += Z
+    #   Z[:] = X
+    #   Z *= Y
+    return Z, varZ
+
+def sub(X,varX, Y, varY):
+    """Subtraction with error propagation"""
+    Z = X - Y
+    varZ = varX + varY
+    return Z, varZ
+
+def add(X,varX, Y,varY):
+    """Addition with error propagation"""
+    Z = X + Y
+    varZ = varX + varY
+    return Z, varZ
+
+def exp(X,varX):
+    """Exponentiation with error propagation"""
+    Z = numpy.exp(X)
+    varZ = varX * Z**2
+    return Z,varZ
+
+def log(X,varX):
+    """Logarithm with error propagation"""
+    Z = numpy.log(X)
+    varZ = varX / X**2
+    return Z,varZ
+
+# Confirm this formula before using it
+# def pow(X,varX, Y,varY):
+#    Z = X**Y
+#    varZ = (Y**2 * varX/X**2 + varY * numpy.log(X)**2) * Z**2
+#    return Z,varZ
+#
+
+def pow(X,varX,n):
+    """X**n with error propagation"""
+    # Direct algorithm
+    #   Z = X**n
+    #   varZ = n*n * varX/X**2 * Z**2
+    # Indirect algorithm to minimize intermediates
+    Z = X**n
+    varZ = varX/X
+    varZ /= X
+    varZ *= Z
+    varZ *= Z
+    varZ *= n**2
+    return Z, varZ
+
+def div_inplace(X,varX, Y,varY):
+    """In-place division with error propagation"""
+    # Z = X/Y
+    # varZ = (varX + varY * (X/Y)**2) / Y**2 = (varX + varY * Z**2) / Y**2
+    X /= Y     # X now has Z = X/Y
+    T = X**2   # create T with Z**2
+    T *= varY  # T now has varY * Z**2
+    varX += T  # varX now has varX + varY*Z**2
+    del T   # may want to use T[:] = Y for vectors
+    T = Y   # reuse T for Y
+    T **=2     # T now has Y**2
+    varX /= T  # varX now has varZ
+    return X,varX
+
+def mul_inplace(X,varX, Y,varY):
+    """In-place multiplication with error propagation"""
+    # Z = X * Y
+    # varZ = Y**2 * varX + X**2 * varY
+    T = Y**2   # create T with Y**2
+    varX *= T  # varX now has Y**2 * varX
+    del T   # may want to use T[:] = X for vectors
+    T = X   # reuse T for X**2 * varY
+    T **=2     # T now has X**2
+    T *= varY  # T now has X**2 * varY
+    varX += T  # varX now has varZ
+    X *= Y     # X now has Z
+    return X,varX
+
+def sub_inplace(X,varX, Y, varY):
+    """In-place subtraction with error propagation"""
+    # Z = X - Y
+    # varZ = varX + varY
+    X -= Y
+    varX += varY
+    return X,varX
+
+def add_inplace(X,varX, Y,varY):
+    """In-place addition with error propagation"""
+    # Z = X + Y
+    # varZ = varX + varY
+    X += Y
+    varX += varY
+    return X,varX
+
+def pow_inplace(X,varX,n):
+    """In-place X**n with error propagation"""
+    # Direct algorithm
+    #   Z = X**n
+    #   varZ = abs(n) * varX/X**2 * Z**2
+    # Indirect algorithm to minimize intermediates
+    varX /= X
+    varX /= X     # varX now has varX/X**2
+    X **= n       # X now has Z = X**n
+    varX *= X     
+    varX *= X     # varX now has varX/X**2 * Z**2
+    varX *= n**2  # varX now has varZ
+    return X,varX
Index: sansutil/formatnum.py
===================================================================
--- sansutil/formatnum.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/formatnum.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,437 @@
+# This program is public domain
+# Author: Paul Kienzle
+"""
+Format values and uncertainties nicely for printing.
+
+:func:`format_uncertainty_pm`(v,err) produces the expanded format v +/- err.
+
+:func:`format_uncertainty_compact`(v,err) produces the compact format v(##), 
+where the number in parenthesis is the uncertainty in the last two digits of v.
+
+:func:`format_uncertainty`(v,err) uses the compact format by default, but this 
+can be changed to use the expanded +/- format by setting 
+format_uncertainty.compact to False.
+    
+The formatted string uses only the number of digits warranted by
+the uncertainty in the measurement.
+
+If the uncertainty is 0 or not otherwise provided, the simple
+%g floating point format option is used.
+
+Infinite and indefinite numbers are represented as inf and NaN.
+
+Example::
+
+    >>> v,dv = 757.2356,0.01032
+    >>> print format_uncertainty_pm(v,dv)
+    757.236 +/- 0.010
+    >>> print format_uncertainty_compact(v,dv)
+    757.236(10)
+    >>> print format_uncertainty(v,dv)
+    757.236(10)
+    >>> format_uncertainty.compact = False
+    >>> print format_uncertainty(v,dv)
+    757.236 +/- 0.010
+
+UncertaintyFormatter() returns a private formatter with its own 
+formatter.compact flag.
+"""
+from __future__ import division
+
+import math
+import numpy
+__all__ = ['format_uncertainty', 'format_uncertainty_pm',
+           'format_uncertainty_compact']
+
+# Coordinating scales across a set of numbers is not supported.  For easy
+# comparison a set of numbers should be shown in the same scale.  One could
+# force this from the outside by adding scale parameter (either 10**n, n, or
+# a string representing the desired SI prefix) and having a separate routine
+# which computes the scale given a set of values.
+
+# Coordinating scales with units offers its own problems.  Again, the user
+# may want to force particular units.  This can be done by outside of the
+# formatting routines by scaling the numbers to the appropriate units then
+# forcing them to print with scale 10**0.  If this is a common operation,
+# however, it may want to happen inside.
+
+# The value e<n> is currently formatted into the number.  Alternatively this
+# scale factor could be returned so that the user can choose the appropriate
+# SI prefix when printing the units.  This gets tricky when talking about
+# composite units such as 2.3e-3 m**2 -> 2300 mm**2, and with volumes
+# such as 1 g/cm**3 -> 1 kg/L.
+
+def format_uncertainty_pm(value, uncertainty):
+    """
+    Given *value* v and *uncertainty* dv, return a string v +/- dv.
+    """
+    return _format_uncertainty(value, uncertainty, compact=False)
+
+def format_uncertainty_compact(value, uncertainty):
+    """
+    Given *value* v and *uncertainty* dv, return the compact
+    representation v(##), where ## are the first two digits of 
+    the uncertainty.
+    """
+    return _format_uncertainty(value, uncertainty, compact=True)
+
+class UncertaintyFormatter:
+    """
+    Value and uncertainty formatter.
+
+    The *formatter* instance will use either the expanded v +/- dv form 
+    or the compact v(##) form depending on whether *formatter.compact* is 
+    True or False.  The default is True.
+    """
+    compact = True
+    def __call__(self, value, uncertainty):
+        """
+        Given *value* and *uncertainty*, return a string representation.
+        """
+        return _format_uncertainty(value, uncertainty, self.compact)
+format_uncertainty = UncertaintyFormatter()
+
+def _format_uncertainty(value, uncertainty, compact):
+    """
+    Implementation of both the compact and the +/- formats.
+    """
+    # Handle indefinite value
+    if numpy.isinf(value):
+        return "inf" if value > 0 else "-inf"
+    if numpy.isnan(value):
+        return "NaN"
+
+    # Handle indefinite uncertainty
+    if uncertainty is None or uncertainty<=0 or numpy.isnan(uncertainty):
+        return "%g"%value
+    if numpy.isinf(uncertainty):
+        if compact:
+            return "%.2g(inf)"%value
+        else:
+            return "%.2g +/- inf"%value
+
+    # Handle zero and negative values
+    sign = "-" if value < 0 else ""
+    value = abs(value)
+
+    # Determine scale of value and error
+    err_place = int(math.floor(math.log10(uncertainty)))
+    if value == 0:
+        val_place = err_place - 1
+    else:
+        val_place = int(math.floor(math.log10(value)))
+
+    if err_place > val_place:
+        # Degenerate case: error bigger than value
+        # The mantissa is 0.#(##)e#, 0.0#(##)e# or 0.00#(##)e#
+        val_place = err_place+2
+    elif err_place == val_place:
+        # Degenerate case: error and value the same order of magnitude
+        # The value is ##(##)e#, #.#(##)e# or 0.##(##)e#
+        val_place = err_place+1
+    elif err_place <= 1 and val_place >= -3:
+        # Normal case: nice numbers and errors
+        # The value is ###.###(##)
+        val_place = 0
+    else:
+        # Extreme cases: zeros before value or after error
+        # The value is ###.###(##)e#, ##.####(##)e# or #.#####(##)e#
+        pass
+    
+    # Force engineering notation, with exponent a multiple of 3
+    val_place = int(math.floor(val_place/3.))*3
+
+    # Format the result
+    digits_after_decimal = abs(val_place - err_place + 1)
+    val_str = "%.*f"%(digits_after_decimal,value/10.**val_place)
+    exp_str = "e%d"%val_place if val_place != 0 else ""
+    if compact:
+        err_str = "(%2d)"%int(uncertainty/10.**(err_place-1)+0.5)
+        result = "".join((sign,val_str,err_str,exp_str))
+    else:
+        err_str = "%.*f"%(digits_after_decimal,uncertainty/10.**val_place)
+        result = "".join((sign,val_str,exp_str+" +/- ",err_str,exp_str))
+    #print sign,value, uncertainty, "=>", result
+    return result
+
+
+def test_compact():
+    # Oops... renamed function after writing tests
+    value_str = format_uncertainty_compact
+
+    # val_place > err_place
+    assert value_str(1235670,766000) == "1.24(77)e6"
+    assert value_str(123567.,76600) == "124(77)e3"
+    assert value_str(12356.7,7660) == "12.4(77)e3"
+    assert value_str(1235.67,766) == "1.24(77)e3"
+    assert value_str(123.567,76.6) == "124(77)"
+    assert value_str(12.3567,7.66) == "12.4(77)"
+    assert value_str(1.23567,.766) == "1.24(77)"
+    assert value_str(.123567,.0766) == "0.124(77)"
+    assert value_str(.0123567,.00766) == "0.0124(77)"
+    assert value_str(.00123567,.000766) == "0.00124(77)"
+    assert value_str(.000123567,.0000766) == "124(77)e-6"
+    assert value_str(.0000123567,.00000766) == "12.4(77)e-6"
+    assert value_str(.00000123567,.000000766) == "1.24(77)e-6"
+    assert value_str(.000000123567,.0000000766) == "124(77)e-9"
+    assert value_str(.00000123567,.0000000766) == "1.236(77)e-6"
+    assert value_str(.0000123567,.0000000766) == "12.357(77)e-6"
+    assert value_str(.000123567,.0000000766) == "123.567(77)e-6"
+    assert value_str(.00123567,.000000766) == "0.00123567(77)"
+    assert value_str(.0123567,.00000766) == "0.0123567(77)"
+    assert value_str(.123567,.0000766) == "0.123567(77)"
+    assert value_str(1.23567,.000766) == "1.23567(77)"
+    assert value_str(12.3567,.00766) == "12.3567(77)"
+    assert value_str(123.567,.0764) == "123.567(76)"
+    assert value_str(1235.67,.764) == "1235.67(76)"
+    assert value_str(12356.7,7.64) == "12356.7(76)"
+    assert value_str(123567,76.4) == "123567(76)"
+    assert value_str(1235670,764) == "1.23567(76)e6"
+    assert value_str(12356700,764) == "12.35670(76)e6"
+    assert value_str(123567000,764) == "123.56700(76)e6"
+    assert value_str(123567000,7640) == "123.5670(76)e6"
+    assert value_str(1235670000,76400) == "1.235670(76)e9"
+
+    # val_place == err_place
+    assert value_str(123567,764000) == "0.12(76)e6"
+    assert value_str(12356.7,76400) == "12(76)e3"
+    assert value_str(1235.67,7640) == "1.2(76)e3"
+    assert value_str(123.567,764) == "0.12(76)e3"
+    assert value_str(12.3567,76.4) == "12(76)"
+    assert value_str(1.23567,7.64) == "1.2(76)"
+    assert value_str(.123567,.764) == "0.12(76)"
+    assert value_str(.0123567,.0764) == "12(76)e-3"
+    assert value_str(.00123567,.00764) == "1.2(76)e-3"
+    assert value_str(.000123567,.000764) == "0.12(76)e-3"
+
+    # val_place == err_place-1
+    assert value_str(123567,7640000) == "0.1(76)e6"
+    assert value_str(12356.7,764000) == "0.01(76)e6"
+    assert value_str(1235.67,76400) == "0.001(76)e6"
+    assert value_str(123.567,7640) == "0.1(76)e3"
+    assert value_str(12.3567,764) == "0.01(76)e3"
+    assert value_str(1.23567,76.4) == "0.001(76)e3"
+    assert value_str(.123567,7.64) == "0.1(76)"
+    assert value_str(.0123567,.764) == "0.01(76)"
+    assert value_str(.00123567,.0764) == "0.001(76)"
+    assert value_str(.000123567,.00764) == "0.1(76)e-3"
+
+    # val_place == err_place-2
+    assert value_str(12356700,7640000000) == "0.0(76)e9"
+    assert value_str(1235670,764000000) == "0.00(76)e9"
+    assert value_str(123567,76400000) == "0.000(76)e9"
+    assert value_str(12356,7640000) == "0.0(76)e6"
+    assert value_str(1235,764000) == "0.00(76)e6"
+    assert value_str(123,76400) == "0.000(76)e6"
+    assert value_str(12,7640) == "0.0(76)e3"
+    assert value_str(1,764) == "0.00(76)e3"
+    assert value_str(0.1,76.4) == "0.000(76)e3"
+    assert value_str(0.01,7.64) == "0.0(76)"
+    assert value_str(0.001,0.764) == "0.00(76)"
+    assert value_str(0.0001,0.0764) == "0.000(76)"
+    assert value_str(0.00001,0.00764) == "0.0(76)e-3"
+
+    # val_place == err_place-3
+    assert value_str(12356700,76400000000) == "0.000(76)e12"
+    assert value_str(1235670,7640000000) == "0.0(76)e9"
+    assert value_str(123567,764000000) == "0.00(76)e9"
+    assert value_str(12356,76400000) == "0.000(76)e9"
+    assert value_str(1235,7640000) == "0.0(76)e6"
+    assert value_str(123,764000) == "0.00(76)e6"
+    assert value_str(12,76400) == "0.000(76)e6"
+    assert value_str(1,7640) == "0.0(76)e3"
+    assert value_str(0.1,764) == "0.00(76)e3"
+    assert value_str(0.01,76.4) == "0.000(76)e3"
+    assert value_str(0.001,7.64) == "0.0(76)"
+    assert value_str(0.0001,0.764) == "0.00(76)"
+    assert value_str(0.00001,0.0764) == "0.000(76)"
+    assert value_str(0.000001,0.00764) == "0.0(76)e-3"
+
+    # Zero values
+    assert value_str(0,7640000) == "0.0(76)e6"
+    assert value_str(0, 764000) == "0.00(76)e6"
+    assert value_str(0,  76400) == "0.000(76)e6"
+    assert value_str(0,   7640) == "0.0(76)e3"
+    assert value_str(0,    764) == "0.00(76)e3"
+    assert value_str(0,     76.4) == "0.000(76)e3"
+    assert value_str(0,      7.64) == "0.0(76)"
+    assert value_str(0,      0.764) == "0.00(76)"
+    assert value_str(0,      0.0764) == "0.000(76)"
+    assert value_str(0,      0.00764) == "0.0(76)e-3"
+    assert value_str(0,      0.000764) == "0.00(76)e-3"
+    assert value_str(0,      0.0000764) == "0.000(76)e-3"
+
+    # negative values
+    assert value_str(-1235670,765000) == "-1.24(77)e6"
+    assert value_str(-1.23567,.766) == "-1.24(77)"
+    assert value_str(-.00000123567,.0000000766) == "-1.236(77)e-6"
+    assert value_str(-12356.7,7.64) == "-12356.7(76)"
+    assert value_str(-123.567,764) == "-0.12(76)e3"
+    assert value_str(-1235.67,76400) == "-0.001(76)e6"
+    assert value_str(-.000123567,.00764) == "-0.1(76)e-3"
+    assert value_str(-12356,7640000) == "-0.0(76)e6"
+    assert value_str(-12,76400) == "-0.000(76)e6"
+    assert value_str(-0.0001,0.764) == "-0.00(76)"
+
+    # non-finite values
+    assert value_str(-numpy.inf,None) == "-inf"
+    assert value_str(numpy.inf,None) == "inf"
+    assert value_str(numpy.NaN,None) == "NaN"
+    
+    # bad or missing uncertainty
+    assert value_str(-1.23567,numpy.NaN) == "-1.23567"
+    assert value_str(-1.23567,-numpy.inf) == "-1.23567"
+    assert value_str(-1.23567,-0.1) == "-1.23567"
+    assert value_str(-1.23567,0) == "-1.23567"
+    assert value_str(-1.23567,None) == "-1.23567"
+    assert value_str(-1.23567,numpy.inf) == "-1.2(inf)"
+
+def test_pm():
+    # Oops... renamed function after writing tests
+    value_str = format_uncertainty_pm
+
+    # val_place > err_place
+    assert value_str(1235670,766000) == "1.24e6 +/- 0.77e6"
+    assert value_str(123567., 76600) == "124e3 +/- 77e3"
+    assert value_str(12356.7,  7660) == "12.4e3 +/- 7.7e3"
+    assert value_str(1235.67,   766) == "1.24e3 +/- 0.77e3"
+    assert value_str(123.567,    76.6) == "124 +/- 77"
+    assert value_str(12.3567,     7.66) == "12.4 +/- 7.7"
+    assert value_str(1.23567,      .766) == "1.24 +/- 0.77"
+    assert value_str(.123567,      .0766) == "0.124 +/- 0.077"
+    assert value_str(.0123567,     .00766) == "0.0124 +/- 0.0077"
+    assert value_str(.00123567,    .000766) == "0.00124 +/- 0.00077"
+    assert value_str(.000123567,   .0000766) == "124e-6 +/- 77e-6"
+    assert value_str(.0000123567,  .00000766) == "12.4e-6 +/- 7.7e-6"
+    assert value_str(.00000123567, .000000766) == "1.24e-6 +/- 0.77e-6"
+    assert value_str(.000000123567,.0000000766) == "124e-9 +/- 77e-9"
+    assert value_str(.00000123567, .0000000766) == "1.236e-6 +/- 0.077e-6"
+    assert value_str(.0000123567,  .0000000766) == "12.357e-6 +/- 0.077e-6"
+    assert value_str(.000123567,   .0000000766) == "123.567e-6 +/- 0.077e-6"
+    assert value_str(.00123567,    .000000766) == "0.00123567 +/- 0.00000077"
+    assert value_str(.0123567,     .00000766) == "0.0123567 +/- 0.0000077"
+    assert value_str(.123567,      .0000766) == "0.123567 +/- 0.000077"
+    assert value_str(1.23567,      .000766) == "1.23567 +/- 0.00077"
+    assert value_str(12.3567,      .00766) == "12.3567 +/- 0.0077"
+    assert value_str(123.567,      .0764) == "123.567 +/- 0.076"
+    assert value_str(1235.67,      .764) == "1235.67 +/- 0.76"
+    assert value_str(12356.7,     7.64) == "12356.7 +/- 7.6"
+    assert value_str(123567,     76.4) == "123567 +/- 76"
+    assert value_str(1235670,   764) == "1.23567e6 +/- 0.00076e6"
+    assert value_str(12356700,  764) == "12.35670e6 +/- 0.00076e6"
+    assert value_str(123567000, 764) == "123.56700e6 +/- 0.00076e6"
+    assert value_str(123567000,7640) == "123.5670e6 +/- 0.0076e6"
+    assert value_str(1235670000,76400) == "1.235670e9 +/- 0.000076e9"
+
+    # val_place == err_place
+    assert value_str(123567,764000) == "0.12e6 +/- 0.76e6"
+    assert value_str(12356.7,76400) == "12e3 +/- 76e3"
+    assert value_str(1235.67,7640) == "1.2e3 +/- 7.6e3"
+    assert value_str(123.567,764) == "0.12e3 +/- 0.76e3"
+    assert value_str(12.3567,76.4) == "12 +/- 76"
+    assert value_str(1.23567,7.64) == "1.2 +/- 7.6"
+    assert value_str(.123567,.764) == "0.12 +/- 0.76"
+    assert value_str(.0123567,.0764) == "12e-3 +/- 76e-3"
+    assert value_str(.00123567,.00764) == "1.2e-3 +/- 7.6e-3"
+    assert value_str(.000123567,.000764) == "0.12e-3 +/- 0.76e-3"
+
+    # val_place == err_place-1
+    assert value_str(123567,7640000) == "0.1e6 +/- 7.6e6"
+    assert value_str(12356.7,764000) == "0.01e6 +/- 0.76e6"
+    assert value_str(1235.67,76400) == "0.001e6 +/- 0.076e6"
+    assert value_str(123.567,7640) == "0.1e3 +/- 7.6e3"
+    assert value_str(12.3567,764) == "0.01e3 +/- 0.76e3"
+    assert value_str(1.23567,76.4) == "0.001e3 +/- 0.076e3"
+    assert value_str(.123567,7.64) == "0.1 +/- 7.6"
+    assert value_str(.0123567,.764) == "0.01 +/- 0.76"
+    assert value_str(.00123567,.0764) == "0.001 +/- 0.076"
+    assert value_str(.000123567,.00764) == "0.1e-3 +/- 7.6e-3"
+
+    # val_place == err_place-2
+    assert value_str(12356700,7640000000) == "0.0e9 +/- 7.6e9"
+    assert value_str(1235670,764000000) == "0.00e9 +/- 0.76e9"
+    assert value_str(123567,76400000) == "0.000e9 +/- 0.076e9"
+    assert value_str(12356,7640000) == "0.0e6 +/- 7.6e6"
+    assert value_str(1235,764000) == "0.00e6 +/- 0.76e6"
+    assert value_str(123,76400) == "0.000e6 +/- 0.076e6"
+    assert value_str(12,7640) == "0.0e3 +/- 7.6e3"
+    assert value_str(1,764) == "0.00e3 +/- 0.76e3"
+    assert value_str(0.1,76.4) == "0.000e3 +/- 0.076e3"
+    assert value_str(0.01,7.64) == "0.0 +/- 7.6"
+    assert value_str(0.001,0.764) == "0.00 +/- 0.76"
+    assert value_str(0.0001,0.0764) == "0.000 +/- 0.076"
+    assert value_str(0.00001,0.00764) == "0.0e-3 +/- 7.6e-3"
+
+    # val_place == err_place-3
+    assert value_str(12356700,76400000000) == "0.000e12 +/- 0.076e12"
+    assert value_str(1235670,7640000000) == "0.0e9 +/- 7.6e9"
+    assert value_str(123567,764000000) == "0.00e9 +/- 0.76e9"
+    assert value_str(12356,76400000) == "0.000e9 +/- 0.076e9"
+    assert value_str(1235,7640000) == "0.0e6 +/- 7.6e6"
+    assert value_str(123,764000) == "0.00e6 +/- 0.76e6"
+    assert value_str(12,76400) == "0.000e6 +/- 0.076e6"
+    assert value_str(1,7640) == "0.0e3 +/- 7.6e3"
+    assert value_str(0.1,764) == "0.00e3 +/- 0.76e3"
+    assert value_str(0.01,76.4) == "0.000e3 +/- 0.076e3"
+    assert value_str(0.001,7.64) == "0.0 +/- 7.6"
+    assert value_str(0.0001,0.764) == "0.00 +/- 0.76"
+    assert value_str(0.00001,0.0764) == "0.000 +/- 0.076"
+    assert value_str(0.000001,0.00764) == "0.0e-3 +/- 7.6e-3"
+
+    # Zero values
+    assert value_str(0,7640000) == "0.0e6 +/- 7.6e6"
+    assert value_str(0, 764000) == "0.00e6 +/- 0.76e6"
+    assert value_str(0,  76400) == "0.000e6 +/- 0.076e6"
+    assert value_str(0,   7640) == "0.0e3 +/- 7.6e3"
+    assert value_str(0,    764) == "0.00e3 +/- 0.76e3"
+    assert value_str(0,     76.4) == "0.000e3 +/- 0.076e3"
+    assert value_str(0,      7.64) == "0.0 +/- 7.6"
+    assert value_str(0,      0.764) == "0.00 +/- 0.76"
+    assert value_str(0,      0.0764) == "0.000 +/- 0.076"
+    assert value_str(0,      0.00764) == "0.0e-3 +/- 7.6e-3"
+    assert value_str(0,      0.000764) == "0.00e-3 +/- 0.76e-3"
+    assert value_str(0,      0.0000764) == "0.000e-3 +/- 0.076e-3"
+
+    # negative values
+    assert value_str(-1235670,766000) == "-1.24e6 +/- 0.77e6"
+    assert value_str(-1.23567,.766) == "-1.24 +/- 0.77"
+    assert value_str(-.00000123567,.0000000766) == "-1.236e-6 +/- 0.077e-6"
+    assert value_str(-12356.7,7.64) == "-12356.7 +/- 7.6"
+    assert value_str(-123.567,764) == "-0.12e3 +/- 0.76e3"
+    assert value_str(-1235.67,76400) == "-0.001e6 +/- 0.076e6"
+    assert value_str(-.000123567,.00764) == "-0.1e-3 +/- 7.6e-3"
+    assert value_str(-12356,7640000) == "-0.0e6 +/- 7.6e6"
+    assert value_str(-12,76400) == "-0.000e6 +/- 0.076e6"
+    assert value_str(-0.0001,0.764) == "-0.00 +/- 0.76"
+
+    # non-finite values
+    assert value_str(-numpy.inf,None) == "-inf"
+    assert value_str(numpy.inf,None) == "inf"
+    assert value_str(numpy.NaN,None) == "NaN"
+    
+    # bad or missing uncertainty
+    assert value_str(-1.23567,numpy.NaN) == "-1.23567"
+    assert value_str(-1.23567,-numpy.inf) == "-1.23567"
+    assert value_str(-1.23567,-0.1) == "-1.23567"
+    assert value_str(-1.23567,0) == "-1.23567"
+    assert value_str(-1.23567,None) == "-1.23567"
+    assert value_str(-1.23567,numpy.inf) == "-1.2 +/- inf"
+
+def test_default():
+    # Check that the default is the compact format
+    assert format_uncertainty(-1.23567,0.766) == "-1.24(77)"
+
+def main():
+    """
+    Run all tests.
+
+    This is equivalent to "nosetests --with-doctest"
+    """
+    test_compact()
+    test_pm()
+    test_default()
+    
+    import doctest
+    doctest.testmod()
+
+if __name__ == "__main__": main()
Index: sansutil/nxsunit.py
===================================================================
--- sansutil/nxsunit.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/nxsunit.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,209 @@
+# This program is public domain
+# Author: Paul Kienzle
+"""
+Define unit conversion support for NeXus style units.
+
+The unit format is somewhat complicated.  There are variant spellings
+and incorrect capitalization to worry about, as well as forms such as
+"mili*metre" and "1e-7 seconds".
+
+This is a minimal implementation of units including only what I happen to
+need now.  It does not support the complete dimensional analysis provided
+by the package udunits on which NeXus is based, or even the units used
+in the NeXus definition files.
+
+Unlike other units packages, this package does not carry the units along with 
+the value but merely provides a conversion function for transforming values.
+
+Usage example::
+
+    import nxsunit
+    u = nxsunit.Converter('mili*metre')  # Units stored in mm
+    v = u(3000,'m')  # Convert the value 3000 mm into meters
+
+NeXus example::
+
+    # Load sample orientation in radians regardless of how it is stored.
+    # 1. Open the path
+    file.openpath('/entry1/sample/sample_orientation')
+    # 2. scan the attributes, retrieving 'units'
+    units = [for attr,value in file.attrs() if attr == 'units']
+    # 3. set up the converter (assumes that units actually exists)
+    u = nxsunit.Converter(units[0])
+    # 4. read the data and convert to the correct units
+    v = u(file.read(),'radians')
+
+This is a standalone module, not relying on either DANSE or NeXus, and
+can be used for other unit conversion tasks.
+
+Note: minutes are used for angle and seconds are used for time.  We
+cannot tell what the correct interpretation is without knowing something
+about the fields themselves.  If this becomes an issue, we will need to
+allow the application to set the dimension for the unit rather than
+inferring the dimension from an example unit.
+"""
+
+# TODO: Add udunits to NAPI rather than reimplementing it in python
+# TODO: Alternatively, parse the udunits database directly
+# UDUnits:
+#  http://www.unidata.ucar.edu/software/udunits/udunits-1/udunits.txt
+
+from __future__ import division
+import math
+
+__all__ = ['Converter']
+
+# Limited form of units for returning objects of a specific type.
+# Maybe want to do full units handling with e.g., pyre's
+# unit class. For now lets keep it simple.  Note that
+def _build_metric_units(unit,abbr):
+    """
+    Construct standard SI names for the given unit.
+    Builds e.g.,
+        s, ns
+        second, nanosecond, nano*second
+        seconds, nanoseconds
+    Includes prefixes for femto through peta.
+
+    Ack! Allows, e.g., Coulomb and coulomb even though Coulomb is not
+    a unit because some NeXus files store it that way!
+    
+    Returns a dictionary of names and scales.
+    """
+    prefix = dict(peta=1e15,tera=1e12,giga=1e9,mega=1e6,kilo=1e3,
+                  deci=1e-1,centi=1e-2,milli=1e-3,mili=1e-3,micro=1e-6,
+                  nano=1e-9,pico=1e-12,femto=1e-15)
+    short_prefix = dict(P=1e15,T=1e12,G=1e9,M=1e6,k=1e3,
+                        d=1e-1,c=1e-2,m=1e-3,u=1e-6,
+                        n=1e-9,p=1e-12,f=1e-15)
+    map = {abbr:1}
+    map.update([(P+abbr,scale) for (P,scale) in short_prefix.iteritems()])
+    for name in [unit,unit.capitalize()]:
+        map.update({name:1,name+'s':1})
+        map.update([(P+name,scale) for (P,scale) in prefix.iteritems()])
+        map.update([(P+'*'+name,scale) for (P,scale) in prefix.iteritems()])
+        map.update([(P+name+'s',scale) for (P,scale) in prefix.iteritems()])
+    return map
+
+def _build_plural_units(**kw):
+    """
+    Construct names for the given units.  Builds singular and plural form.
+    """
+    map = {}
+    map.update([(name,scale) for name,scale in kw.iteritems()])
+    map.update([(name+'s',scale) for name,scale in kw.iteritems()])
+    return map
+
+def _caret_optional(s):
+    """
+    Strip '^' from unit names.
+
+    * WARNING * this will incorrect transform 10^3 to 103.
+    """
+    s.update((k.replace('^',''),v) 
+             for k,v in s.items()
+             if '^' in k)
+
+def _build_all_units():
+    distance = _build_metric_units('meter','m')
+    distance.update(_build_metric_units('metre','m'))
+    distance.update(_build_plural_units(micron=1e-6, Angstrom=1e-10))
+    distance.update({'A':1e-10, 'Ang':1e-10})
+
+    # Note: minutes are used for angle
+    time = _build_metric_units('second','s')
+    time.update(_build_plural_units(hour=3600,day=24*3600,week=7*24*3600))
+
+    # Note: seconds are used for time
+    angle = _build_plural_units(degree=1, minute=1/60.,
+                  arcminute=1/60., arcsecond=1/3600., radian=180/math.pi)
+    angle.update(deg=1, arcmin=1/60., arcsec=1/3600., rad=180/math.pi)
+
+    frequency = _build_metric_units('hertz','Hz')
+    frequency.update(_build_metric_units('Hertz','Hz'))
+    frequency.update(_build_plural_units(rpm=1/60.))
+
+    # Note: degrees are used for angle
+    # Note: temperature needs an offset as well as a scale
+    temperature = _build_metric_units('kelvin','K')
+    temperature.update(_build_metric_units('Kelvin','K'))
+
+    charge = _build_metric_units('coulomb','C')
+    charge.update({'microAmp*hour':0.0036})
+
+    sld = { '10^-6 Angstrom^-2': 1e-6, 'Angstrom^-2': 1 }
+    Q = { 'invA': 1, 'invAng': 1, 'invAngstroms': 1, '1/A': 1, 
+          '10^-3 Angstrom^-1': 1e-3, '1/cm': 1e-8,
+          'nm^-1': 0.1, '1/nm': 0.1, 'n_m^-1': 0.1 }
+
+    _caret_optional(sld)
+    _caret_optional(Q)
+
+    dims = [distance, time, angle, frequency, temperature, charge, sld, Q]
+    return dims
+
+class Converter(object):
+    """
+    Unit converter for NeXus style units.
+
+    """
+    # Define the units, using both American and European spelling.
+    scalemap = None
+    scalebase = 1
+    dims = _build_all_units()
+
+    # Note: a.u. stands for arbitrary units, which should return the default
+    # units for that particular dimension.
+    # Note: don't have support for dimensionless units.
+    unknown = {None:1, '???':1, '': 1, 'a.u.': 1}
+
+    def __init__(self, name):
+        self.base = name
+        for map in self.dims:
+            if name in map:
+                self.scalemap = map
+                self.scalebase = self.scalemap[name]
+                return
+        if name in self.unknown:
+            return # default scalemap and scalebase correspond to unknown
+        else:
+            raise KeyError("Unknown unit %s"%name)
+
+    def scale(self, units=""):
+        if units == "" or self.scalemap is None: return 1
+        return self.scalebase/self.scalemap[units]
+
+    def __call__(self, value, units=""):
+        # Note: calculating a*1 rather than simply returning a would produce
+        # an unnecessary copy of the array, which in the case of the raw
+        # counts array would be bad.  Sometimes copying and other times
+        # not copying is also bad, but copy on modify semantics isn't
+        # supported.
+        if units == "" or self.scalemap is None: return value
+        try:
+            return value * (self.scalebase/self.scalemap[units])
+        except KeyError:
+            possible_units = ", ".join(str(k) for k in self.scalemap.keys())
+            raise KeyError("%s not in %s"%(units,possible_units))
+
+def _check(expect,get):
+    if expect != get: raise ValueError, "Expected %s but got %s"%(expect,get)
+     #print expect,"==",get
+
+def test():
+    _check(1,Converter('n_m^-1')(10,'invA')) # 10 nm^-1 = 1 inv Angstroms
+    _check(2,Converter('mm')(2000,'m')) # 2000 mm -> 2 m
+    _check(0.003,Converter('microseconds')(3,units='ms')) # 3 us -> 0.003 ms
+    _check(45,Converter('nanokelvin')(45))  # 45 nK -> 45 nK
+    _check(0.5,Converter('seconds')(1800,units='hours')) # 1800 s -> 0.5 hr
+    _check(123,Converter('a.u.')(123,units='mm')) # arbitrary units always returns the same value
+    _check(123,Converter('a.u.')(123,units='s')) # arbitrary units always returns the same value
+    _check(123,Converter('a.u.')(123,units='')) # arbitrary units always returns the same value
+    try: Converter('help')
+    except KeyError: pass
+    else: raise Exception("unknown unit did not raise an error")
+
+    # TODO: more tests
+
+if __name__ == "__main__":
+    test()
Index: sansutil/odict.py
===================================================================
--- sansutil/odict.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/odict.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,1399 @@
+# odict.py
+# An Ordered Dictionary object
+# Copyright (C) 2005 Nicola Larosa, Michael Foord
+# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk
+
+# This software is licensed under the terms of the BSD license.
+# http://www.voidspace.org.uk/python/license.shtml
+# Basically you're free to copy, modify, distribute and relicense it,
+# So long as you keep a copy of the license with it.
+
+# Documentation at http://www.voidspace.org.uk/python/odict.html
+# For information about bugfixes, updates and support, please join the
+# Pythonutils mailing list:
+# http://groups.google.com/group/pythonutils/
+# Comments, suggestions and bug reports welcome.
+
+"""A dict that keeps keys in insertion order"""
+from __future__ import generators
+
+__author__ = ('Nicola Larosa <nico-NoSp@m-tekNico.net>,'
+    'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>')
+
+__docformat__ = "restructuredtext en"
+
+__revision__ = '$Id: odict.py 58 2008-09-02 14:09:54Z farrowch $'
+
+__version__ = '0.2.2'
+
+__all__ = ['OrderedDict', 'SequenceOrderedDict']
+
+import sys
+INTP_VER = sys.version_info[:2]
+if INTP_VER < (2, 2):
+    raise RuntimeError("Python v.2.2 or later required")
+
+import types, warnings
+
+class OrderedDict(dict):
+    """
+    A class of dictionary that keeps the insertion order of keys.
+    
+    All appropriate methods return keys, items, or values in an ordered way.
+    
+    All normal dictionary methods are available. Update and comparison is
+    restricted to other OrderedDict objects.
+    
+    Various sequence methods are available, including the ability to explicitly
+    mutate the key ordering.
+    
+    __contains__ tests:
+    
+    >>> d = OrderedDict(((1, 3),))
+    >>> 1 in d
+    1
+    >>> 4 in d
+    0
+    
+    __getitem__ tests:
+    
+    >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2]
+    1
+    >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4]
+    Traceback (most recent call last):
+    KeyError: 4
+    
+    __len__ tests:
+    
+    >>> len(OrderedDict())
+    0
+    >>> len(OrderedDict(((1, 3), (3, 2), (2, 1))))
+    3
+    
+    get tests:
+    
+    >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+    >>> d.get(1)
+    3
+    >>> d.get(4) is None
+    1
+    >>> d.get(4, 5)
+    5
+    >>> d
+    OrderedDict([(1, 3), (3, 2), (2, 1)])
+    
+    has_key tests:
+    
+    >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+    >>> d.has_key(1)
+    1
+    >>> d.has_key(4)
+    0
+    """
+
+    def __init__(self, init_val=(), strict=False):
+        """
+        Create a new ordered dictionary. Cannot init from a normal dict,
+        nor from kwargs, since items order is undefined in those cases.
+        
+        If the ``strict`` keyword argument is ``True`` (``False`` is the
+        default) then when doing slice assignment - the ``OrderedDict`` you are
+        assigning from *must not* contain any keys in the remaining dict.
+        
+        >>> OrderedDict()
+        OrderedDict([])
+        >>> OrderedDict({1: 1})
+        Traceback (most recent call last):
+        TypeError: undefined order, cannot get items from dict
+        >>> OrderedDict({1: 1}.items())
+        OrderedDict([(1, 1)])
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d
+        OrderedDict([(1, 3), (3, 2), (2, 1)])
+        >>> OrderedDict(d)
+        OrderedDict([(1, 3), (3, 2), (2, 1)])
+        """
+        self.strict = strict
+        dict.__init__(self)
+        if isinstance(init_val, OrderedDict):
+            self._sequence = init_val.keys()
+            dict.update(self, init_val)
+        elif isinstance(init_val, dict):
+            # we lose compatibility with other ordered dict types this way
+            raise TypeError('undefined order, cannot get items from dict')
+        else:
+            self._sequence = []
+            self.update(init_val)
+
+### Special methods ###
+
+    def __delitem__(self, key):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> del d[3]
+        >>> d
+        OrderedDict([(1, 3), (2, 1)])
+        >>> del d[3]
+        Traceback (most recent call last):
+        KeyError: 3
+        >>> d[3] = 2
+        >>> d
+        OrderedDict([(1, 3), (2, 1), (3, 2)])
+        >>> del d[0:1]
+        >>> d
+        OrderedDict([(2, 1), (3, 2)])
+        """
+        if isinstance(key, types.SliceType):
+            # FIXME: efficiency?
+            keys = self._sequence[key]
+            for entry in keys:
+                dict.__delitem__(self, entry)
+            del self._sequence[key]
+        else:
+            # do the dict.__delitem__ *first* as it raises
+            # the more appropriate error
+            dict.__delitem__(self, key)
+            self._sequence.remove(key)
+
+    def __eq__(self, other):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d == OrderedDict(d)
+        True
+        >>> d == OrderedDict(((1, 3), (2, 1), (3, 2)))
+        False
+        >>> d == OrderedDict(((1, 0), (3, 2), (2, 1)))
+        False
+        >>> d == OrderedDict(((0, 3), (3, 2), (2, 1)))
+        False
+        >>> d == dict(d)
+        False
+        >>> d == False
+        False
+        """
+        if isinstance(other, OrderedDict):
+            # FIXME: efficiency?
+            #   Generate both item lists for each compare
+            return (self.items() == other.items())
+        else:
+            return False
+
+    def __lt__(self, other):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
+        >>> c < d
+        True
+        >>> d < c
+        False
+        >>> d < dict(c)
+        Traceback (most recent call last):
+        TypeError: Can only compare with other OrderedDicts
+        """
+        if not isinstance(other, OrderedDict):
+            raise TypeError('Can only compare with other OrderedDicts')
+        # FIXME: efficiency?
+        #   Generate both item lists for each compare
+        return (self.items() < other.items())
+
+    def __le__(self, other):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
+        >>> e = OrderedDict(d)
+        >>> c <= d
+        True
+        >>> d <= c
+        False
+        >>> d <= dict(c)
+        Traceback (most recent call last):
+        TypeError: Can only compare with other OrderedDicts
+        >>> d <= e
+        True
+        """
+        if not isinstance(other, OrderedDict):
+            raise TypeError('Can only compare with other OrderedDicts')
+        # FIXME: efficiency?
+        #   Generate both item lists for each compare
+        return (self.items() <= other.items())
+
+    def __ne__(self, other):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d != OrderedDict(d)
+        False
+        >>> d != OrderedDict(((1, 3), (2, 1), (3, 2)))
+        True
+        >>> d != OrderedDict(((1, 0), (3, 2), (2, 1)))
+        True
+        >>> d == OrderedDict(((0, 3), (3, 2), (2, 1)))
+        False
+        >>> d != dict(d)
+        True
+        >>> d != False
+        True
+        """
+        if isinstance(other, OrderedDict):
+            # FIXME: efficiency?
+            #   Generate both item lists for each compare
+            return not (self.items() == other.items())
+        else:
+            return True
+
+    def __gt__(self, other):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
+        >>> d > c
+        True
+        >>> c > d
+        False
+        >>> d > dict(c)
+        Traceback (most recent call last):
+        TypeError: Can only compare with other OrderedDicts
+        """
+        if not isinstance(other, OrderedDict):
+            raise TypeError('Can only compare with other OrderedDicts')
+        # FIXME: efficiency?
+        #   Generate both item lists for each compare
+        return (self.items() > other.items())
+
+    def __ge__(self, other):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> c = OrderedDict(((0, 3), (3, 2), (2, 1)))
+        >>> e = OrderedDict(d)
+        >>> c >= d
+        False
+        >>> d >= c
+        True
+        >>> d >= dict(c)
+        Traceback (most recent call last):
+        TypeError: Can only compare with other OrderedDicts
+        >>> e >= d
+        True
+        """
+        if not isinstance(other, OrderedDict):
+            raise TypeError('Can only compare with other OrderedDicts')
+        # FIXME: efficiency?
+        #   Generate both item lists for each compare
+        return (self.items() >= other.items())
+
+    def __repr__(self):
+        """
+        Used for __repr__ and __str__
+        
+        >>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f'))))
+        >>> r1
+        "OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])"
+        >>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd'))))
+        >>> r2
+        "OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])"
+        >>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f'))))
+        True
+        >>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd'))))
+        True
+        """
+        return '%s([%s])' % (self.__class__.__name__, ', '.join(
+            ['(%r, %r)' % (key, self[key]) for key in self._sequence]))
+
+    def __setitem__(self, key, val):
+        """
+        Allows slice assignment, so long as the slice is an OrderedDict
+        >>> d = OrderedDict()
+        >>> d['a'] = 'b'
+        >>> d['b'] = 'a'
+        >>> d[3] = 12
+        >>> d
+        OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)])
+        >>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4)))
+        >>> d
+        OrderedDict([(1, 2), (2, 3), (3, 4)])
+        >>> d[::2] = OrderedDict(((7, 8), (9, 10)))
+        >>> d
+        OrderedDict([(7, 8), (2, 3), (9, 10)])
+        >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)))
+        >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8)))
+        >>> d
+        OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)])
+        >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True)
+        >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8)))
+        >>> d
+        OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)])
+        
+        >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True)
+        >>> a[3] = 4
+        >>> a
+        OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a
+        OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)])
+        Traceback (most recent call last):
+        ValueError: slice assignment must be from unique keys
+        >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)))
+        >>> a[3] = 4
+        >>> a
+        OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a
+        OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a
+        OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> a
+        OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)])
+        
+        >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> d[:1] = 3
+        Traceback (most recent call last):
+        TypeError: slice assignment requires an OrderedDict
+        
+        >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)])
+        >>> d[:1] = OrderedDict([(9, 8)])
+        >>> d
+        OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)])
+        """
+        if isinstance(key, types.SliceType):
+            if not isinstance(val, OrderedDict):
+                # FIXME: allow a list of tuples?
+                raise TypeError('slice assignment requires an OrderedDict')
+            keys = self._sequence[key]
+            # NOTE: Could use ``range(*key.indices(len(self._sequence)))``
+            indexes = range(len(self._sequence))[key]
+            if key.step is None:
+                # NOTE: new slice may not be the same size as the one being
+                #   overwritten !
+                # NOTE: What is the algorithm for an impossible slice?
+                #   e.g. d[5:3]
+                pos = key.start or 0
+                del self[key]
+                newkeys = val.keys()
+                for k in newkeys:
+                    if k in self:
+                        if self.strict:
+                            raise ValueError('slice assignment must be from '
+                                'unique keys')
+                        else:
+                            # NOTE: This removes duplicate keys *first*
+                            #   so start position might have changed?
+                            del self[k]
+                self._sequence = (self._sequence[:pos] + newkeys +
+                    self._sequence[pos:])
+                dict.update(self, val)
+            else:
+                # extended slice - length of new slice must be the same
+                # as the one being replaced
+                if len(keys) != len(val):
+                    raise ValueError('attempt to assign sequence of size %s '
+                        'to extended slice of size %s' % (len(val), len(keys)))
+                # FIXME: efficiency?
+                del self[key]
+                item_list = zip(indexes, val.items())
+                # smallest indexes first - higher indexes not guaranteed to
+                # exist
+                item_list.sort()
+                for pos, (newkey, newval) in item_list:
+                    if self.strict and newkey in self:
+                        raise ValueError('slice assignment must be from unique'
+                            ' keys')
+                    self.insert(pos, newkey, newval)
+        else:
+            if key not in self:
+                self._sequence.append(key)
+            dict.__setitem__(self, key, val)
+
+    def __getitem__(self, key):
+        """
+        Allows slicing. Returns an OrderedDict if you slice.
+        >>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)])
+        >>> b[::-1]
+        OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)])
+        >>> b[2:5]
+        OrderedDict([(5, 2), (4, 3), (3, 4)])
+        >>> type(b[2:4])
+        <class '__main__.OrderedDict'>
+        """
+        if isinstance(key, types.SliceType):
+            # FIXME: does this raise the error we want?
+            keys = self._sequence[key]
+            # FIXME: efficiency?
+            return OrderedDict([(entry, self[entry]) for entry in keys])
+        else:
+            return dict.__getitem__(self, key)
+
+    __str__ = __repr__
+
+    def __setattr__(self, name, value):
+        """
+        Implemented so that accesses to ``sequence`` raise a warning and are
+        diverted to the new ``setkeys`` method.
+        """
+        if name == 'sequence':
+            warnings.warn('Use of the sequence attribute is deprecated.'
+                ' Use the keys method instead.', DeprecationWarning)
+            # NOTE: doesn't return anything
+            self.setkeys(value)
+        else:
+            # FIXME: do we want to allow arbitrary setting of attributes?
+            #   Or do we want to manage it?
+            object.__setattr__(self, name, value)
+
+    def __getattr__(self, name):
+        """
+        Implemented so that access to ``sequence`` raises a warning.
+        
+        >>> d = OrderedDict()
+        >>> d.sequence
+        []
+        """
+        if name == 'sequence':
+            warnings.warn('Use of the sequence attribute is deprecated.'
+                ' Use the keys method instead.', DeprecationWarning)
+            # NOTE: Still (currently) returns a direct reference. Need to
+            #   because code that uses sequence will expect to be able to
+            #   mutate it in place.
+            return self._sequence
+        else:
+            # raise the appropriate error
+            raise AttributeError("OrderedDict has no '%s' attribute" % name)
+
+    def __deepcopy__(self, memo):
+        """
+        To allow deepcopy to work with OrderedDict.
+        
+        >>> from copy import deepcopy
+        >>> a = OrderedDict([(1, 1), (2, 2), (3, 3)])
+        >>> a['test'] = {}
+        >>> b = deepcopy(a)
+        >>> b == a
+        True
+        >>> b is a
+        False
+        >>> a['test'] is b['test']
+        False
+        """
+        from copy import deepcopy
+        return self.__class__(deepcopy(self.items(), memo), self.strict)
+
+
+### Read-only methods ###
+
+    def copy(self):
+        """
+        >>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy()
+        OrderedDict([(1, 3), (3, 2), (2, 1)])
+        """
+        return OrderedDict(self)
+
+    def items(self):
+        """
+        ``items`` returns a list of tuples representing all the 
+        ``(key, value)`` pairs in the dictionary.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.items()
+        [(1, 3), (3, 2), (2, 1)]
+        >>> d.clear()
+        >>> d.items()
+        []
+        """
+        return zip(self._sequence, self.values())
+
+    def keys(self):
+        """
+        Return a list of keys in the ``OrderedDict``.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.keys()
+        [1, 3, 2]
+        """
+        return self._sequence[:]
+
+    def values(self, values=None):
+        """
+        Return a list of all the values in the OrderedDict.
+        
+        Optionally you can pass in a list of values, which will replace the
+        current list. The value list must be the same len as the OrderedDict.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.values()
+        [3, 2, 1]
+        """
+        return [self[key] for key in self._sequence]
+
+    def iteritems(self):
+        """
+        >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems()
+        >>> ii.next()
+        (1, 3)
+        >>> ii.next()
+        (3, 2)
+        >>> ii.next()
+        (2, 1)
+        >>> ii.next()
+        Traceback (most recent call last):
+        StopIteration
+        """
+        def make_iter(self=self):
+            keys = self.iterkeys()
+            while True:
+                key = keys.next()
+                yield (key, self[key])
+        return make_iter()
+
+    def iterkeys(self):
+        """
+        >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys()
+        >>> ii.next()
+        1
+        >>> ii.next()
+        3
+        >>> ii.next()
+        2
+        >>> ii.next()
+        Traceback (most recent call last):
+        StopIteration
+        """
+        return iter(self._sequence)
+
+    __iter__ = iterkeys
+
+    def itervalues(self):
+        """
+        >>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues()
+        >>> iv.next()
+        3
+        >>> iv.next()
+        2
+        >>> iv.next()
+        1
+        >>> iv.next()
+        Traceback (most recent call last):
+        StopIteration
+        """
+        def make_iter(self=self):
+            keys = self.iterkeys()
+            while True:
+                yield self[keys.next()]
+        return make_iter()
+
+### Read-write methods ###
+
+    def clear(self):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.clear()
+        >>> d
+        OrderedDict([])
+        """
+        dict.clear(self)
+        self._sequence = []
+
+    def pop(self, key, *args):
+        """
+        No dict.pop in Python 2.2, gotta reimplement it
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.pop(3)
+        2
+        >>> d
+        OrderedDict([(1, 3), (2, 1)])
+        >>> d.pop(4)
+        Traceback (most recent call last):
+        KeyError: 4
+        >>> d.pop(4, 0)
+        0
+        >>> d.pop(4, 0, 1)
+        Traceback (most recent call last):
+        TypeError: pop expected at most 2 arguments, got 3
+        """
+        if len(args) > 1:
+            raise TypeError, ('pop expected at most 2 arguments, got %s' %
+                (len(args) + 1))
+        if key in self:
+            val = self[key]
+            del self[key]
+        else:
+            try:
+                val = args[0]
+            except IndexError:
+                raise KeyError(key)
+        return val
+
+    def popitem(self, i=-1):
+        """
+        Delete and return an item specified by index, not a random one as in
+        dict. The index is -1 by default (the last item).
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.popitem()
+        (2, 1)
+        >>> d
+        OrderedDict([(1, 3), (3, 2)])
+        >>> d.popitem(0)
+        (1, 3)
+        >>> OrderedDict().popitem()
+        Traceback (most recent call last):
+        KeyError: 'popitem(): dictionary is empty'
+        >>> d.popitem(2)
+        Traceback (most recent call last):
+        IndexError: popitem(): index 2 not valid
+        """
+        if not self._sequence:
+            raise KeyError('popitem(): dictionary is empty')
+        try:
+            key = self._sequence[i]
+        except IndexError:
+            raise IndexError('popitem(): index %s not valid' % i)
+        return (key, self.pop(key))
+
+    def setdefault(self, key, defval = None):
+        """
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.setdefault(1)
+        3
+        >>> d.setdefault(4) is None
+        True
+        >>> d
+        OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)])
+        >>> d.setdefault(5, 0)
+        0
+        >>> d
+        OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)])
+        """
+        if key in self:
+            return self[key]
+        else:
+            self[key] = defval
+            return defval
+
+    def update(self, from_od):
+        """
+        Update from another OrderedDict or sequence of (key, value) pairs
+        
+        >>> d = OrderedDict(((1, 0), (0, 1)))
+        >>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1))))
+        >>> d
+        OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)])
+        >>> d.update({4: 4})
+        Traceback (most recent call last):
+        TypeError: undefined order, cannot get items from dict
+        >>> d.update((4, 4))
+        Traceback (most recent call last):
+        TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence
+        """
+        if isinstance(from_od, OrderedDict):
+            for key, val in from_od.items():
+                self[key] = val
+        elif isinstance(from_od, dict):
+            # we lose compatibility with other ordered dict types this way
+            raise TypeError('undefined order, cannot get items from dict')
+        else:
+            # FIXME: efficiency?
+            # sequence of 2-item sequences, or error
+            for item in from_od:
+                try:
+                    key, val = item
+                except TypeError:
+                    raise TypeError('cannot convert dictionary update'
+                        ' sequence element "%s" to a 2-item sequence' % item)
+                self[key] = val
+
+    def rename(self, old_key, new_key):
+        """
+        Rename the key for a given value, without modifying sequence order.
+        
+        For the case where new_key already exists this raise an exception,
+        since if new_key exists, it is ambiguous as to what happens to the
+        associated values, and the position of new_key in the sequence.
+        
+        >>> od = OrderedDict()
+        >>> od['a'] = 1
+        >>> od['b'] = 2
+        >>> od.items()
+        [('a', 1), ('b', 2)]
+        >>> od.rename('b', 'c')
+        >>> od.items()
+        [('a', 1), ('c', 2)]
+        >>> od.rename('c', 'a')
+        Traceback (most recent call last):
+        ValueError: New key already exists: 'a'
+        >>> od.rename('d', 'b')
+        Traceback (most recent call last):
+        KeyError: 'd'
+        """
+        if new_key == old_key:
+            # no-op
+            return
+        if new_key in self:
+            raise ValueError("New key already exists: %r" % new_key)
+        # rename sequence entry
+        value = self[old_key] 
+        old_idx = self._sequence.index(old_key)
+        self._sequence[old_idx] = new_key
+        # rename internal dict entry
+        dict.__delitem__(self, old_key)
+        dict.__setitem__(self, new_key, value)
+
+    def setitems(self, items):
+        """
+        This method allows you to set the items in the dict.
+        
+        It takes a list of tuples - of the same sort returned by the ``items``
+        method.
+        
+        >>> d = OrderedDict()
+        >>> d.setitems(((3, 1), (2, 3), (1, 2)))
+        >>> d
+        OrderedDict([(3, 1), (2, 3), (1, 2)])
+        """
+        self.clear()
+        # FIXME: this allows you to pass in an OrderedDict as well :-)
+        self.update(items)
+
+    def setkeys(self, keys):
+        """
+        ``setkeys`` all ows you to pass in a new list of keys which will
+        replace the current set. This must contain the same set of keys, but
+        need not be in the same order.
+        
+        If you pass in new keys that don't match, a ``KeyError`` will be
+        raised.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.keys()
+        [1, 3, 2]
+        >>> d.setkeys((1, 2, 3))
+        >>> d
+        OrderedDict([(1, 3), (2, 1), (3, 2)])
+        >>> d.setkeys(['a', 'b', 'c'])
+        Traceback (most recent call last):
+        KeyError: 'Keylist is not the same as current keylist.'
+        """
+        # FIXME: Efficiency? (use set for Python 2.4 :-)
+        # NOTE: list(keys) rather than keys[:] because keys[:] returns
+        #   a tuple, if keys is a tuple.
+        kcopy = list(keys)
+        kcopy.sort()
+        self._sequence.sort()
+        if kcopy != self._sequence:
+            raise KeyError('Keylist is not the same as current keylist.')
+        # NOTE: This makes the _sequence attribute a new object, instead
+        #       of changing it in place.
+        # FIXME: efficiency?
+        self._sequence = list(keys)
+
+    def setvalues(self, values):
+        """
+        You can pass in a list of values, which will replace the
+        current list. The value list must be the same len as the OrderedDict.
+        
+        (Or a ``ValueError`` is raised.)
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.setvalues((1, 2, 3))
+        >>> d
+        OrderedDict([(1, 1), (3, 2), (2, 3)])
+        >>> d.setvalues([6])
+        Traceback (most recent call last):
+        ValueError: Value list is not the same length as the OrderedDict.
+        """
+        if len(values) != len(self):
+            # FIXME: correct error to raise?
+            raise ValueError('Value list is not the same length as the '
+                'OrderedDict.')
+        self.update(zip(self, values))
+
+### Sequence Methods ###
+
+    def index(self, key):
+        """
+        Return the position of the specified key in the OrderedDict.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.index(3)
+        1
+        >>> d.index(4)
+        Traceback (most recent call last):
+        ValueError: list.index(x): x not in list
+        """
+        return self._sequence.index(key)
+
+    def insert(self, index, key, value):
+        """
+        Takes ``index``, ``key``, and ``value`` as arguments.
+        
+        Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in
+        the OrderedDict.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.insert(0, 4, 0)
+        >>> d
+        OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)])
+        >>> d.insert(0, 2, 1)
+        >>> d
+        OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)])
+        >>> d.insert(8, 8, 1)
+        >>> d
+        OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)])
+        """
+        if key in self:
+            # FIXME: efficiency?
+            del self[key]
+        self._sequence.insert(index, key)
+        dict.__setitem__(self, key, value)
+
+    def reverse(self):
+        """
+        Reverse the order of the OrderedDict.
+        
+        >>> d = OrderedDict(((1, 3), (3, 2), (2, 1)))
+        >>> d.reverse()
+        >>> d
+        OrderedDict([(2, 1), (3, 2), (1, 3)])
+        """
+        self._sequence.reverse()
+
+    def sort(self, *args, **kwargs):
+        """
+        Sort the key order in the OrderedDict.
+        
+        This method takes the same arguments as the ``list.sort`` method on
+        your version of Python.
+        
+        >>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4)))
+        >>> d.sort()
+        >>> d
+        OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)])
+        """
+        self._sequence.sort(*args, **kwargs)
+
+class Keys(object):
+    # FIXME: should this object be a subclass of list?
+    """
+    Custom object for accessing the keys of an OrderedDict.
+    
+    Can be called like the normal ``OrderedDict.keys`` method, but also
+    supports indexing and sequence methods.
+    """
+
+    def __init__(self, main):
+        self._main = main
+
+    def __call__(self):
+        """Pretend to be the keys method."""
+        return self._main._keys()
+
+    def __getitem__(self, index):
+        """Fetch the key at position i."""
+        # NOTE: this automatically supports slicing :-)
+        return self._main._sequence[index]
+
+    def __setitem__(self, index, name):
+        """
+        You cannot assign to keys, but you can do slice assignment to re-order
+        them.
+        
+        You can only do slice assignment if the new set of keys is a reordering
+        of the original set.
+        """
+        if isinstance(index, types.SliceType):
+            # FIXME: efficiency?
+            # check length is the same
+            indexes = range(len(self._main._sequence))[index]
+            if len(indexes) != len(name):
+                raise ValueError('attempt to assign sequence of size %s '
+                    'to slice of size %s' % (len(name), len(indexes)))
+            # check they are the same keys
+            # FIXME: Use set
+            old_keys = self._main._sequence[index]
+            new_keys = list(name)
+            old_keys.sort()
+            new_keys.sort()
+            if old_keys != new_keys:
+                raise KeyError('Keylist is not the same as current keylist.')
+            orig_vals = [self._main[k] for k in name]
+            del self._main[index]
+            vals = zip(indexes, name, orig_vals)
+            vals.sort()
+            for i, k, v in vals:
+                if self._main.strict and k in self._main:
+                    raise ValueError('slice assignment must be from '
+                        'unique keys')
+                self._main.insert(i, k, v)
+        else:
+            raise ValueError('Cannot assign to keys')
+
+    ### following methods pinched from UserList and adapted ###
+    def __repr__(self): return repr(self._main._sequence)
+
+    # FIXME: do we need to check if we are comparing with another ``Keys``
+    #   object? (like the __cast method of UserList)
+    def __lt__(self, other): return self._main._sequence <  other
+    def __le__(self, other): return self._main._sequence <= other
+    def __eq__(self, other): return self._main._sequence == other
+    def __ne__(self, other): return self._main._sequence != other
+    def __gt__(self, other): return self._main._sequence >  other
+    def __ge__(self, other): return self._main._sequence >= other
+    # FIXME: do we need __cmp__ as well as rich comparisons?
+    def __cmp__(self, other): return cmp(self._main._sequence, other)
+
+    def __contains__(self, item): return item in self._main._sequence
+    def __len__(self): return len(self._main._sequence)
+    def __iter__(self): return self._main.iterkeys()
+    def count(self, item): return self._main._sequence.count(item)
+    def index(self, item, *args): return self._main._sequence.index(item, *args)
+    def reverse(self): self._main._sequence.reverse()
+    def sort(self, *args, **kwds): self._main._sequence.sort(*args, **kwds)
+    def __mul__(self, n): return self._main._sequence*n
+    __rmul__ = __mul__
+    def __add__(self, other): return self._main._sequence + other
+    def __radd__(self, other): return other + self._main._sequence
+
+    ## following methods not implemented for keys ##
+    def __delitem__(self, i): raise TypeError('Can\'t delete items from keys')
+    def __iadd__(self, other): raise TypeError('Can\'t add in place to keys')
+    def __imul__(self, n): raise TypeError('Can\'t multiply keys in place')
+    def append(self, item): raise TypeError('Can\'t append items to keys')
+    def insert(self, i, item): raise TypeError('Can\'t insert items into keys')
+    def pop(self, i=-1): raise TypeError('Can\'t pop items from keys')
+    def remove(self, item): raise TypeError('Can\'t remove items from keys')
+    def extend(self, other): raise TypeError('Can\'t extend keys')
+
+class Items(object):
+    """
+    Custom object for accessing the items of an OrderedDict.
+    
+    Can be called like the normal ``OrderedDict.items`` method, but also
+    supports indexing and sequence methods.
+    """
+
+    def __init__(self, main):
+        self._main = main
+
+    def __call__(self):
+        """Pretend to be the items method."""
+        return self._main._items()
+
+    def __getitem__(self, index):
+        """Fetch the item at position i."""
+        if isinstance(index, types.SliceType):
+            # fetching a slice returns an OrderedDict
+            return self._main[index].items()
+        key = self._main._sequence[index]
+        return (key, self._main[key])
+
+    def __setitem__(self, index, item):
+        """Set item at position i to item."""
+        if isinstance(index, types.SliceType):
+            # NOTE: item must be an iterable (list of tuples)
+            self._main[index] = OrderedDict(item)
+        else:
+            # FIXME: Does this raise a sensible error?
+            orig = self._main.keys[index]
+            key, value = item
+            if self._main.strict and key in self and (key != orig):
+                raise ValueError('slice assignment must be from '
+                        'unique keys')
+            # delete the current one
+            del self._main[self._main._sequence[index]]
+            self._main.insert(index, key, value)
+
+    def __delitem__(self, i):
+        """Delete the item at position i."""
+        key = self._main._sequence[i]
+        if isinstance(i, types.SliceType):
+            for k in key:
+                # FIXME: efficiency?
+                del self._main[k]
+        else:
+            del self._main[key]
+
+    ### following methods pinched from UserList and adapted ###
+    def __repr__(self): return repr(self._main.items())
+
+    # FIXME: do we need to check if we are comparing with another ``Items``
+    #   object? (like the __cast method of UserList)
+    def __lt__(self, other): return self._main.items() <  other
+    def __le__(self, other): return self._main.items() <= other
+    def __eq__(self, other): return self._main.items() == other
+    def __ne__(self, other): return self._main.items() != other
+    def __gt__(self, other): return self._main.items() >  other
+    def __ge__(self, other): return self._main.items() >= other
+    def __cmp__(self, other): return cmp(self._main.items(), other)
+
+    def __contains__(self, item): return item in self._main.items()
+    def __len__(self): return len(self._main._sequence) # easier :-)
+    def __iter__(self): return self._main.iteritems()
+    def count(self, item): return self._main.items().count(item)
+    def index(self, item, *args): return self._main.items().index(item, *args)
+    def reverse(self): self._main.reverse()
+    def sort(self, *args, **kwds): self._main.sort(*args, **kwds)
+    def __mul__(self, n): return self._main.items()*n
+    __rmul__ = __mul__
+    def __add__(self, other): return self._main.items() + other
+    def __radd__(self, other): return other + self._main.items()
+
+    def append(self, item):
+        """Add an item to the end."""
+        # FIXME: this is only append if the key isn't already present
+        key, value = item
+        self._main[key] = value
+
+    def insert(self, i, item):
+        key, value = item
+        self._main.insert(i, key, value)
+
+    def pop(self, i=-1):
+        key = self._main._sequence[i]
+        return (key, self._main.pop(key))
+
+    def remove(self, item):
+        key, value = item
+        try:
+            assert value == self._main[key]
+        except (KeyError, AssertionError):
+            raise ValueError('ValueError: list.remove(x): x not in list')
+        else:
+            del self._main[key]
+
+    def extend(self, other):
+        # FIXME: is only a true extend if none of the keys already present
+        for item in other:
+            key, value = item
+            self._main[key] = value
+
+    def __iadd__(self, other):
+        self.extend(other)
+
+    ## following methods not implemented for items ##
+
+    def __imul__(self, n): raise TypeError('Can\'t multiply items in place')
+
+class Values(object):
+    """
+    Custom object for accessing the values of an OrderedDict.
+    
+    Can be called like the normal ``OrderedDict.values`` method, but also
+    supports indexing and sequence methods.
+    """
+
+    def __init__(self, main):
+        self._main = main
+
+    def __call__(self):
+        """Pretend to be the values method."""
+        return self._main._values()
+
+    def __getitem__(self, index):
+        """Fetch the value at position i."""
+        if isinstance(index, types.SliceType):
+            return [self._main[key] for key in self._main._sequence[index]]
+        else:
+            return self._main[self._main._sequence[index]]
+
+    def __setitem__(self, index, value):
+        """
+        Set the value at position i to value.
+        
+        You can only do slice assignment to values if you supply a sequence of
+        equal length to the slice you are replacing.
+        """
+        if isinstance(index, types.SliceType):
+            keys = self._main._sequence[index]
+            if len(keys) != len(value):
+                raise ValueError('attempt to assign sequence of size %s '
+                    'to slice of size %s' % (len(name), len(keys)))
+            # FIXME: efficiency?  Would be better to calculate the indexes
+            #   directly from the slice object
+            # NOTE: the new keys can collide with existing keys (or even
+            #   contain duplicates) - these will overwrite
+            for key, val in zip(keys, value):
+                self._main[key] = val
+        else:
+            self._main[self._main._sequence[index]] = value
+
+    ### following methods pinched from UserList and adapted ###
+    def __repr__(self): return repr(self._main.values())
+
+    # FIXME: do we need to check if we are comparing with another ``Values``
+    #   object? (like the __cast method of UserList)
+    def __lt__(self, other): return self._main.values() <  other
+    def __le__(self, other): return self._main.values() <= other
+    def __eq__(self, other): return self._main.values() == other
+    def __ne__(self, other): return self._main.values() != other
+    def __gt__(self, other): return self._main.values() >  other
+    def __ge__(self, other): return self._main.values() >= other
+    def __cmp__(self, other): return cmp(self._main.values(), other)
+
+    def __contains__(self, item): return item in self._main.values()
+    def __len__(self): return len(self._main._sequence) # easier :-)
+    def __iter__(self): return self._main.itervalues()
+    def count(self, item): return self._main.values().count(item)
+    def index(self, item, *args): return self._main.values().index(item, *args)
+
+    def reverse(self):
+        """Reverse the values"""
+        vals = self._main.values()
+        vals.reverse()
+        # FIXME: efficiency
+        self[:] = vals
+
+    def sort(self, *args, **kwds):
+        """Sort the values."""
+        vals = self._main.values()
+        vals.sort(*args, **kwds)
+        self[:] = vals
+
+    def __mul__(self, n): return self._main.values()*n
+    __rmul__ = __mul__
+    def __add__(self, other): return self._main.values() + other
+    def __radd__(self, other): return other + self._main.values()
+
+    ## following methods not implemented for values ##
+    def __delitem__(self, i): raise TypeError('Can\'t delete items from values')
+    def __iadd__(self, other): raise TypeError('Can\'t add in place to values')
+    def __imul__(self, n): raise TypeError('Can\'t multiply values in place')
+    def append(self, item): raise TypeError('Can\'t append items to values')
+    def insert(self, i, item): raise TypeError('Can\'t insert items into values')
+    def pop(self, i=-1): raise TypeError('Can\'t pop items from values')
+    def remove(self, item): raise TypeError('Can\'t remove items from values')
+    def extend(self, other): raise TypeError('Can\'t extend values')
+
+class SequenceOrderedDict(OrderedDict):
+    """
+    Experimental version of OrderedDict that has a custom object for ``keys``,
+    ``values``, and ``items``.
+    
+    These are callable sequence objects that work as methods, or can be
+    manipulated directly as sequences.
+    
+    Test for ``keys``, ``items`` and ``values``.
+    
+    >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)))
+    >>> d
+    SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
+    >>> d.keys
+    [1, 2, 3]
+    >>> d.keys()
+    [1, 2, 3]
+    >>> d.setkeys((3, 2, 1))
+    >>> d
+    SequenceOrderedDict([(3, 4), (2, 3), (1, 2)])
+    >>> d.setkeys((1, 2, 3))
+    >>> d.keys[0]
+    1
+    >>> d.keys[:]
+    [1, 2, 3]
+    >>> d.keys[-1]
+    3
+    >>> d.keys[-2]
+    2
+    >>> d.keys[0:2] = [2, 1]
+    >>> d
+    SequenceOrderedDict([(2, 3), (1, 2), (3, 4)])
+    >>> d.keys.reverse()
+    >>> d.keys
+    [3, 1, 2]
+    >>> d.keys = [1, 2, 3]
+    >>> d
+    SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
+    >>> d.keys = [3, 1, 2]
+    >>> d
+    SequenceOrderedDict([(3, 4), (1, 2), (2, 3)])
+    >>> a = SequenceOrderedDict()
+    >>> b = SequenceOrderedDict()
+    >>> a.keys == b.keys
+    1
+    >>> a['a'] = 3
+    >>> a.keys == b.keys
+    0
+    >>> b['a'] = 3
+    >>> a.keys == b.keys
+    1
+    >>> b['b'] = 3
+    >>> a.keys == b.keys
+    0
+    >>> a.keys > b.keys
+    0
+    >>> a.keys < b.keys
+    1
+    >>> 'a' in a.keys
+    1
+    >>> len(b.keys)
+    2
+    >>> 'c' in d.keys
+    0
+    >>> 1 in d.keys
+    1
+    >>> [v for v in d.keys]
+    [3, 1, 2]
+    >>> d.keys.sort()
+    >>> d.keys
+    [1, 2, 3]
+    >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)), strict=True)
+    >>> d.keys[::-1] = [1, 2, 3]
+    >>> d
+    SequenceOrderedDict([(3, 4), (2, 3), (1, 2)])
+    >>> d.keys[:2]
+    [3, 2]
+    >>> d.keys[:2] = [1, 3]
+    Traceback (most recent call last):
+    KeyError: 'Keylist is not the same as current keylist.'
+
+    >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)))
+    >>> d
+    SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
+    >>> d.values
+    [2, 3, 4]
+    >>> d.values()
+    [2, 3, 4]
+    >>> d.setvalues((4, 3, 2))
+    >>> d
+    SequenceOrderedDict([(1, 4), (2, 3), (3, 2)])
+    >>> d.values[::-1]
+    [2, 3, 4]
+    >>> d.values[0]
+    4
+    >>> d.values[-2]
+    3
+    >>> del d.values[0]
+    Traceback (most recent call last):
+    TypeError: Can't delete items from values
+    >>> d.values[::2] = [2, 4]
+    >>> d
+    SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
+    >>> 7 in d.values
+    0
+    >>> len(d.values)
+    3
+    >>> [val for val in d.values]
+    [2, 3, 4]
+    >>> d.values[-1] = 2
+    >>> d.values.count(2)
+    2
+    >>> d.values.index(2)
+    0
+    >>> d.values[-1] = 7
+    >>> d.values
+    [2, 3, 7]
+    >>> d.values.reverse()
+    >>> d.values
+    [7, 3, 2]
+    >>> d.values.sort()
+    >>> d.values
+    [2, 3, 7]
+    >>> d.values.append('anything')
+    Traceback (most recent call last):
+    TypeError: Can't append items to values
+    >>> d.values = (1, 2, 3)
+    >>> d
+    SequenceOrderedDict([(1, 1), (2, 2), (3, 3)])
+    
+    >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)))
+    >>> d
+    SequenceOrderedDict([(1, 2), (2, 3), (3, 4)])
+    >>> d.items()
+    [(1, 2), (2, 3), (3, 4)]
+    >>> d.setitems([(3, 4), (2 ,3), (1, 2)])
+    >>> d
+    SequenceOrderedDict([(3, 4), (2, 3), (1, 2)])
+    >>> d.items[0]
+    (3, 4)
+    >>> d.items[:-1]
+    [(3, 4), (2, 3)]
+    >>> d.items[1] = (6, 3)
+    >>> d.items
+    [(3, 4), (6, 3), (1, 2)]
+    >>> d.items[1:2] = [(9, 9)]
+    >>> d
+    SequenceOrderedDict([(3, 4), (9, 9), (1, 2)])
+    >>> del d.items[1:2]
+    >>> d
+    SequenceOrderedDict([(3, 4), (1, 2)])
+    >>> (3, 4) in d.items
+    1
+    >>> (4, 3) in d.items
+    0
+    >>> len(d.items)
+    2
+    >>> [v for v in d.items]
+    [(3, 4), (1, 2)]
+    >>> d.items.count((3, 4))
+    1
+    >>> d.items.index((1, 2))
+    1
+    >>> d.items.index((2, 1))
+    Traceback (most recent call last):
+    ValueError: list.index(x): x not in list
+    >>> d.items.reverse()
+    >>> d.items
+    [(1, 2), (3, 4)]
+    >>> d.items.reverse()
+    >>> d.items.sort()
+    >>> d.items
+    [(1, 2), (3, 4)]
+    >>> d.items.append((5, 6))
+    >>> d.items
+    [(1, 2), (3, 4), (5, 6)]
+    >>> d.items.insert(0, (0, 0))
+    >>> d.items
+    [(0, 0), (1, 2), (3, 4), (5, 6)]
+    >>> d.items.insert(-1, (7, 8))
+    >>> d.items
+    [(0, 0), (1, 2), (3, 4), (7, 8), (5, 6)]
+    >>> d.items.pop()
+    (5, 6)
+    >>> d.items
+    [(0, 0), (1, 2), (3, 4), (7, 8)]
+    >>> d.items.remove((1, 2))
+    >>> d.items
+    [(0, 0), (3, 4), (7, 8)]
+    >>> d.items.extend([(1, 2), (5, 6)])
+    >>> d.items
+    [(0, 0), (3, 4), (7, 8), (1, 2), (5, 6)]
+    """
+
+    def __init__(self, init_val=(), strict=True):
+        OrderedDict.__init__(self, init_val, strict=strict)
+        self._keys = self.keys
+        self._values = self.values
+        self._items = self.items
+        self.keys = Keys(self)
+        self.values = Values(self)
+        self.items = Items(self)
+        self._att_dict = {
+            'keys': self.setkeys,
+            'items': self.setitems,
+            'values': self.setvalues,
+        }
+
+    def __setattr__(self, name, value):
+        """Protect keys, items, and values."""
+        if not '_att_dict' in self.__dict__:
+            object.__setattr__(self, name, value)
+        else:
+            try:
+                fun = self._att_dict[name]
+            except KeyError:
+                OrderedDict.__setattr__(self, name, value)
+            else:
+                fun(value)
+
+if __name__ == '__main__':
+    if INTP_VER < (2, 3):
+        raise RuntimeError("Tests require Python v.2.3 or later")
+    # turn off warnings for tests
+    warnings.filterwarnings('ignore')
+    # run the code tests in doctest format
+    import doctest
+    m = sys.modules.get('__main__')
+    globs = m.__dict__.copy()
+    globs.update({
+        'INTP_VER': INTP_VER,
+    })
+    doctest.testmod(m, globs=globs)
+
Index: sansutil/ordereddict.py
===================================================================
--- sansutil/ordereddict.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/ordereddict.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+"""Backport from python2.7 to python <= 2.6."""
+
+from itertools import repeat as _repeat, chain as _chain, starmap as _starmap
+
+try:
+    from itertools import izip_longest as _zip_longest
+except ImportError:
+
+    from itertools import izip
+
+    def _zip_longest(*args, **kwds):
+        # izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
+        fillvalue = kwds.get('fillvalue')
+        def sentinel(counter = ([fillvalue]*(len(args)-1)).pop):
+            yield counter()         # yields the fillvalue, or raises IndexError
+        fillers = _repeat(fillvalue)
+        iters = [_chain(it, sentinel(), fillers) for it in args]
+        try:
+            for tup in izip(*iters):
+                yield tup
+        except IndexError:
+            pass
+
+class OrderedDict(dict):
+
+    def __init__(self, *args, **kwds):
+        if len(args) > 1:
+            raise TypeError('expected at most 1 arguments, got %d' % len(args))
+        if not hasattr(self, '_keys'):
+            self._keys = []
+        self.update(*args, **kwds)
+
+    def clear(self):
+        del self._keys[:]
+        dict.clear(self)
+
+    def __setitem__(self, key, value):
+        if key not in self:
+            self._keys.append(key)
+        dict.__setitem__(self, key, value)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        self._keys.remove(key)
+
+    def __iter__(self):
+        return iter(self._keys)
+
+    def __reversed__(self):
+        return reversed(self._keys)
+
+    def popitem(self):
+        if not self:
+            raise KeyError('dictionary is empty')
+        key = self._keys.pop()
+        value = dict.pop(self, key)
+        return key, value
+
+    def __reduce__(self):
+        items = [[k, self[k]] for k in self]
+        inst_dict = vars(self).copy()
+        inst_dict.pop('_keys', None)
+        return (self.__class__, (items,), inst_dict)
+
+    def setdefault(self, key, default=None):
+        try:
+            return self[key]
+        except KeyError:
+            self[key] = default
+        return default
+
+    def update(self, other=(), **kwds):
+        if hasattr(other, "keys"):
+            for key in other.keys():
+                self[key] = other[key]
+        else:
+            for key, value in other:
+                self[key] = value
+        for key, value in kwds.items():
+            self[key] = value
+
+    __marker = object()
+
+    def pop(self, key, default=__marker):
+        try:
+            value = self[key]
+        except KeyError:
+            if default is self.__marker:
+                raise
+            return default
+        else:
+            del self[key]
+            return value
+
+    def keys(self):
+        return list(self)
+
+    def values(self):
+        return [self[key] for key in self]
+
+    def items(self):
+        return [(key, self[key]) for key in self]
+
+    def __repr__(self):
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, list(self.items()))
+
+    def copy(self):
+        return self.__class__(self)
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        d = cls()
+        for key in iterable:
+            d[key] = value
+        return d
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedDict):
+            return all(p==q for p, q in  _zip_longest(self.items(), other.items()))
+        return dict.__eq__(self, other)
+
+
+
+# End class OrderedDict
+
Index: sansutil/ordereddicttest.py
===================================================================
--- sansutil/ordereddicttest.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/ordereddicttest.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,186 @@
+#!/usr/bin/env python
+
+from random import shuffle
+import copy
+import inspect
+import pickle
+import unittest
+
+
+from ordereddict import OrderedDict
+
+
+class TestOrderedDict(unittest.TestCase):
+
+    def test_init(self):
+        self.assertRaises(TypeError, OrderedDict, ([('a', 1), ('b', 2)], None))
+                # too many args
+        pairs = [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        self.assertEqual(sorted(OrderedDict(dict(pairs)).items()), pairs)           # dict input
+        self.assertEqual(sorted(OrderedDict(**dict(pairs)).items()), pairs)         # kwds input
+        self.assertEqual(list(OrderedDict(pairs).items()), pairs)                   # pairs input
+        self.assertEqual(list(OrderedDict([('a', 1), ('b', 2), ('c', 9), ('d', 4)],
+                                          c=3, e=5).items()), pairs)                # mixed input
+
+        # make sure no positional args conflict with possible kwdargs
+        self.assertEqual(inspect.getargspec(OrderedDict.__dict__['__init__'])[0],
+                         ['self'])
+
+        # Make sure that direct calls to __init__ do not clear previous contents
+        d = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 44), ('e', 55)])
+        d.__init__([('e', 5), ('f', 6)], g=7, d=4)
+        self.assertEqual(list(d.items()),
+            [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', 6), ('g', 7)])
+
+    def test_update(self):
+        self.assertRaises(TypeError, OrderedDict().update, [('a', 1), ('b',
+            2)], None)                        # too many args
+        pairs = [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]
+        od = OrderedDict()
+        od.update(dict(pairs))
+        self.assertEqual(sorted(od.items()), pairs)                                 # dict input
+        od = OrderedDict()
+        od.update(**dict(pairs))
+        self.assertEqual(sorted(od.items()), pairs)                                 # kwds input
+        od = OrderedDict()
+        od.update(pairs)
+        self.assertEqual(list(od.items()), pairs)                                   # pairs input
+        od = OrderedDict()
+        od.update([('a', 1), ('b', 2), ('c', 9), ('d', 4)], c=3, e=5)
+        self.assertEqual(list(od.items()), pairs)                                   # mixed input
+
+        # Make sure that direct calls to update do not clear previous contents
+        # add that updates items are not moved to the end
+        d = OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 44), ('e', 55)])
+        d.update([('e', 5), ('f', 6)], g=7, d=4)
+        self.assertEqual(list(d.items()),
+            [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5), ('f', 6), ('g', 7)])
+
+    def test_clear(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        shuffle(pairs)
+        od = OrderedDict(pairs)
+        self.assertEqual(len(od), len(pairs))
+        od.clear()
+        self.assertEqual(len(od), 0)
+
+    def test_delitem(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        od = OrderedDict(pairs)
+        del od['a']
+        self.assert_('a' not in od)
+        self.assertRaises(KeyError, od.__delitem__, 'a')
+        self.assertEqual(list(od.items()), pairs[:2] + pairs[3:])
+
+    def test_setitem(self):
+        od = OrderedDict([('d', 1), ('b', 2), ('c', 3), ('a', 4), ('e', 5)])
+        od['c'] = 10           # existing element
+        od['f'] = 20           # new element
+        self.assertEqual(list(od.items()),
+                         [('d', 1), ('b', 2), ('c', 10), ('a', 4), ('e', 5), ('f', 20)])
+
+    def test_iterators(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        shuffle(pairs)
+        od = OrderedDict(pairs)
+        self.assertEqual(list(od), [t[0] for t in pairs])
+        self.assertEqual(list(od.keys()), [t[0] for t in pairs])
+        self.assertEqual(list(od.values()), [t[1] for t in pairs])
+        self.assertEqual(list(od.items()), pairs)
+        self.assertEqual(list(reversed(od)),
+                         [t[0] for t in reversed(pairs)])
+
+    def test_popitem(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        shuffle(pairs)
+        od = OrderedDict(pairs)
+        while pairs:
+            self.assertEqual(od.popitem(), pairs.pop())
+        self.assertRaises(KeyError, od.popitem)
+        self.assertEqual(len(od), 0)
+
+    def test_pop(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        shuffle(pairs)
+        od = OrderedDict(pairs)
+        shuffle(pairs)
+        while pairs:
+            k, v = pairs.pop()
+            self.assertEqual(od.pop(k), v)
+        self.assertRaises(KeyError, od.pop, 'xyz')
+        self.assertEqual(len(od), 0)
+        self.assertEqual(od.pop(k, 12345), 12345)
+
+    def test_equality(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        shuffle(pairs)
+        od1 = OrderedDict(pairs)
+        od2 = OrderedDict(pairs)
+        self.assertEqual(od1, od2)          # same order implies equality
+        pairs = pairs[2:] + pairs[:2]
+        od2 = OrderedDict(pairs)
+        self.assertNotEqual(od1, od2)       # different order implies inequality
+        # comparison to regular dict is not order sensitive
+        self.assertEqual(od1, dict(od2))
+        self.assertEqual(dict(od2), od1)
+        # different length implied inequality
+        self.assertNotEqual(od1, OrderedDict(pairs[:-1]))
+
+    def test_copying(self):
+        # Check that ordered dicts are copyable, deepcopyable, picklable,
+        # and have a repr/eval round-trip
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        od = OrderedDict(pairs)
+        update_test = OrderedDict()
+        update_test.update(od)
+        for i, dup in enumerate([
+                    od.copy(),
+                    copy.copy(od),
+                    copy.deepcopy(od),
+                    pickle.loads(pickle.dumps(od, 0)),
+                    pickle.loads(pickle.dumps(od, 1)),
+                    pickle.loads(pickle.dumps(od, 2)),
+                    pickle.loads(pickle.dumps(od, -1)),
+                    eval(repr(od)),
+                    update_test,
+                    OrderedDict(od),
+                    ]):
+            self.assert_(dup is not od)
+            self.assertEquals(dup, od)
+            self.assertEquals(list(dup.items()), list(od.items()))
+            self.assertEquals(len(dup), len(od))
+            self.assertEquals(type(dup), type(od))
+
+    def test_repr(self):
+        od = OrderedDict([('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)])
+        self.assertEqual(repr(od),
+            "OrderedDict([('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)])")
+        self.assertEqual(eval(repr(od)), od)
+        self.assertEqual(repr(OrderedDict()), "OrderedDict()")
+
+    def test_setdefault(self):
+        pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
+        shuffle(pairs)
+        od = OrderedDict(pairs)
+        pair_order = list(od.items())
+        self.assertEqual(od.setdefault('a', 10), 3)
+        # make sure order didn't change
+        self.assertEqual(list(od.items()), pair_order)
+        self.assertEqual(od.setdefault('x', 10), 10)
+        # make sure 'x' is added to the end
+        self.assertEqual(list(od.items())[-1], ('x', 10))
+
+    def test_reinsert(self):
+        # Given insert a, insert b, delete a, re-insert a,
+        # verify that a is now later than b.
+        od = OrderedDict()
+        od['a'] = 1
+        od['b'] = 2
+        del od['a']
+        od['a'] = 1
+        self.assertEqual(list(od.items()), [('b', 2), ('a', 1)])
+
+if __name__ == "__main__":
+
+    unittest.main()
+
Index: sansutil/pathutils.py
===================================================================
--- sansutil/pathutils.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/pathutils.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+"""
+Utilities for path manipulation. Not to be confused with the pathutils module
+from the pythonutils package (http://groups.google.com/group/pythonutils).
+"""
+
+# NOTE: If enough of _that_ pathutils functionality is required, we can switch
+# this module for that one.
+
+# TODO: Make algorithm more robust and complete; consider using abspath.
+
+__all__ = ['relpath']
+
+from os.path import join
+from os.path import sep
+
+def relpath(p1, p2):
+    """Compute the relative path of p1 with respect to p2."""
+
+    def commonpath(L1, L2, common=[]):
+        if len(L1) < 1: return (common, L1, L2)
+        if len(L2) < 1: return (common, L1, L2)
+        if L1[0] != L2[0]: return (common, L1, L2)
+        return commonpath(L1[1:], L2[1:], common=common+[L1[0]])
+
+    # if the strings are equal, then return "."
+    if p1 == p2: return "."
+    (common,L1,L2) = commonpath(p2.split(sep), p1.split(sep))
+    # if there is nothing in common, then return an empty string
+    if not common: return ""
+    # otherwise, replace the common pieces with "../" (or "..\")
+    p = [(".."+sep) * len(L1)] + L2
+    return join(*p)
+
+def test():
+    p1 = sep.join(["a","b","c","d"])
+    p2 = sep.join(["a","b","c1","d1"])
+    p3 = sep.join(["a","b","c","d","e"])
+    p4 = sep.join(["a","b","c","d1","e"])
+    p5 = sep.join(["w","x","y","z"])
+
+    assert relpath(p1, p1) == "."
+    assert relpath(p2, p1) ==  sep.join(["..", "..", "c1", "d1"])
+    assert relpath(p3, p1) == "e"
+    assert relpath(p4, p1) ==  sep.join(["..", "d1", "e"])
+    assert relpath(p5, p1) == ""
+
+if __name__ == '__main__':
+    test()
Index: sansutil/registry.py
===================================================================
--- sansutil/registry.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/registry.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,174 @@
+# This program is public domain
+"""
+File extension registry.
+
+This provides routines for opening files based on extension,
+and registers the built-in file extensions.
+"""
+
+import os.path
+
+class ExtensionRegistry(object):
+    """
+    Associate a file loader with an extension.
+
+    Note that there may be multiple loaders for the same extension.
+
+    Example:
+
+    registry = ExtensionRegistry()
+
+    # Add an association by setting an element
+    registry['.zip'] = unzip
+    
+    # Multiple extensions for one loader
+    registry['.tgz'] = untar
+    registry['.tar.gz'] = untar
+
+    # Generic extensions to use after trying more specific extensions; 
+    # these will be checked after the more specific extensions fail.
+    registry['.gz'] = gunzip
+
+    # Multiple loaders for one extension
+    registry['.cx'] = cx1
+    registry['.cx'] = cx2
+    registry['.cx'] = cx3
+
+    # Show registered extensions
+    print registry.extensions()
+    
+    # Can also register a format name for explicit control from caller
+    registry['cx3'] = cx3
+    print registry.formats()
+
+    # Retrieve loaders for a file name
+    registry.lookup('hello.cx') -> [cx3,cx2,cx1]
+
+    # Run loader on a filename
+    registry.load('hello.cx') ->
+        try:
+            return cx3('hello.cx')
+        except:
+            try:
+                return cx2('hello.cx')
+            except:
+                return cx1('hello.cx')
+
+    # Load in a specific format ignoring extension
+    registry.load('hello.cx',format='cx3') ->
+        return cx3('hello.cx')
+    """
+    def __init__(self, **kw):
+        self.loaders = {}
+    def __setitem__(self, ext, loader):
+        if ext not in self.loaders:
+            self.loaders[ext] = []
+        self.loaders[ext].insert(0,loader)
+    def __getitem__(self, ext):
+        return self.loaders[ext]
+    def __contains__(self, ext):
+        return ext in self.loaders
+    def formats(self):
+        """
+        Return a sorted list of the registered formats.
+        """
+        names = [a for a in self.loaders.keys() if not a.startswith('.')]
+        names.sort()
+        return names
+    def extensions(self):
+        """
+        Return a sorted list of registered extensions.
+        """
+        exts = [a for a in self.loaders.keys() if a.startswith('.')]
+        exts.sort()
+        return exts
+    def lookup(self, path):
+        """
+        Return the loader associated with the file type of path.
+        
+        Raises ValueError if file type is not known.
+        """        
+        # Find matching extensions
+        extlist = [ext for ext in self.extensions() if path.endswith(ext)]
+        # Sort matching extensions by decreasing order of length
+        extlist.sort(lambda a,b: len(a)<len(b))
+        # Combine loaders for matching extensions into one big list
+        loaders = []
+        for L in [self.loaders[ext] for ext in extlist]:
+            loaders.extend(L)
+        # Remove duplicates if they exist
+        if len(loaders) != len(set(loaders)):
+            result = []
+            for L in loaders:
+                if L not in result: result.append(L)
+            loaders = L
+        # Raise an error if there are no matching extensions
+        if len(loaders) == 0:
+            raise ValueError, "Unknown file type for "+path
+        # All done
+        return loaders
+    def load(self, path, format=None):
+        """
+        Call the loader for the file type of path.
+
+        Raises ValueError if no loader is available.
+        Raises KeyError if format is not available.
+        May raise a loader-defined exception if loader fails.        
+        """
+        if format is None:
+            loaders = self.lookup(path)
+        else:
+            loaders = self.loaders[format]
+        for fn in loaders:
+            try:
+                return fn(path)
+            except:
+                pass # give other loaders a chance to succeed
+        # If we get here it is because all loaders failed
+        raise # reraises last exception
+
+def test():
+    reg = ExtensionRegistry()
+    class CxError(Exception): pass
+    def cx(file): return 'cx'
+    def new_cx(file): return 'new_cx'
+    def fail_cx(file): raise CxError
+    def cat(file): return 'cat'
+    def gunzip(file): return 'gunzip'
+    reg['.cx'] = cx
+    reg['.cx1'] = cx
+    reg['.cx'] = new_cx
+    reg['.gz'] = gunzip
+    reg['.cx.gz'] = new_cx
+    reg['.cx1.gz'] = fail_cx
+    reg['.cx1'] = fail_cx
+    reg['.cx2'] = fail_cx
+    reg['new_cx'] = new_cx
+
+    # Two loaders associated with .cx
+    assert reg.lookup('hello.cx') == [new_cx,cx]
+    # Make sure the last loader applies first
+    assert reg.load('hello.cx') == 'new_cx'
+    # Make sure the next loader applies if the first fails
+    assert reg.load('hello.cx1') == 'cx'
+    # Make sure the format override works
+    assert reg.load('hello.cx1',format='.cx.gz') == 'new_cx'
+    # Make sure the format override works
+    assert reg.load('hello.cx1',format='new_cx') == 'new_cx'
+    # Make sure the case of all loaders failing is correct
+    try:  reg.load('hello.cx2')
+    except CxError: pass # correct failure
+    else: raise AssertError,"Incorrect error on load failure"
+    # Make sure the case of no loaders fails correctly
+    try: reg.load('hello.missing')
+    except ValueError,msg:
+        assert str(msg)=="Unknown file type for hello.missing",'Message: <%s>'%(msg)
+    else: raise AssertError,"No error raised for missing extension"
+    assert reg.formats() == ['new_cx']
+    assert reg.extensions() == ['.cx','.cx.gz','.cx1','.cx1.gz','.cx2','.gz']
+    # make sure that it supports multiple '.' in filename
+    assert reg.load('hello.extra.cx1') == 'cx'
+    assert reg.load('hello.gz') == 'gunzip'
+    assert reg.load('hello.cx1.gz') == 'gunzip' # Since .cx1.gz fails
+
+if __name__ == "__main__": test()
Index: sansutil/release_notes.txt
===================================================================
--- sansutil/release_notes.txt	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/release_notes.txt	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,60 @@
+Release Notes
+=============
+
+Package name: data_util 0.1.5
+
+1-	Version 0.1.5
+	- Release date: 28/4/2010
+	- Added OrderedDict (backport from 2.7)
+	- Improved NeXus unit support
+	- Number/uncertainty formats fixes
+ 
+	Version 0.1.2
+	- Release date: 7/4/2009
+	- Added calcthread
+	
+ 	Version 0.1.1
+	- Release date: 4/21/2009
+	
+	Version 0.1
+	- Release date: 8/1/2008
+	- Contains useful data handling utilities from reflectometry group. 
+
+2- Downloading and Installing
+
+	2.1- System Requirements:
+		- Python version >= 2.4 should be running on the system
+
+	2.2- Installing:
+		- Get the code from svn://danse.us/common/releases/data_util-0.1.1
+		- The following modules are required:
+			* numpy
+
+3- Known Issues
+
+	3.1- All systems:
+		- None
+
+	3.2- Windows:
+		- None
+
+	3.3- Linux:
+		- None
+
+4- Troubleshooting
+
+	- None
+
+5- Frequently Asked Questions
+
+	- None
+
+6- Other Resources
+
+	- None
+
+	
+
+	
+
+
Index: sansutil/setup.py
===================================================================
--- sansutil/setup.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/setup.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,17 @@
+"""
+     Installation script for DANSE data handling utilities
+"""
+
+# Then build and install the modules
+from distutils.core import setup, Extension
+
+
+setup(
+    name="data_util",
+    version = "1.0.0",
+    description = "Utilities to handle data",
+    
+    package_dir = {"data_util":"."},
+    packages = ["data_util"]
+)
+        
Index: sansutil/uncertainty.py
===================================================================
--- sansutil/uncertainty.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/uncertainty.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,317 @@
+"""
+
+Uncertainty propagation class, and log() and exp() functions.
+
+Based on scalars or numpy vectors, this class allows you to store and 
+manipulate values+uncertainties, with propagation of gaussian error for
+addition, subtraction, multiplication, division, power, exp() and log().
+
+Storage properties are determined by the numbers used to set the value
+and uncertainty.  Be sure to use floating point uncertainty vectors 
+for inplace operations since numpy does not do automatic type conversion.
+Normal operations can use mixed integer and floating point.  In place
+operations (a *= b, etc.) create at most one extra copy for each operation.
+c = a*b by contrast uses four intermediate vectors, so shouldn't be used
+for huge arrays.
+"""
+
+from __future__ import division
+
+import numpy
+import err1d
+from formatnum import format_uncertainty
+
+__all__ = ['Uncertainty']
+
+# TODO: rename to Measurement and add support for units?
+# TODO: C implementation of *,/,**?
+class Uncertainty(object):
+    # Make standard deviation available
+    def _getdx(self): return numpy.sqrt(self.variance)
+    def _setdx(self,dx): 
+        # Direct operation
+        #    variance = dx**2
+        # Indirect operation to avoid temporaries
+        self.variance[:] = dx
+        self.variance **= 2
+    dx = property(_getdx,_setdx,doc="standard deviation")
+
+    # Constructor
+    def __init__(self, x, variance=None):
+        self.x, self.variance = x, variance    
+ 
+    # Numpy array slicing operations
+    def __len__(self): 
+        return len(self.x)
+    def __getitem__(self,key): 
+        return Uncertainty(self.x[key],self.variance[key])
+    def __setitem__(self,key,value):
+        self.x[key] = value.x
+        self.variance[key] = value.variance
+    def __delitem__(self, key):
+        del self.x[key]
+        del self.variance[key]
+    #def __iter__(self): pass # Not sure we need iter
+
+    # Normal operations: may be of mixed type
+    def __add__(self, other):
+        if isinstance(other,Uncertainty):
+            return Uncertainty(*err1d.add(self.x,self.variance,other.x,other.variance))
+        else:
+            return Uncertainty(self.x+other, self.variance+0) # Force copy
+    def __sub__(self, other):
+        if isinstance(other,Uncertainty):
+            return Uncertainty(*err1d.sub(self.x,self.variance,other.x,other.variance))
+        else:
+            return Uncertainty(self.x-other, self.variance+0) # Force copy
+    def __mul__(self, other):
+        if isinstance(other,Uncertainty):
+            return Uncertainty(*err1d.mul(self.x,self.variance,other.x,other.variance))
+        else:
+            return Uncertainty(self.x*other, self.variance*other**2)
+    def __truediv__(self, other):
+        if isinstance(other,Uncertainty):
+            return Uncertainty(*err1d.div(self.x,self.variance,other.x,other.variance))
+        else:
+            return Uncertainty(self.x/other, self.variance/other**2)
+    def __pow__(self, other):
+        if isinstance(other,Uncertainty):
+            # Haven't calcuated variance in (a+/-da) ** (b+/-db)
+            return NotImplemented
+        else:
+            return Uncertainty(*err1d.pow(self.x,self.variance,other))
+
+    # Reverse operations
+    def __radd__(self, other):
+        return Uncertainty(self.x+other, self.variance+0) # Force copy
+    def __rsub__(self, other):
+        return Uncertainty(other-self.x, self.variance+0)
+    def __rmul__(self, other):
+        return Uncertainty(self.x*other, self.variance*other**2)
+    def __rtruediv__(self, other):
+        x,variance = err1d.pow(self.x,self.variance,-1)
+        return Uncertainty(x*other,variance*other**2)
+    def __rpow__(self, other): return NotImplemented
+
+    # In-place operations: may be of mixed type
+    def __iadd__(self, other):
+        if isinstance(other,Uncertainty):
+            self.x,self.variance \
+                = err1d.add_inplace(self.x,self.variance,other.x,other.variance)
+        else:
+            self.x+=other
+        return self
+    def __isub__(self, other):
+        if isinstance(other,Uncertainty):
+            self.x,self.variance \
+                = err1d.sub_inplace(self.x,self.variance,other.x,other.variance)
+        else:
+            self.x-=other
+        return self
+    def __imul__(self, other):
+        if isinstance(other,Uncertainty):
+            self.x, self.variance \
+                = err1d.mul_inplace(self.x,self.variance,other.x,other.variance)
+        else:
+            self.x *= other
+            self.variance *= other**2
+        return self
+    def __itruediv__(self, other):
+        if isinstance(other,Uncertainty):
+            self.x,self.variance \
+                = err1d.div_inplace(self.x,self.variance,other.x,other.variance)
+        else:
+            self.x /= other
+            self.variance /= other**2
+        return self
+    def __ipow__(self, other):
+        if isinstance(other,Uncertainty):
+            # Haven't calcuated variance in (a+/-da) ** (b+/-db)
+            return NotImplemented
+        else:
+            self.x,self.variance = err1d.pow_inplace(self.x, self.variance, other)
+        return self
+
+    # Use true division instead of integer division
+    def __div__(self, other): return self.__truediv__(other)
+    def __rdiv__(self, other): return self.__rtruediv__(other)
+    def __idiv__(self, other): return self.__itruediv__(other)
+
+        
+    # Unary ops
+    def __neg__(self):
+        return Uncertainty(-self.x,self.variance)
+    def __pos__(self):
+        return self
+    def __abs__(self):
+        return Uncertainty(numpy.abs(self.x),self.variance)
+
+    def __str__(self):
+        #return str(self.x)+" +/- "+str(numpy.sqrt(self.variance))
+        if numpy.isscalar(self.x):
+            return format_uncertainty(self.x,numpy.sqrt(self.variance))
+        else:
+            return [format_uncertainty(v,dv) 
+                    for v,dv in zip(self.x,numpy.sqrt(self.variance))]
+    def __repr__(self):
+        return "Uncertainty(%s,%s)"%(str(self.x),str(self.variance))
+
+    # Not implemented
+    def __floordiv__(self, other): return NotImplemented
+    def __mod__(self, other): return NotImplemented
+    def __divmod__(self, other): return NotImplemented
+    def __mod__(self, other): return NotImplemented
+    def __lshift__(self, other): return NotImplemented
+    def __rshift__(self, other): return NotImplemented
+    def __and__(self, other): return NotImplemented
+    def __xor__(self, other): return NotImplemented
+    def __or__(self, other): return NotImplemented
+
+    def __rfloordiv__(self, other): return NotImplemented
+    def __rmod__(self, other): return NotImplemented
+    def __rdivmod__(self, other): return NotImplemented
+    def __rmod__(self, other): return NotImplemented
+    def __rlshift__(self, other): return NotImplemented
+    def __rrshift__(self, other): return NotImplemented
+    def __rand__(self, other): return NotImplemented
+    def __rxor__(self, other): return NotImplemented
+    def __ror__(self, other): return NotImplemented
+
+    def __ifloordiv__(self, other): return NotImplemented
+    def __imod__(self, other): return NotImplemented
+    def __idivmod__(self, other): return NotImplemented
+    def __imod__(self, other): return NotImplemented
+    def __ilshift__(self, other): return NotImplemented
+    def __irshift__(self, other): return NotImplemented
+    def __iand__(self, other): return NotImplemented
+    def __ixor__(self, other): return NotImplemented
+    def __ior__(self, other): return NotImplemented
+
+    def __invert__(self): return NotImplmented  # For ~x
+    def __complex__(self): return NotImplmented
+    def __int__(self): return NotImplmented
+    def __long__(self): return NotImplmented
+    def __float__(self): return NotImplmented
+    def __oct__(self): return NotImplmented
+    def __hex__(self): return NotImplmented
+    def __index__(self): return NotImplmented
+    def __coerce__(self): return NotImplmented
+
+    def log(self):
+        return Uncertainty(*err1d.log(self.x,self.variance))
+
+    def exp(self):
+        return Uncertainty(*err1d.exp(self.x,self.variance))
+
+def log(val): return self.log()
+def exp(val): return self.exp()
+
+def test():
+    a = Uncertainty(5,3)
+    b = Uncertainty(4,2)
+
+    # Scalar operations
+    z = a+4
+    assert z.x == 5+4 and z.variance == 3
+    z = a-4
+    assert z.x == 5-4 and z.variance == 3
+    z = a*4
+    assert z.x == 5*4 and z.variance == 3*4**2
+    z = a/4
+    assert z.x == 5./4 and z.variance == 3./4**2
+    
+    # Reverse scalar operations
+    z = 4+a
+    assert z.x == 4+5 and z.variance == 3
+    z = 4-a
+    assert z.x == 4-5 and z.variance == 3
+    z = 4*a
+    assert z.x == 4*5 and z.variance == 3*4**2
+    z = 4/a
+    assert z.x == 4./5 and abs(z.variance - 3./5**4 * 4**2) < 1e-15
+    
+    # Power operations
+    z = a**2
+    assert z.x == 5**2 and z.variance == 4*3*5**2
+    z = a**1
+    assert z.x == 5**1 and z.variance == 3
+    z = a**0
+    assert z.x == 5**0 and z.variance == 0
+    z = a**-1
+    assert z.x == 5**-1 and abs(z.variance - 3./5**4) < 1e-15
+
+    # Binary operations
+    z = a+b
+    assert z.x == 5+4 and z.variance == 3+2
+    z = a-b
+    assert z.x == 5-4 and z.variance == 3+2
+    z = a*b
+    assert z.x == 5*4 and z.variance == (5**2*2 + 4**2*3)
+    z = a/b
+    assert z.x == 5./4 and abs(z.variance - (3./5**2 + 2./4**2)*(5./4)**2) < 1e-15
+
+    # ===== Inplace operations =====    
+    # Scalar operations
+    y = a+0; y += 4
+    z = a+4
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+    y = a+0; y -= 4
+    z = a-4
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+    y = a+0; y *= 4
+    z = a*4
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+    y = a+0; y /= 4
+    z = a/4
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+
+    # Power operations
+    y = a+0; y **= 4
+    z = a**4
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+
+    # Binary operations
+    y = a+0; y += b
+    z = a+b
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+    y = a+0; y -= b
+    z = a-b
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+    y = a+0; y *= b
+    z = a*b
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+    y = a+0; y /= b
+    z = a/b
+    assert y.x == z.x and abs(y.variance-z.variance) < 1e-15
+
+
+    # =============== vector operations ================
+    # Slicing
+    z = Uncertainty(numpy.array([1,2,3,4,5]),numpy.array([2,1,2,3,2]))
+    assert z[2].x == 3 and z[2].variance == 2
+    assert (z[2:4].x == [3,4]).all()
+    assert (z[2:4].variance == [2,3]).all()
+    z[2:4] = Uncertainty(numpy.array([8,7]),numpy.array([4,5]))
+    assert z[2].x == 8 and z[2].variance == 4
+    A = Uncertainty(numpy.array([a.x]*2),numpy.array([a.variance]*2))
+    B = Uncertainty(numpy.array([b.x]*2),numpy.array([b.variance]*2))
+
+    # TODO complete tests of copy and inplace operations for vectors and slices.
+
+    # Binary operations
+    z = A+B
+    assert (z.x == 5+4).all() and (z.variance == 3+2).all()
+    z = A-B
+    assert (z.x == 5-4).all() and (z.variance == 3+2).all()
+    z = A*B
+    assert (z.x == 5*4).all() and (z.variance == (5**2*2 + 4**2*3)).all()
+    z = A/B
+    assert (z.x == 5./4).all()
+    assert (abs(z.variance - (3./5**2 + 2./4**2)*(5./4)**2) < 1e-15).all()
+    
+    # printing; note that sqrt(3) ~ 1.7
+    assert str(Uncertainty(5,3)) == "5.0(17)"
+    assert str(Uncertainty(15,3)) == "15.0(17)"
+    assert str(Uncertainty(151.23356,0.324185**2)) == "151.23(32)"
+
+if __name__ == "__main__": test()
Index: sansutil/uniquelist.py
===================================================================
--- sansutil/uniquelist.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
+++ sansutil/uniquelist.py	(revision 402501942f1285a702e0d42b78fbaa03aae64c6d)
@@ -0,0 +1,40 @@
+#
+
+def uniquelist(inputlist, hash=None): 
+    '''remove redunduant elements from the give list
+    and return a list of unique elements.
+
+    inputlist: input list
+    hash: use this function to make the items in the list hashable.
+
+    Implementation details:
+        This function is order-preserving.
+    '''
+    if hash is None:
+        hash = __builtins__.hash
+    seen = {}
+    result = []
+    for item in inputlist:
+        marker = hash(item)
+        if marker in seen: continue
+        seen[marker] = 1
+        result.append(item)
+    return result
+
+
+def test():
+    assert uniquelist([1,2,2,3])==[1,2,3]
+    return
+
+
+def main():
+    test()
+    return
+
+if __name__ == '__main__': main()
+
+
+# version
+__id__ = "$Id$"
+
+# End of file 
