Package reflectometry :: Package reduction :: Module nxsunit

Source Code for Module reflectometry.reduction.nxsunit

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