source: sasview/DataLoader/data_info.py @ 84545dd

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 84545dd was 442f42f, checked in by Mathieu Doucet <doucetm@…>, 16 years ago

Working on 2D manipulations

  • Property mode set to 100644
File size: 25.1 KB
Line 
1"""
2    Module that contains classes to hold information read from
3    reduced data files.
4   
5    A good description of the data members can be found in
6    the CanSAS 1D XML data format:
7   
8    http://www.smallangles.net/wgwiki/index.php/cansas1d_documentation
9"""
10
11"""
12This software was developed by the University of Tennessee as part of the
13Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
14project funded by the US National Science Foundation.
15
16If you use DANSE applications to do scientific research that leads to
17publication, we ask that you acknowledge the use of the software with the
18following sentence:
19
20"This work benefited from DANSE software developed under NSF award DMR-0520547."
21
22copyright 2008, University of Tennessee
23"""
24
25#TODO: Keep track of data manipulation in the 'process' data structure.
26#TODO: This module should be independent of plottables. We should write
27#        an adapter class for plottables when needed.
28
29#from sans.guitools.plottables import Data1D as plottable_1D
30from data_util.uncertainty import Uncertainty
31import numpy
32import math
33
34class plottable_1D:
35    """
36        Data1D is a place holder for 1D plottables.
37    """
38    x = None
39    y = None
40    dx = None
41    dy = None
42   
43    # Units
44    _xaxis = ''
45    _xunit = ''
46    _yaxis = ''
47    _yunit = ''
48   
49    def __init__(self,x,y,dx=None,dy=None):
50        self.x = x
51        self.y = y
52        self.dx = dx
53        self.dy = dy
54
55    def xaxis(self, label, unit):
56        self._xaxis = label
57        self._xunit = unit
58       
59    def yaxis(self, label, unit):
60        self._yaxis = label
61        self._yunit = unit
62
63class plottable_2D:
64    """
65        Data2D is a place holder for 2D plottables.
66    """
67    xmin = None
68    xmax = None
69    ymin = None
70    ymax = None
71    data = None
72    err_data = None
73   
74    # Units
75    _xaxis = ''
76    _xunit = ''
77    _yaxis = ''
78    _yunit = ''
79    _zaxis = ''
80    _zunit = ''
81   
82    def __init__(self, data=None, err_data=None):
83        self.data = numpy.asarray(data)
84        self.err_data = numpy.asarray(err_data)
85       
86    def xaxis(self, label, unit):
87        self._xaxis = label
88        self._xunit = unit
89       
90    def yaxis(self, label, unit):
91        self._yaxis = label
92        self._yunit = unit
93           
94    def zaxis(self, label, unit):
95        self._zaxis = label
96        self._zunit = unit
97           
98class Vector:
99    """
100        Vector class to hold multi-dimensional objects
101    """
102    ## x component
103    x = None
104    ## y component
105    y = None
106    ## z component
107    z = None
108   
109    def __init__(self, x=None, y=None, z=None):
110        """
111            Initialization. Components that are not
112            set a set to None by default.
113           
114            @param x: x component
115            @param y: y component
116            @param z: z component
117        """
118        self.x = x
119        self.y = y
120        self.z = z
121       
122    def __str__(self):
123        return "x = %s\ty = %s\tz = %s" % (str(self.x), str(self.y), str(self.z))
124       
125
126class Detector:
127    """
128        Class to hold detector information
129    """
130    ## Name of the instrument [string]
131    name = ''
132    ## Sample to detector distance [float] [mm]
133    distance = None
134    distance_unit = 'mm'
135    ## Offset of this detector position in X, Y, (and Z if necessary) [Vector] [mm]
136    offset = None
137    offset_unit = 'm'
138    ## Orientation (rotation) of this detector in roll, pitch, and yaw [Vector] [degrees]
139    orientation = None
140    orientation_unit = 'degree'
141    ## Center of the beam on the detector in X and Y (and Z if necessary) [Vector] [mm]
142    beam_center = None
143    beam_center_unit = 'mm'
144    ## Pixel size in X, Y, (and Z if necessary) [Vector] [mm]
145    pixel_size = None
146    pixel_size_unit = 'mm'
147    ## Slit length of the instrument for this detector.[float] [mm]
148    slit_length = None
149    slit_length_unit = 'mm'
150   
151    def __init__(self):
152        """
153            Initialize class attribute that are objects...
154        """
155        self.offset      = Vector()
156        self.orientation = Vector()
157        self.beam_center = Vector()
158        self.pixel_size  = Vector()
159       
160   
161    def __str__(self):
162        _str  = "Detector:\n"
163        _str += "   Name:         %s\n" % self.name
164        _str += "   Distance:     %s [%s]\n" % \
165            (str(self.distance), str(self.distance_unit))
166        _str += "   Offset:       %s [%s]\n" % \
167            (str(self.offset), str(self.offset_unit))
168        _str += "   Orientation:  %s [%s]\n" % \
169            (str(self.orientation), str(self.orientation_unit))
170        _str += "   Beam center:  %s [%s]\n" % \
171            (str(self.beam_center), str(self.beam_center_unit))
172        _str += "   Pixel size:   %s [%s]\n" % \
173            (str(self.pixel_size), str(self.pixel_size_unit))
174        _str += "   Slit length:  %s [%s]\n" % \
175            (str(self.slit_length), str(self.slit_length_unit))
176        return _str
177
178class Aperture:
179    ## Name
180    name = None
181    ## Type
182    type = None
183    ## Size name
184    size_name = None
185    ## Aperture size [Vector]
186    size = None
187    size_unit = 'mm'
188    ## Aperture distance [float]
189    distance = None
190    distance_unit = 'mm'
191   
192    def __init__(self):
193        self.size = Vector()
194   
195class Collimation:
196    """
197        Class to hold collimation information
198    """
199    ## Name
200    name = ''
201    ## Length [float] [mm]
202    length = None
203    length_unit = 'mm'
204    ## Aperture
205    aperture = None
206   
207    def __init__(self):
208        self.aperture = []
209   
210    def __str__(self):
211        _str = "Collimation:\n"
212        _str += "   Length:       %s [%s]\n" % \
213            (str(self.length), str(self.length_unit))
214        for item in self.aperture:
215            _str += "   Aperture size:%s [%s]\n" % \
216                (str(item.size), str(item.size_unit))
217            _str += "   Aperture_dist:%s [%s]\n" % \
218                (str(item.distance), str(item.distance_unit))
219        return _str
220
221class Source:
222    """
223        Class to hold source information
224    """ 
225    ## Name
226    name = None
227    ## Radiation type [string]
228    radiation = None
229    ## Beam size name
230    beam_size_name = None
231    ## Beam size [Vector] [mm]
232    beam_size = None
233    beam_size_unit = 'mm'
234    ## Beam shape [string]
235    beam_shape = None
236    ## Wavelength [float] [Angstrom]
237    wavelength = None
238    wavelength_unit = 'A'
239    ## Minimum wavelength [float] [Angstrom]
240    wavelength_min = None
241    wavelength_min_unit = 'nm'
242    ## Maximum wavelength [float] [Angstrom]
243    wavelength_max = None
244    wavelength_max_unit = 'nm'
245    ## Wavelength spread [float] [Angstrom]
246    wavelength_spread = None
247    wavelength_spread_unit = 'percent'
248   
249    def __init__(self):
250        self.beam_size = Vector()
251       
252   
253    def __str__(self):
254        _str  = "Source:\n"
255        _str += "   Radiation:    %s\n" % str(self.radiation)
256        _str += "   Shape:        %s\n" % str(self.beam_shape)
257        _str += "   Wavelength:   %s [%s]\n" % \
258            (str(self.wavelength), str(self.wavelength_unit))
259        _str += "   Waveln_min:   %s [%s]\n" % \
260            (str(self.wavelength_min), str(self.wavelength_min_unit))
261        _str += "   Waveln_max:   %s [%s]\n" % \
262            (str(self.wavelength_max), str(self.wavelength_max_unit))
263        _str += "   Waveln_spread:%s [%s]\n" % \
264            (str(self.wavelength_spread), str(self.wavelength_spread_unit))
265        _str += "   Beam_size:    %s [%s]\n" % \
266            (str(self.beam_size), str(self.beam_size_unit))
267        return _str
268   
269   
270"""
271    Definitions of radiation types
272"""
273NEUTRON  = 'neutron'
274XRAY     = 'x-ray'
275MUON     = 'muon'
276ELECTRON = 'electron'
277   
278class Sample:
279    """
280        Class to hold the sample description
281    """
282    ## Short name for sample
283    name = ''
284    ## ID
285    ID = ''
286    ## Thickness [float] [mm]
287    thickness = None
288    thickness_unit = 'mm'
289    ## Transmission [float] [fraction]
290    transmission = None
291    ## Temperature [float] [C]
292    temperature = None
293    temperature_unit = 'C'
294    ## Position [Vector] [mm]
295    position = None
296    position_unit = 'mm'
297    ## Orientation [Vector] [degrees]
298    orientation = None
299    orientation_unit = 'degree'
300    ## Details
301    details = None
302   
303    def __init__(self):
304        self.position    = Vector()
305        self.orientation = Vector()
306        self.details     = []
307   
308    def __str__(self):
309        _str  = "Sample:\n"
310        _str += "   ID:           %s\n" % str(self.ID)
311        _str += "   Transmission: %s\n" % str(self.transmission)
312        _str += "   Thickness:    %s [%s]\n" % \
313            (str(self.thickness), str(self.thickness_unit))
314        _str += "   Temperature:  %s [%s]\n" % \
315            (str(self.temperature), str(self.temperature_unit))
316        _str += "   Position:     %s [%s]\n" % \
317            (str(self.position), str(self.position_unit))
318        _str += "   Orientation:  %s [%s]\n" % \
319            (str(self.orientation), str(self.orientation_unit))
320       
321        _str += "   Details:\n"
322        for item in self.details:
323            _str += "      %s\n" % item
324           
325        return _str
326 
327class Process:
328    """
329        Class that holds information about the processes
330        performed on the data.
331    """
332    name = ''
333    date = ''
334    description= ''
335    term = None
336    notes = None
337   
338    def __init__(self):
339        self.term = []
340        self.notes = []
341   
342    def __str__(self):
343        _str  = "Process:\n"
344        _str += "   Name:         %s\n" % self.name
345        _str += "   Date:         %s\n" % self.date
346        _str += "   Description:  %s\n" % self.description
347        for item in self.term:
348            _str += "   Term:         %s\n" % item
349        for item in self.notes:
350            _str += "   Note:         %s\n" % item
351        return _str
352   
353 
354class DataInfo:
355    """
356        Class to hold the data read from a file.
357        It includes four blocks of data for the
358        instrument description, the sample description,
359        the data itself and any other meta data.
360    """
361    ## Title
362    title      = ''
363    ## Run number
364    run        = None
365    ## Run name
366    run_name   = None
367    ## File name
368    filename   = ''
369    ## Notes
370    notes      = None
371    ## Processes (Action on the data)
372    process    = None
373    ## Instrument name
374    instrument = ''
375    ## Detector information
376    detector   = None
377    ## Sample information
378    sample     = None
379    ## Source information
380    source     = None
381    ## Collimation information
382    collimation = None
383    ## Additional meta-data
384    meta_data  = None
385    ## Loading errors
386    errors = None
387           
388    def __init__(self):
389        """
390            Initialization
391        """
392        ## Title
393        self.title      = ''
394        ## Run number
395        self.run        = []
396        self.run_name   = {}
397        ## File name
398        self.filename   = ''
399        ## Notes
400        self.notes      = []
401        ## Processes (Action on the data)
402        self.process    = []
403        ## Instrument name
404        self.instrument = ''
405        ## Detector information
406        self.detector   = []
407        ## Sample information
408        self.sample     = Sample()
409        ## Source information
410        self.source     = Source()
411        ## Collimation information
412        self.collimation = []
413        ## Additional meta-data
414        self.meta_data  = {}
415        ## Loading errors
416        self.errors = []       
417       
418    def __str__(self):
419        """
420            Nice printout
421        """
422        _str =  "File:            %s\n" % self.filename
423        _str += "Title:           %s\n" % self.title
424        _str += "Run:             %s\n" % str(self.run)
425        _str += "Instrument:      %s\n" % str(self.instrument)
426        _str += "%s\n" % str(self.sample)
427        _str += "%s\n" % str(self.source)
428        for item in self.detector:
429            _str += "%s\n" % str(item)
430        for item in self.collimation:
431            _str += "%s\n" % str(item)
432        for item in self.process:
433            _str += "%s\n" % str(item)
434        for item in self.notes:
435            _str += "%s\n" % str(item)
436
437        return _str
438           
439    # Private method to perform operation. Not implemented for DataInfo,
440    # but should be implemented for each data class inherited from DataInfo
441    # that holds actual data (ex.: Data1D)
442    def _perform_operation(self, other, operation): return NotImplemented
443
444    def __add__(self, other):
445        """
446            Add two data sets
447           
448            @param other: data set to add to the current one
449            @return: new data set
450            @raise ValueError: raised when two data sets are incompatible
451        """
452        def operation(a, b): return a+b
453        return self._perform_operation(other, operation)
454       
455    def __radd__(self, other):
456        """
457            Add two data sets
458           
459            @param other: data set to add to the current one
460            @return: new data set
461            @raise ValueError: raised when two data sets are incompatible
462        """
463        def operation(a, b): return b+a
464        return self._perform_operation(other, operation)
465       
466    def __sub__(self, other):
467        """
468            Subtract two data sets
469           
470            @param other: data set to subtract from the current one
471            @return: new data set
472            @raise ValueError: raised when two data sets are incompatible
473        """
474        def operation(a, b): return a-b
475        return self._perform_operation(other, operation)
476       
477    def __rsub__(self, other):
478        """
479            Subtract two data sets
480           
481            @param other: data set to subtract from the current one
482            @return: new data set
483            @raise ValueError: raised when two data sets are incompatible
484        """
485        def operation(a, b): return b-a
486        return self._perform_operation(other, operation)
487       
488    def __mul__(self, other):
489        """
490            Multiply two data sets
491           
492            @param other: data set to subtract from the current one
493            @return: new data set
494            @raise ValueError: raised when two data sets are incompatible
495        """
496        def operation(a, b): return a*b
497        return self._perform_operation(other, operation)
498       
499    def __rmul__(self, other):
500        """
501            Multiply two data sets
502           
503            @param other: data set to subtract from the current one
504            @return: new data set
505            @raise ValueError: raised when two data sets are incompatible
506        """
507        def operation(a, b): return b*a
508        return self._perform_operation(other, operation)
509       
510    def __div__(self, other):
511        """
512            Divided a data set by another
513           
514            @param other: data set that the current one is divided by
515            @return: new data set
516            @raise ValueError: raised when two data sets are incompatible
517        """
518        def operation(a, b): return a/b
519        return self._perform_operation(other, operation)
520       
521    def __rdiv__(self, other):
522        """
523            Divided a data set by another
524           
525            @param other: data set that the current one is divided by
526            @return: new data set
527            @raise ValueError: raised when two data sets are incompatible
528        """
529        def operation(a, b): return b/a
530        return self._perform_operation(other, operation)           
531           
532class Data1D(plottable_1D, DataInfo):
533    """
534        1D data class
535    """
536    x_unit = '1/A'
537    y_unit = '1/cm'
538   
539    def __init__(self, x, y, dx=None, dy=None):
540        DataInfo.__init__(self)
541        plottable_1D.__init__(self, x, y, dx, dy)
542        if len(self.detector)>0:
543            raise RuntimeError, "Data1D: Detector bank already filled at init"
544       
545       
546    def __str__(self):
547        """
548            Nice printout
549        """
550        _str =  "%s\n" % DataInfo.__str__(self)
551   
552        _str += "Data:\n"
553        _str += "   Type:         %s\n" % self.__class__.__name__
554        _str += "   X-axis:       %s\t[%s]\n" % (self._xaxis, self._xunit)
555        _str += "   Y-axis:       %s\t[%s]\n" % (self._yaxis, self._yunit)
556        _str += "   Length:       %g\n" % len(self.x)
557
558        return _str
559
560    def clone_without_data(self, length=0):
561        """
562            Clone the current object, without copying the data (which
563            will be filled out by a subsequent operation).
564            The data arrays will be initialized to zero.
565           
566            @param length: length of the data array to be initialized
567        """
568        from copy import deepcopy
569       
570        x  = numpy.zeros(length) 
571        dx = numpy.zeros(length) 
572        y  = numpy.zeros(length) 
573        dy = numpy.zeros(length) 
574       
575        clone = Data1D(x, y, dx=dx, dy=dy)
576        clone.title       = self.title
577        clone.run         = self.run
578        clone.filename    = self.filename
579        clone.notes       = deepcopy(self.notes) 
580        clone.process     = deepcopy(self.process) 
581        clone.detector    = deepcopy(self.detector) 
582        clone.sample      = deepcopy(self.sample) 
583        clone.source      = deepcopy(self.source) 
584        clone.collimation = deepcopy(self.collimation) 
585        clone.meta_data   = deepcopy(self.meta_data) 
586        clone.errors      = deepcopy(self.errors) 
587       
588        return clone
589
590    def _validity_check(self, other):
591        """
592            Checks that the data lengths are compatible.
593            Checks that the x vectors are compatible.
594            Returns errors vectors equal to original
595            errors vectors if they were present or vectors
596            of zeros when none was found.
597           
598            @param other: other data set for operation
599            @return: dy for self, dy for other [numpy arrays]
600            @raise ValueError: when lengths are not compatible
601        """
602        dy_other = None
603        if isinstance(other, Data1D):
604            # Check that data lengths are the same
605            if len(self.x) != len(other.x) or \
606                len(self.y) != len(other.y):
607                raise ValueError, "Unable to perform operation: data length are not equal"
608           
609            # Here we could also extrapolate between data points
610            for i in range(len(self.x)):
611                if self.x[i] != other.x[i]:
612                    raise ValueError, "Incompatible data sets: x-values do not match"
613           
614            # Check that the other data set has errors, otherwise
615            # create zero vector
616            dy_other = other.dy
617            if other.dy==None or (len(other.dy) != len(other.y)):
618                dy_other = numpy.zeros(len(other.y))
619           
620        # Check that we have errors, otherwise create zero vector
621        dy = self.dy
622        if self.dy==None or (len(self.dy) != len(self.y)):
623            dy = numpy.zeros(len(self.y))           
624           
625        return dy, dy_other
626
627    def _perform_operation(self, other, operation):
628        """
629        """
630        # First, check the data compatibility
631        dy, dy_other = self._validity_check(other)
632        result = self.clone_without_data(len(self.x))
633       
634        for i in range(len(self.x)):
635            result.x[i] = self.x[i]
636            if self.dx is not None and len(self.x)==len(self.dx):
637                result.dx[i] = self.dx[i]
638           
639            a = Uncertainty(self.y[i], dy[i]**2)
640            if isinstance(other, Data1D):
641                b = Uncertainty(other.y[i], dy_other[i]**2)
642            else:
643                b = other
644           
645            output = operation(a, b)
646            result.y[i] = output.x
647            result.dy[i] = math.sqrt(math.fabs(output.variance))
648        return result
649       
650class Data2D(plottable_2D, DataInfo):
651    """
652        2D data class
653    """
654    ## Units for Q-values
655    Q_unit = '1/A'
656   
657    ## Units for I(Q) values
658    I_unit = '1/cm'
659   
660    ## Vector of Q-values at the center of each bin in x
661    x_bins = None
662   
663    ## Vector of Q-values at the center of each bin in y
664    y_bins = None
665   
666   
667    def __init__(self, data=None, err_data=None):
668        self.y_bins = []
669        self.x_bins = []
670        DataInfo.__init__(self)
671        plottable_2D.__init__(self, data, err_data)
672        if len(self.detector)>0:
673            raise RuntimeError, "Data2D: Detector bank already filled at init"
674
675    def __str__(self):
676        _str =  "%s\n" % DataInfo.__str__(self)
677       
678        _str += "Data:\n"
679        _str += "   Type:         %s\n" % self.__class__.__name__
680        _str += "   X- & Y-axis:  %s\t[%s]\n" % (self._yaxis, self._yunit)
681        _str += "   Z-axis:       %s\t[%s]\n" % (self._zaxis, self._zunit)
682        leny = 0
683        if len(self.data)>0:
684            leny = len(self.data[0])
685        _str += "   Length:       %g x %g\n" % (len(self.data), leny)
686       
687        return _str
688 
689    def clone_without_data(self, length=0):
690        """
691            Clone the current object, without copying the data (which
692            will be filled out by a subsequent operation).
693            The data arrays will be initialized to zero.
694           
695            @param length: length of the data array to be initialized
696        """
697        from copy import deepcopy
698       
699        data     = numpy.zeros(length) 
700        err_data = numpy.zeros(length) 
701       
702        clone = Data2D(data, err_data)
703        clone.title       = self.title
704        clone.run         = self.run
705        clone.filename    = self.filename
706        clone.notes       = deepcopy(self.notes) 
707        clone.process     = deepcopy(self.process) 
708        clone.detector    = deepcopy(self.detector) 
709        clone.sample      = deepcopy(self.sample) 
710        clone.source      = deepcopy(self.source) 
711        clone.collimation = deepcopy(self.collimation) 
712        clone.meta_data   = deepcopy(self.meta_data) 
713        clone.errors      = deepcopy(self.errors) 
714       
715        return clone
716 
717 
718    def _validity_check(self, other):
719        """
720            Checks that the data lengths are compatible.
721            Checks that the x vectors are compatible.
722            Returns errors vectors equal to original
723            errors vectors if they were present or vectors
724            of zeros when none was found.
725           
726            @param other: other data set for operation
727            @return: dy for self, dy for other [numpy arrays]
728            @raise ValueError: when lengths are not compatible
729        """
730        err_other = None
731        if isinstance(other, Data2D):
732            # Check that data lengths are the same
733            if numpy.size(self.data) != numpy.size(other.data):
734                raise ValueError, "Unable to perform operation: data length are not equal"
735               
736            # Check that the scales match
737            #TODO: matching scales?     
738           
739            # Check that the other data set has errors, otherwise
740            # create zero vector
741            #TODO: test this
742            err_other = other.err_data
743            if other.err_data==None or (numpy.size(other.err_data) != numpy.size(other.data)):
744                err_other = numpy.zeros([numpy.size(other.data,0), numpy.size(other.data,1)])
745           
746        # Check that we have errors, otherwise create zero vector
747        err = self.err_data
748        if self.err_data==None or (numpy.size(self.err_data) != numpy.size(self.data)):
749            err = numpy.zeros([numpy.size(self.data,0), numpy.size(self.data,1)])
750           
751        return err, err_other
752 
753 
754    def _perform_operation(self, other, operation):
755        """
756            Perform 2D operations between data sets
757           
758            @param other: other data set
759            @param operation: function defining the operation
760        """
761        # First, check the data compatibility
762        dy, dy_other = self._validity_check(other)
763   
764        result = self.clone_without_data([numpy.size(self.data,0), numpy.size(self.data,1)])
765       
766        for i in range(numpy.size(self.data,0)):
767            for j in range(numpy.size(self.data,1)):
768                result.data[i][j] = self.data[i][j]
769                if self.err_data is not None and numpy.size(self.data)==numpy.size(self.err_data):
770                    result.err_data[i][j] = self.err_data[i][j]
771               
772                a = Uncertainty(self.data[i][j], dy[i][j]**2)
773                if isinstance(other, Data2D):
774                    b = Uncertainty(other.data[i][j], dy_other[i][j]**2)
775                else:
776                    b = other
777               
778                output = operation(a, b)
779                result.data[i][j] = output.x
780                result.err_data[i][j] = math.sqrt(math.fabs(output.variance))
781        return result
782   
Note: See TracBrowser for help on using the repository browser.