source: trunk/bindings/python/nxs/unit.py @ 1822

Revision 1547, 7.4 KB checked in by Ray Osborn, 20 months ago (diff)

Refs #252: First commit of the tree interface to the Python bindings

Line 
1# This program is public domain
2# Author: Paul Kienzle
3"""
4Define unit conversion support for NeXus style units.
5
6The unit format is somewhat complicated.  There are variant spellings
7and incorrect capitalization to worry about, as well as forms such as
8"mili*metre" and "1e-7 seconds".
9
10This is a minimal implementation of units including only what I happen to
11need now.  It does not support the complete dimensional analysis provided
12by the package udunits on which NeXus is based, or even the units used
13in the NeXus definition files.
14
15Unlike other units modules, this module does not carry the units along
16with the value, but merely provides a conversion function for
17transforming values.
18
19Usage example::
20
21    import nxs.unit
22    u = nxs.unit.Converter('mili*metre')  # Units stored in mm
23    v = u(3000,'m')  # Convert the value 3000 mm into meters
24
25NeXus example::
26
27    # Load sample orientation in radians regardless of how it is stored.
28    # 1. Open the path
29    file.openpath('/entry1/sample/sample_orientation')
30    # 2. scan the attributes, retrieving 'units'
31    units = [for attr,value in file.attrs() if attr == 'units']
32    # 3. set up the converter (assumes that units actually exists)
33    u = nxs.unit.Converter(units[0])
34    # 4. read the data and convert to the correct units
35    v = u(file.read(),'radians')
36
37This is a standalone module, not relying on either DANSE or NeXus, and
38can be used for other unit conversion tasks.
39
40Note: minutes are used for angle and seconds are used for time.  We
41cannot tell what the correct interpretation is without knowing something
42about the fields themselves.  If this becomes an issue, we will need to
43allow the application to set the dimension for the units rather than
44getting the dimension from the units as we are currently doing.
45"""
46
47# TODO: Add udunits to NAPI rather than reimplementing it in python
48# TODO: Alternatively, parse the udunits database directly
49# UDUnits:
50#  http://www.unidata.ucar.edu/software/udunits/udunits-1/udunits.txt
51
52# TODO: Allow application to impose the map on the units
53
54from __future__ import division
55
56__all__ = ['Converter']
57
58import math
59
60
61# Limited form of units for returning objects of a specific type.
62# Maybe want to do full units handling with e.g., pyre's
63# unit class. For now lets keep it simple.  Note that
64def _build_metric_units(unit,abbr):
65    """
66    Construct standard SI names for the given unit.
67    Builds e.g.,
68        s, ns
69        second, nanosecond, nano*second
70        seconds, nanoseconds
71    Includes prefixes for femto through peta.
72
73    Ack! Allows, e.g., Coulomb and coulomb even though Coulomb is not
74    a unit because some NeXus files store it that way!
75   
76    Returns a dictionary of names and scales.
77    """
78    prefix = dict(peta=1e15,tera=1e12,giga=1e9,mega=1e6,kilo=1e3,
79                  deci=1e-1,centi=1e-2,milli=1e-3,mili=1e-3,micro=1e-6,
80                  nano=1e-9,pico=1e-12,femto=1e-15)
81    short_prefix = dict(P=1e15,T=1e12,G=1e9,M=1e6,k=1e3,
82                        d=1e-1,c=1e-2,m=1e-3,u=1e-6,
83                        n=1e-9,p=1e-12,f=1e-15)
84    map = {abbr:1}
85    map.update([(P+abbr,scale) for (P,scale) in short_prefix.iteritems()])
86    for name in [unit,unit.capitalize()]:
87        map.update({name:1,name+'s':1})
88        map.update([(P+name,scale) for (P,scale) in prefix.iteritems()])
89        map.update([(P+'*'+name,scale) for (P,scale) in prefix.iteritems()])
90        map.update([(P+name+'s',scale) for (P,scale) in prefix.iteritems()])
91    return map
92
93def _build_plural_units(**kw):
94    """
95    Construct names for the given units.  Builds singular and plural form.
96    """
97    map = {}
98    map.update([(name,scale) for name,scale in kw.iteritems()])
99    map.update([(name+'s',scale) for name,scale in kw.iteritems()])
100    return map
101
102def _build_all_units():
103    # Various distance measures
104    distance = _build_metric_units('meter','m')
105    distance.update(_build_metric_units('metre','m'))
106    distance.update(_build_plural_units(micron=1e-6, Angstrom=1e-10))
107    distance.update({'A':1e-10, 'Ang':1e-10})
108
109    # Various time measures.
110    # Note: minutes are used for angle rather than time
111    time = _build_metric_units('second','s')
112    time.update(_build_plural_units(hour=3600,day=24*3600,week=7*24*3600))
113
114    # Various angle measures.
115    # Note: seconds are used for time rather than angle
116    angle = _build_plural_units(degree=1, minute=1/60.,
117                  arcminute=1/60., arcsecond=1/3600., radian=180/math.pi)
118    angle.update(deg=1, arcmin=1/60., arcsec=1/3600., rad=180/math.pi)
119
120    frequency = _build_metric_units('hertz','Hz')
121    frequency.update(_build_metric_units('Hertz','Hz'))
122    frequency.update(_build_plural_units(rpm=1/60.))
123
124    # Note: degrees are used for angle
125    # Note: temperature needs an offset as well as a scale
126    temperature = _build_metric_units('kelvin','K')
127    temperature.update(_build_metric_units('Kelvin','K'))
128
129    charge = _build_metric_units('coulomb','C')
130    charge.update({'microAmp*hour':0.0036})
131
132    sld = { '10^-6 Angstrom^-2': 1e-6, 'Angstrom^-2': 1}
133    Q = { 'invAng': 1, 'invAngstroms': 1,
134          '10^-3 Angstrom^-1': 1e-3, 'nm^-1': 10 }
135
136    # APS files may be using 'a.u.' for 'arbitrary units'.  Other
137    # facilities are leaving the units blank, using ??? or not even
138    # writing the units attributes.
139    unknown = {None:1, '???':1, '': 1, 'a.u.':1}
140
141    dims = [unknown, distance, time, angle, frequency,
142            temperature, charge, sld, Q]
143    return dims
144
145class Converter(object):
146    """
147    Unit converter for NeXus style units.
148
149    """
150    # Define the units, using both American and European spelling.
151    scalemap = None
152    scalebase = 1
153    dims = _build_all_units()
154
155    def __init__(self,name):
156        self.base = name
157        for map in self.dims:
158            if name in map:
159                self.scalemap = map
160                self.scalebase = self.scalemap[name]
161                break
162        else:
163            self.scalemap = {'': 1}
164            self.scalebase = 1
165            #raise ValueError, "Unknown unit %s"%name
166
167    def scale(self, units=""):
168        if units == "" or self.scalemap is None: return 1
169        return self.scalebase/self.scalemap[units]
170
171    def __call__(self, value, units=""):
172        # Note: calculating a*1 rather than simply returning a would produce
173        # an unnecessary copy of the array, which in the case of the raw
174        # counts array would be bad.  Sometimes copying and other times
175        # not copying is also bad, but copy on modify semantics isn't
176        # supported.
177        if units == "" or self.scalemap is None: return value
178        try:
179            return value * (self.scalebase/self.scalemap[units])
180        except KeyError:
181            raise KeyError("%s not in %s"%(units," ".join(self.scalemap.keys())))
182
183def _check(expect,get):
184    if expect != get: raise ValueError, "Expected %s but got %s"%(expect,get)
185    #print expect,"==",get
186
187def test():
188    _check(2,Converter('mm')(2000,'m')) # 2000 mm -> 2 m
189    _check(0.003,Converter('microseconds')(3,units='ms')) # 3 us -> 0.003 ms
190    _check(45,Converter('nanokelvin')(45))  # 45 nK -> 45 nK
191    # TODO: more tests
192    _check(0.5,Converter('seconds')(1800,units='hours')) # 1800 -> 0.5 hr
193    _check(2.5,Converter('a.u.')(2.5,units=''))
194
195if __name__ == "__main__":
196    test()
Note: See TracBrowser for help on using the repository browser.