source: sasview/src/sas/sascalc/dataloader/data_info.py @ 3d6c010

Last change on this file since 3d6c010 was 9a5097c, checked in by andyfaff, 8 years ago

MAINT: import numpy as np

  • Property mode set to 100644
File size: 40.5 KB
RevLine 
[a3084ada]1"""
[f60a8c2]2    Module that contains classes to hold information read from
[a3084ada]3    reduced data files.
[e4f421c]4
[f60a8c2]5    A good description of the data members can be found in
[a3084ada]6    the CanSAS 1D XML data format:
[e4f421c]7
[a3084ada]8    http://www.smallangles.net/wgwiki/index.php/cansas1d_documentation
9"""
[f60a8c2]10#####################################################################
[0997158f]11#This software was developed by the University of Tennessee as part of the
12#Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
[f60a8c2]13#project funded by the US National Science Foundation.
14#See the license text in license.txt
[0997158f]15#copyright 2008, University of Tennessee
[f60a8c2]16######################################################################
[a3084ada]17
18
[b39c817]19#TODO: Keep track of data manipulation in the 'process' data structure.
[579ba85]20#TODO: This module should be independent of plottables. We should write
21#        an adapter class for plottables when needed.
[b39c817]22
[79492222]23#from sas.guitools.plottables import Data1D as plottable_1D
[b699768]24from sas.sascalc.data_util.uncertainty import Uncertainty
[9a5097c]25import numpy as np
[9198b83]26import math
[345e7e4]27
28class plottable_1D(object):
29    """
30    Data1D is a place holder for 1D plottables.
31    """
32    # The presence of these should be mutually
33    # exclusive with the presence of Qdev (dx)
34    x = None
35    y = None
36    dx = None
37    dy = None
38    ## Slit smearing length
39    dxl = None
40    ## Slit smearing width
41    dxw = None
[a9f579c]42    ## SESANS specific params (wavelengths for spin echo length calculation)
43    lam = None
44    dlam = None
[1b82623]45
[579ba85]46    # Units
47    _xaxis = ''
48    _xunit = ''
49    _yaxis = ''
50    _yunit = ''
[e4f421c]51
[a9f579c]52    def __init__(self, x, y, dx=None, dy=None, dxl=None, dxw=None, lam=None, dlam=None):
[9a5097c]53        self.x = np.asarray(x)
54        self.y = np.asarray(y)
[a7a5886]55        if dx is not None:
[9a5097c]56            self.dx = np.asarray(dx)
[f60a8c2]57        if dy is not None:
[9a5097c]58            self.dy = np.asarray(dy)
[f60a8c2]59        if dxl is not None:
[9a5097c]60            self.dxl = np.asarray(dxl)
[e4f421c]61        if dxw is not None:
[9a5097c]62            self.dxw = np.asarray(dxw)
[a9f579c]63        if lam is not None:
[9a5097c]64            self.lam = np.asarray(lam)
[a9f579c]65        if dlam is not None:
[9a5097c]66            self.dlam = np.asarray(dlam)
[579ba85]67
68    def xaxis(self, label, unit):
[a7a5886]69        """
70        set the x axis label and unit
71        """
[579ba85]72        self._xaxis = label
73        self._xunit = unit
[e4f421c]74
[579ba85]75    def yaxis(self, label, unit):
[a7a5886]76        """
77        set the y axis label and unit
78        """
[579ba85]79        self._yaxis = label
80        self._yunit = unit
81
[f60a8c2]82
[e4f421c]83class plottable_2D(object):
[8780e9a]84    """
[0997158f]85    Data2D is a place holder for 2D plottables.
[8780e9a]86    """
87    xmin = None
88    xmax = None
89    ymin = None
90    ymax = None
[99d1af6]91    data = None
[f60a8c2]92    qx_data = None
93    qy_data = None
94    q_data = None
95    err_data = None
96    dqx_data = None
97    dqy_data = None
98    mask = None
[e4f421c]99
[99d1af6]100    # Units
101    _xaxis = ''
102    _xunit = ''
103    _yaxis = ''
104    _yunit = ''
105    _zaxis = ''
106    _zunit = ''
[e4f421c]107
[a7a5886]108    def __init__(self, data=None, err_data=None, qx_data=None,
[f60a8c2]109                 qy_data=None, q_data=None, mask=None,
110                 dqx_data=None, dqy_data=None):
[9a5097c]111        self.data = np.asarray(data)
112        self.qx_data = np.asarray(qx_data)
113        self.qy_data = np.asarray(qy_data)
114        self.q_data = np.asarray(q_data)
115        self.mask = np.asarray(mask)
116        self.err_data = np.asarray(err_data)
[f60a8c2]117        if dqx_data is not None:
[9a5097c]118            self.dqx_data = np.asarray(dqx_data)
[f60a8c2]119        if dqy_data is not None:
[9a5097c]120            self.dqy_data = np.asarray(dqy_data)
[e4f421c]121
[99d1af6]122    def xaxis(self, label, unit):
[a7a5886]123        """
124        set the x axis label and unit
125        """
[99d1af6]126        self._xaxis = label
127        self._xunit = unit
[e4f421c]128
[99d1af6]129    def yaxis(self, label, unit):
[a7a5886]130        """
131        set the y axis label and unit
132        """
[99d1af6]133        self._yaxis = label
134        self._yunit = unit
[e4f421c]135
[99d1af6]136    def zaxis(self, label, unit):
[a7a5886]137        """
138        set the z axis label and unit
139        """
[99d1af6]140        self._zaxis = label
141        self._zunit = unit
[de5c813]142
[e4f421c]143
144class Vector(object):
[a3084ada]145    """
[0997158f]146    Vector class to hold multi-dimensional objects
[a3084ada]147    """
148    ## x component
149    x = None
150    ## y component
151    y = None
152    ## z component
153    z = None
[e4f421c]154
[a3084ada]155    def __init__(self, x=None, y=None, z=None):
156        """
[0997158f]157        Initialization. Components that are not
158        set a set to None by default.
[e4f421c]159
[0997158f]160        :param x: x component
161        :param y: y component
162        :param z: z component
[a3084ada]163        """
164        self.x = x
165        self.y = y
166        self.z = z
[e4f421c]167
[a3084ada]168    def __str__(self):
[a7a5886]169        msg = "x = %s\ty = %s\tz = %s" % (str(self.x), str(self.y), str(self.z))
170        return msg
[a3084ada]171
[e4f421c]172
173class Detector(object):
[a3084ada]174    """
[0997158f]175    Class to hold detector information
[a3084ada]176    """
177    ## Name of the instrument [string]
[fe78c7b]178    name = None
[a3084ada]179    ## Sample to detector distance [float] [mm]
180    distance = None
[b39c817]181    distance_unit = 'mm'
[f60a8c2]182    ## Offset of this detector position in X, Y,
183    #(and Z if necessary) [Vector] [mm]
[d6513cd]184    offset = None
[b39c817]185    offset_unit = 'm'
[a7a5886]186    ## Orientation (rotation) of this detector in roll,
187    # pitch, and yaw [Vector] [degrees]
[d6513cd]188    orientation = None
[8780e9a]189    orientation_unit = 'degree'
[f60a8c2]190    ## Center of the beam on the detector in X and Y
[a7a5886]191    #(and Z if necessary) [Vector] [mm]
[d6513cd]192    beam_center = None
[8780e9a]193    beam_center_unit = 'mm'
[a3084ada]194    ## Pixel size in X, Y, (and Z if necessary) [Vector] [mm]
[d6513cd]195    pixel_size = None
[8780e9a]196    pixel_size_unit = 'mm'
[a3084ada]197    ## Slit length of the instrument for this detector.[float] [mm]
198    slit_length = None
[2e9b98c]199    slit_length_unit = 'mm'
[e4f421c]200
[d6513cd]201    def __init__(self):
202        """
[0997158f]203        Initialize class attribute that are objects...
[d6513cd]204        """
[e4f421c]205        self.offset = Vector()
[d6513cd]206        self.orientation = Vector()
207        self.beam_center = Vector()
[e4f421c]208        self.pixel_size = Vector()
209
[8780e9a]210    def __str__(self):
[e4f421c]211        _str = "Detector:\n"
[8780e9a]212        _str += "   Name:         %s\n" % self.name
213        _str += "   Distance:     %s [%s]\n" % \
214            (str(self.distance), str(self.distance_unit))
215        _str += "   Offset:       %s [%s]\n" % \
216            (str(self.offset), str(self.offset_unit))
217        _str += "   Orientation:  %s [%s]\n" % \
218            (str(self.orientation), str(self.orientation_unit))
219        _str += "   Beam center:  %s [%s]\n" % \
220            (str(self.beam_center), str(self.beam_center_unit))
221        _str += "   Pixel size:   %s [%s]\n" % \
222            (str(self.pixel_size), str(self.pixel_size_unit))
223        _str += "   Slit length:  %s [%s]\n" % \
224            (str(self.slit_length), str(self.slit_length_unit))
225        return _str
[a3084ada]226
[f60a8c2]227
[e4f421c]228class Aperture(object):
[4c00964]229    ## Name
[579ba85]230    name = None
[4c00964]231    ## Type
[579ba85]232    type = None
233    ## Size name
234    size_name = None
[4c00964]235    ## Aperture size [Vector]
[d6513cd]236    size = None
237    size_unit = 'mm'
[4c00964]238    ## Aperture distance [float]
[d6513cd]239    distance = None
240    distance_unit = 'mm'
[e4f421c]241
[d6513cd]242    def __init__(self):
243        self.size = Vector()
[e4f421c]244
245
246class Collimation(object):
[a3084ada]247    """
[0997158f]248    Class to hold collimation information
[a3084ada]249    """
[4c00964]250    ## Name
[fe78c7b]251    name = None
[a3084ada]252    ## Length [float] [mm]
253    length = None
[8780e9a]254    length_unit = 'mm'
255    ## Aperture
[d6513cd]256    aperture = None
[e4f421c]257
[d6513cd]258    def __init__(self):
259        self.aperture = []
[e4f421c]260
[8780e9a]261    def __str__(self):
262        _str = "Collimation:\n"
263        _str += "   Length:       %s [%s]\n" % \
264            (str(self.length), str(self.length_unit))
265        for item in self.aperture:
266            _str += "   Aperture size:%s [%s]\n" % \
267                (str(item.size), str(item.size_unit))
268            _str += "   Aperture_dist:%s [%s]\n" % \
269                (str(item.distance), str(item.distance_unit))
270        return _str
[a3084ada]271
[f60a8c2]272
[e4f421c]273class Source(object):
[a3084ada]274    """
[0997158f]275    Class to hold source information
[f60a8c2]276    """
[4c00964]277    ## Name
[579ba85]278    name = None
[a3084ada]279    ## Radiation type [string]
[579ba85]280    radiation = None
281    ## Beam size name
282    beam_size_name = None
[a3084ada]283    ## Beam size [Vector] [mm]
[d6513cd]284    beam_size = None
[8780e9a]285    beam_size_unit = 'mm'
[a3084ada]286    ## Beam shape [string]
[579ba85]287    beam_shape = None
[a3084ada]288    ## Wavelength [float] [Angstrom]
289    wavelength = None
[8780e9a]290    wavelength_unit = 'A'
[a3084ada]291    ## Minimum wavelength [float] [Angstrom]
292    wavelength_min = None
[8780e9a]293    wavelength_min_unit = 'nm'
[a3084ada]294    ## Maximum wavelength [float] [Angstrom]
295    wavelength_max = None
[8780e9a]296    wavelength_max_unit = 'nm'
[a3084ada]297    ## Wavelength spread [float] [Angstrom]
298    wavelength_spread = None
[8780e9a]299    wavelength_spread_unit = 'percent'
[e4f421c]300
[d6513cd]301    def __init__(self):
302        self.beam_size = Vector()
[e4f421c]303
[8780e9a]304    def __str__(self):
[e4f421c]305        _str = "Source:\n"
[8780e9a]306        _str += "   Radiation:    %s\n" % str(self.radiation)
307        _str += "   Shape:        %s\n" % str(self.beam_shape)
308        _str += "   Wavelength:   %s [%s]\n" % \
309            (str(self.wavelength), str(self.wavelength_unit))
310        _str += "   Waveln_min:   %s [%s]\n" % \
311            (str(self.wavelength_min), str(self.wavelength_min_unit))
312        _str += "   Waveln_max:   %s [%s]\n" % \
313            (str(self.wavelength_max), str(self.wavelength_max_unit))
314        _str += "   Waveln_spread:%s [%s]\n" % \
315            (str(self.wavelength_spread), str(self.wavelength_spread_unit))
316        _str += "   Beam_size:    %s [%s]\n" % \
317            (str(self.beam_size), str(self.beam_size_unit))
318        return _str
[e4f421c]319
320
[f60a8c2]321"""
[0997158f]322Definitions of radiation types
[a3084ada]323"""
[e4f421c]324NEUTRON = 'neutron'
325XRAY = 'x-ray'
326MUON = 'muon'
[a3084ada]327ELECTRON = 'electron'
[e4f421c]328
329
330class Sample(object):
[a3084ada]331    """
[0997158f]332    Class to hold the sample description
[a3084ada]333    """
[579ba85]334    ## Short name for sample
335    name = ''
[a3084ada]336    ## ID
337    ID = ''
338    ## Thickness [float] [mm]
339    thickness = None
[8780e9a]340    thickness_unit = 'mm'
341    ## Transmission [float] [fraction]
[a3084ada]342    transmission = None
[b3efb7d]343    ## Temperature [float] [No Default]
[a3084ada]344    temperature = None
[b3efb7d]345    temperature_unit = None
[a3084ada]346    ## Position [Vector] [mm]
[d6513cd]347    position = None
[8780e9a]348    position_unit = 'mm'
[a3084ada]349    ## Orientation [Vector] [degrees]
[d6513cd]350    orientation = None
[8780e9a]351    orientation_unit = 'degree'
[a3084ada]352    ## Details
[d6513cd]353    details = None
[ad4632c]354    ## SESANS zacceptance
355    zacceptance = None
[e4f421c]356
[d6513cd]357    def __init__(self):
[e4f421c]358        self.position = Vector()
[d6513cd]359        self.orientation = Vector()
[e4f421c]360        self.details = []
361
[8780e9a]362    def __str__(self):
[e4f421c]363        _str = "Sample:\n"
[8780e9a]364        _str += "   ID:           %s\n" % str(self.ID)
365        _str += "   Transmission: %s\n" % str(self.transmission)
366        _str += "   Thickness:    %s [%s]\n" % \
367            (str(self.thickness), str(self.thickness_unit))
368        _str += "   Temperature:  %s [%s]\n" % \
369            (str(self.temperature), str(self.temperature_unit))
370        _str += "   Position:     %s [%s]\n" % \
371            (str(self.position), str(self.position_unit))
372        _str += "   Orientation:  %s [%s]\n" % \
373            (str(self.orientation), str(self.orientation_unit))
[e4f421c]374
[8780e9a]375        _str += "   Details:\n"
376        for item in self.details:
377            _str += "      %s\n" % item
[e4f421c]378
[8780e9a]379        return _str
[e4f421c]380
381
382class Process(object):
[8780e9a]383    """
[0997158f]384    Class that holds information about the processes
385    performed on the data.
[8780e9a]386    """
387    name = ''
388    date = ''
[a7a5886]389    description = ''
[d6513cd]390    term = None
391    notes = None
[e4f421c]392
[d6513cd]393    def __init__(self):
394        self.term = []
395        self.notes = []
[e4f421c]396
[3a5f7c8]397    def is_empty(self):
398        """
399            Return True if the object is empty
400        """
401        return len(self.name) == 0 and len(self.date) == 0 and len(self.description) == 0 \
402            and len(self.term) == 0 and len(self.notes) == 0
[a4deca6]403
[3a5f7c8]404    def single_line_desc(self):
405        """
406            Return a single line string representing the process
407        """
408        return "%s %s %s" % (self.name, self.date, self.description)
[a4deca6]409
[8780e9a]410    def __str__(self):
[e4f421c]411        _str = "Process:\n"
[8780e9a]412        _str += "   Name:         %s\n" % self.name
413        _str += "   Date:         %s\n" % self.date
414        _str += "   Description:  %s\n" % self.description
415        for item in self.term:
416            _str += "   Term:         %s\n" % item
417        for item in self.notes:
418            _str += "   Note:         %s\n" % item
419        return _str
[e4f421c]420
421
422class TransmissionSpectrum(object):
[be577e7]423    """
424    Class that holds information about transmission spectrum
425    for white beams and spallation sources.
426    """
427    name = ''
428    timestamp = ''
429    ## Wavelength (float) [A]
430    wavelength = None
431    wavelength_unit = 'A'
432    ## Transmission (float) [unit less]
433    transmission = None
434    transmission_unit = ''
435    ## Transmission Deviation (float) [unit less]
436    transmission_deviation = None
437    transmission_deviation_unit = ''
[e4f421c]438
[be577e7]439    def __init__(self):
440        self.wavelength = []
441        self.transmission = []
442        self.transmission_deviation = []
[e4f421c]443
[be577e7]444    def __str__(self):
[e4f421c]445        _str = "Transmission Spectrum:\n"
[76cd1ae]446        _str += "   Name:             \t{0}\n".format(self.name)
447        _str += "   Timestamp:        \t{0}\n".format(self.timestamp)
448        _str += "   Wavelength unit:  \t{0}\n".format(self.wavelength_unit)
449        _str += "   Transmission unit:\t{0}\n".format(self.transmission_unit)
450        _str += "   Trans. Dev. unit:  \t{0}\n".format(\
451                                            self.transmission_deviation_unit)
452        length_list = [len(self.wavelength), len(self.transmission), \
453                len(self.transmission_deviation)]
454        _str += "   Number of Pts:    \t{0}\n".format(max(length_list))
[be577e7]455        return _str
[e4f421c]456
457
458class DataInfo(object):
[a3084ada]459    """
[0997158f]460    Class to hold the data read from a file.
461    It includes four blocks of data for the
462    instrument description, the sample description,
463    the data itself and any other meta data.
[a3084ada]464    """
[f60a8c2]465    ## Title
[e4f421c]466    title = ''
[a3084ada]467    ## Run number
[e4f421c]468    run = None
[579ba85]469    ## Run name
[e4f421c]470    run_name = None
[a3084ada]471    ## File name
[e4f421c]472    filename = ''
[a3084ada]473    ## Notes
[e4f421c]474    notes = None
[a3084ada]475    ## Processes (Action on the data)
[e4f421c]476    process = None
[8780e9a]477    ## Instrument name
478    instrument = ''
[a3084ada]479    ## Detector information
[e4f421c]480    detector = None
[a3084ada]481    ## Sample information
[e4f421c]482    sample = None
[a3084ada]483    ## Source information
[e4f421c]484    source = None
[8780e9a]485    ## Collimation information
[d6513cd]486    collimation = None
[be577e7]487    ## Transmission Spectrum INfo
488    trans_spectrum = None
[a3084ada]489    ## Additional meta-data
[e4f421c]490    meta_data = None
[8780e9a]491    ## Loading errors
[d6513cd]492    errors = None
[20522e1]493    ## SESANS data check
494    isSesans = None
495
[e4f421c]496
[b99ac227]497    def __init__(self):
498        """
[0997158f]499        Initialization
[b99ac227]500        """
[e4f421c]501        ## Title
502        self.title = ''
[b99ac227]503        ## Run number
[e4f421c]504        self.run = []
505        self.run_name = {}
[b99ac227]506        ## File name
[e4f421c]507        self.filename = ''
[b99ac227]508        ## Notes
[e4f421c]509        self.notes = []
[b99ac227]510        ## Processes (Action on the data)
[e4f421c]511        self.process = []
[b99ac227]512        ## Instrument name
513        self.instrument = ''
514        ## Detector information
[e4f421c]515        self.detector = []
[b99ac227]516        ## Sample information
[e4f421c]517        self.sample = Sample()
[b99ac227]518        ## Source information
[e4f421c]519        self.source = Source()
[b99ac227]520        ## Collimation information
521        self.collimation = []
[be577e7]522        ## Transmission Spectrum
[76cd1ae]523        self.trans_spectrum = []
[b99ac227]524        ## Additional meta-data
[e4f421c]525        self.meta_data = {}
[b99ac227]526        ## Loading errors
[f60a8c2]527        self.errors = []
[20522e1]528        ## SESANS data check
529        self.isSesans = False
[e4f421c]530
[99abf5d]531    def append_empty_process(self):
[67c7e89]532        """
533        """
534        self.process.append(Process())
[e4f421c]535
[67c7e89]536    def add_notes(self, message=""):
537        """
538        Add notes to datainfo
539        """
540        self.notes.append(message)
[e4f421c]541
[99d1af6]542    def __str__(self):
543        """
[0997158f]544        Nice printout
[99d1af6]545        """
[e4f421c]546        _str = "File:            %s\n" % self.filename
[99d1af6]547        _str += "Title:           %s\n" % self.title
548        _str += "Run:             %s\n" % str(self.run)
[ad4632c]549        _str += "SESANS:          %s\n" % str(self.isSesans)
[99d1af6]550        _str += "Instrument:      %s\n" % str(self.instrument)
551        _str += "%s\n" % str(self.sample)
552        _str += "%s\n" % str(self.source)
553        for item in self.detector:
554            _str += "%s\n" % str(item)
555        for item in self.collimation:
556            _str += "%s\n" % str(item)
557        for item in self.process:
558            _str += "%s\n" % str(item)
559        for item in self.notes:
560            _str += "%s\n" % str(item)
[76cd1ae]561        for item in self.trans_spectrum:
562            _str += "%s\n" % str(item)
[99d1af6]563        return _str
[e4f421c]564
[b39c817]565    # Private method to perform operation. Not implemented for DataInfo,
566    # but should be implemented for each data class inherited from DataInfo
567    # that holds actual data (ex.: Data1D)
[f60a8c2]568    def _perform_operation(self, other, operation):
[a7a5886]569        """
570        Private method to perform operation. Not implemented for DataInfo,
571        but should be implemented for each data class inherited from DataInfo
572        that holds actual data (ex.: Data1D)
573        """
574        return NotImplemented
[e4f421c]575
[365aa1ed]576    def _perform_union(self, other):
577        """
578        Private method to perform union operation. Not implemented for DataInfo,
579        but should be implemented for each data class inherited from DataInfo
580        that holds actual data (ex.: Data1D)
581        """
582        return NotImplemented
[b39c817]583
584    def __add__(self, other):
585        """
[0997158f]586        Add two data sets
[e4f421c]587
[0997158f]588        :param other: data set to add to the current one
589        :return: new data set
590        :raise ValueError: raised when two data sets are incompatible
[b39c817]591        """
[a7a5886]592        def operation(a, b):
593            return a + b
[b39c817]594        return self._perform_operation(other, operation)
[e4f421c]595
[b39c817]596    def __radd__(self, other):
597        """
[0997158f]598        Add two data sets
[e4f421c]599
[0997158f]600        :param other: data set to add to the current one
601        :return: new data set
602        :raise ValueError: raised when two data sets are incompatible
[b39c817]603        """
[a7a5886]604        def operation(a, b):
605            return b + a
[b39c817]606        return self._perform_operation(other, operation)
[e4f421c]607
[b39c817]608    def __sub__(self, other):
609        """
[0997158f]610        Subtract two data sets
[e4f421c]611
[0997158f]612        :param other: data set to subtract from the current one
613        :return: new data set
614        :raise ValueError: raised when two data sets are incompatible
[b39c817]615        """
[a7a5886]616        def operation(a, b):
617            return a - b
[b39c817]618        return self._perform_operation(other, operation)
[e4f421c]619
[b39c817]620    def __rsub__(self, other):
621        """
[0997158f]622        Subtract two data sets
[e4f421c]623
[0997158f]624        :param other: data set to subtract from the current one
625        :return: new data set
626        :raise ValueError: raised when two data sets are incompatible
[b39c817]627        """
[a7a5886]628        def operation(a, b):
629            return b - a
[b39c817]630        return self._perform_operation(other, operation)
[e4f421c]631
[b39c817]632    def __mul__(self, other):
633        """
[0997158f]634        Multiply two data sets
[e4f421c]635
[0997158f]636        :param other: data set to subtract from the current one
637        :return: new data set
638        :raise ValueError: raised when two data sets are incompatible
[b39c817]639        """
[a7a5886]640        def operation(a, b):
641            return a * b
[b39c817]642        return self._perform_operation(other, operation)
[e4f421c]643
[b39c817]644    def __rmul__(self, other):
645        """
[0997158f]646        Multiply two data sets
[e4f421c]647
[0997158f]648        :param other: data set to subtract from the current one
649        :return: new data set
650        :raise ValueError: raised when two data sets are incompatible
[b39c817]651        """
[a7a5886]652        def operation(a, b):
653            return b * a
[b39c817]654        return self._perform_operation(other, operation)
[e4f421c]655
[b39c817]656    def __div__(self, other):
657        """
[0997158f]658        Divided a data set by another
[e4f421c]659
[0997158f]660        :param other: data set that the current one is divided by
661        :return: new data set
662        :raise ValueError: raised when two data sets are incompatible
[b39c817]663        """
[a7a5886]664        def operation(a, b):
665            return a/b
[b39c817]666        return self._perform_operation(other, operation)
[e4f421c]667
[b39c817]668    def __rdiv__(self, other):
669        """
[0997158f]670        Divided a data set by another
[e4f421c]671
[0997158f]672        :param other: data set that the current one is divided by
673        :return: new data set
674        :raise ValueError: raised when two data sets are incompatible
[b39c817]675        """
[a7a5886]676        def operation(a, b):
677            return b/a
[f60a8c2]678        return self._perform_operation(other, operation)
[e4f421c]679
[a48842a2]680    def __or__(self, other):
681        """
682        Union a data set with another
[e4f421c]683
[a48842a2]684        :param other: data set to be unified
685        :return: new data set
686        :raise ValueError: raised when two data sets are incompatible
687        """
688        return self._perform_union(other)
[e4f421c]689
[a48842a2]690    def __ror__(self, other):
691        """
692        Union a data set with another
[e4f421c]693
[a48842a2]694        :param other: data set to be unified
695        :return: new data set
696        :raise ValueError: raised when two data sets are incompatible
697        """
698        return self._perform_union(other)
[e4f421c]699
[a3084ada]700class Data1D(plottable_1D, DataInfo):
701    """
[0997158f]702    1D data class
[a3084ada]703    """
[20522e1]704    def __init__(self, x=None, y=None, dx=None, dy=None, lam=None, dlam=None, isSesans=None):
[b99ac227]705        DataInfo.__init__(self)
[a9f579c]706        plottable_1D.__init__(self, x, y, dx, dy,None, None, lam, dlam)
[20522e1]707        self.isSesans = isSesans
708        try:
709            if self.isSesans: # the data is SESANS
[18501795]710                self.x_unit = 'A'
711                self.y_unit = 'pol'
[20522e1]712            elif not self.isSesans: # the data is SANS
[18501795]713                self.x_unit = '1/A'
714                self.y_unit = '1/cm'
[20522e1]715        except: # the data is not recognized/supported, and the user is notified
716            raise(TypeError, 'data not recognized, check documentation for supported 1D data formats')
[e4f421c]717
[a3084ada]718    def __str__(self):
719        """
[0997158f]720        Nice printout
[a3084ada]721        """
[e4f421c]722        _str = "%s\n" % DataInfo.__str__(self)
[a3084ada]723        _str += "Data:\n"
724        _str += "   Type:         %s\n" % self.__class__.__name__
725        _str += "   X-axis:       %s\t[%s]\n" % (self._xaxis, self._xunit)
726        _str += "   Y-axis:       %s\t[%s]\n" % (self._yaxis, self._yunit)
727        _str += "   Length:       %g\n" % len(self.x)
728        return _str
729
[4026380]730    def is_slit_smeared(self):
731        """
[0997158f]732        Check whether the data has slit smearing information
733        :return: True is slit smearing info is present, False otherwise
[4026380]734        """
[f60a8c2]735        def _check(v):
[9a5097c]736            if (v.__class__ == list or v.__class__ == np.ndarray) \
[a7a5886]737                and len(v) > 0 and min(v) > 0:
[4026380]738                return True
739            return False
740        return _check(self.dxl) or _check(self.dxw)
[e4f421c]741
[7d8094b]742    def clone_without_data(self, length=0, clone=None):
[b39c817]743        """
[0997158f]744        Clone the current object, without copying the data (which
745        will be filled out by a subsequent operation).
746        The data arrays will be initialized to zero.
[e4f421c]747
[0997158f]748        :param length: length of the data array to be initialized
749        :param clone: if provided, the data will be copied to clone
[b39c817]750        """
[9198b83]751        from copy import deepcopy
[e4f421c]752
[7d8094b]753        if clone is None or not issubclass(clone.__class__, Data1D):
[9a5097c]754            x = np.zeros(length)
755            dx = np.zeros(length)
756            y = np.zeros(length)
757            dy = np.zeros(length)
758            lam = np.zeros(length)
759            dlam = np.zeros(length)
[a9f579c]760            clone = Data1D(x, y, lam=lam, dx=dx, dy=dy, dlam=dlam)
[e4f421c]761
762        clone.title = self.title
763        clone.run = self.run
764        clone.filename = self.filename
765        clone.instrument = self.instrument
766        clone.notes = deepcopy(self.notes)
767        clone.process = deepcopy(self.process)
768        clone.detector = deepcopy(self.detector)
769        clone.sample = deepcopy(self.sample)
770        clone.source = deepcopy(self.source)
771        clone.collimation = deepcopy(self.collimation)
[76cd1ae]772        clone.trans_spectrum = deepcopy(self.trans_spectrum)
[e4f421c]773        clone.meta_data = deepcopy(self.meta_data)
774        clone.errors = deepcopy(self.errors)
775
[9198b83]776        return clone
777
778    def _validity_check(self, other):
779        """
[0997158f]780        Checks that the data lengths are compatible.
781        Checks that the x vectors are compatible.
782        Returns errors vectors equal to original
783        errors vectors if they were present or vectors
784        of zeros when none was found.
[e4f421c]785
[0997158f]786        :param other: other data set for operation
787        :return: dy for self, dy for other [numpy arrays]
788        :raise ValueError: when lengths are not compatible
[9198b83]789        """
790        dy_other = None
791        if isinstance(other, Data1D):
792            # Check that data lengths are the same
793            if len(self.x) != len(other.x) or \
794                len(self.y) != len(other.y):
[f60a8c2]795                msg = "Unable to perform operation: data length are not equal"
[a7a5886]796                raise ValueError, msg
[9198b83]797            # Here we could also extrapolate between data points
[1b1a1c1]798            TOLERANCE = 0.01
[9198b83]799            for i in range(len(self.x)):
[1b1a1c1]800                if math.fabs((self.x[i] - other.x[i])/self.x[i]) > TOLERANCE:
[a7a5886]801                    msg = "Incompatible data sets: x-values do not match"
802                    raise ValueError, msg
[e4f421c]803
[9198b83]804            # Check that the other data set has errors, otherwise
805            # create zero vector
806            dy_other = other.dy
[a7a5886]807            if other.dy == None or (len(other.dy) != len(other.y)):
[9a5097c]808                dy_other = np.zeros(len(other.y))
[e4f421c]809
[9198b83]810        # Check that we have errors, otherwise create zero vector
811        dy = self.dy
[a7a5886]812        if self.dy == None or (len(self.dy) != len(self.y)):
[9a5097c]813            dy = np.zeros(len(self.y))
[e4f421c]814
[9198b83]815        return dy, dy_other
[a3084ada]816
[9198b83]817    def _perform_operation(self, other, operation):
818        """
819        """
820        # First, check the data compatibility
821        dy, dy_other = self._validity_check(other)
822        result = self.clone_without_data(len(self.x))
[e2605a5]823        if self.dxw == None:
824            result.dxw = None
825        else:
[9a5097c]826            result.dxw = np.zeros(len(self.x))
[e2605a5]827        if self.dxl == None:
828            result.dxl = None
829        else:
[9a5097c]830            result.dxl = np.zeros(len(self.x))
[e2605a5]831
[9198b83]832        for i in range(len(self.x)):
833            result.x[i] = self.x[i]
[a7a5886]834            if self.dx is not None and len(self.x) == len(self.dx):
[9198b83]835                result.dx[i] = self.dx[i]
[e2605a5]836            if self.dxw is not None and len(self.x) == len(self.dxw):
837                result.dxw[i] = self.dxw[i]
838            if self.dxl is not None and len(self.x) == len(self.dxl):
839                result.dxl[i] = self.dxl[i]
[e4f421c]840
[9198b83]841            a = Uncertainty(self.y[i], dy[i]**2)
842            if isinstance(other, Data1D):
843                b = Uncertainty(other.y[i], dy_other[i]**2)
[e2605a5]844                if other.dx is not None:
845                    result.dx[i] *= self.dx[i]
846                    result.dx[i] += (other.dx[i]**2)
847                    result.dx[i] /= 2
848                    result.dx[i] = math.sqrt(result.dx[i])
849                if result.dxl is not None and other.dxl is not None:
850                    result.dxl[i] *= self.dxl[i]
[a48842a2]851                    result.dxl[i] += (other.dxl[i]**2)
[e2605a5]852                    result.dxl[i] /= 2
853                    result.dxl[i] = math.sqrt(result.dxl[i])
[9198b83]854            else:
855                b = other
[e4f421c]856
[9198b83]857            output = operation(a, b)
858            result.y[i] = output.x
859            result.dy[i] = math.sqrt(math.fabs(output.variance))
860        return result
[e4f421c]861
[a48842a2]862    def _validity_check_union(self, other):
863        """
864        Checks that the data lengths are compatible.
865        Checks that the x vectors are compatible.
866        Returns errors vectors equal to original
867        errors vectors if they were present or vectors
868        of zeros when none was found.
[e4f421c]869
[a48842a2]870        :param other: other data set for operation
871        :return: bool
872        :raise ValueError: when data types are not compatible
873        """
874        if not isinstance(other, Data1D):
875            msg = "Unable to perform operation: different types of data set"
[e4f421c]876            raise ValueError, msg
[a48842a2]877        return True
878
879    def _perform_union(self, other):
880        """
881        """
882        # First, check the data compatibility
883        self._validity_check_union(other)
884        result = self.clone_without_data(len(self.x) + len(other.x))
885        if self.dy == None or other.dy is None:
886            result.dy = None
887        else:
[9a5097c]888            result.dy = np.zeros(len(self.x) + len(other.x))
[a48842a2]889        if self.dx == None or other.dx is None:
890            result.dx = None
891        else:
[9a5097c]892            result.dx = np.zeros(len(self.x) + len(other.x))
[a48842a2]893        if self.dxw == None or other.dxw is None:
894            result.dxw = None
895        else:
[9a5097c]896            result.dxw = np.zeros(len(self.x) + len(other.x))
[a48842a2]897        if self.dxl == None or other.dxl is None:
898            result.dxl = None
899        else:
[9a5097c]900            result.dxl = np.zeros(len(self.x) + len(other.x))
[a48842a2]901
[9a5097c]902        result.x = np.append(self.x, other.x)
[a48842a2]903        #argsorting
[9a5097c]904        ind = np.argsort(result.x)
[a48842a2]905        result.x = result.x[ind]
[9a5097c]906        result.y = np.append(self.y, other.y)
[a48842a2]907        result.y = result.y[ind]
908        if result.dy != None:
[9a5097c]909            result.dy = np.append(self.dy, other.dy)
[a48842a2]910            result.dy = result.dy[ind]
911        if result.dx is not None:
[9a5097c]912            result.dx = np.append(self.dx, other.dx)
[a48842a2]913            result.dx = result.dx[ind]
914        if result.dxw is not None:
[9a5097c]915            result.dxw = np.append(self.dxw, other.dxw)
[a48842a2]916            result.dxw = result.dxw[ind]
917        if result.dxl is not None:
[9a5097c]918            result.dxl = np.append(self.dxl, other.dxl)
[a48842a2]919            result.dxl = result.dxl[ind]
920        return result
[e4f421c]921
922
[7eaf9f2]923class Data2D(plottable_2D, DataInfo):
[99d1af6]924    """
[0997158f]925    2D data class
[99d1af6]926    """
927    ## Units for Q-values
[ca10d8e]928    Q_unit = '1/A'
[99d1af6]929    ## Units for I(Q) values
[ca10d8e]930    I_unit = '1/cm'
[99d1af6]931    ## Vector of Q-values at the center of each bin in x
[d6513cd]932    x_bins = None
[99d1af6]933    ## Vector of Q-values at the center of each bin in y
[d6513cd]934    y_bins = None
[2ffe241]935    ## No 2D SESANS data as of yet. Always set it to False
936    isSesans = False
[e4f421c]937
[a7a5886]938    def __init__(self, data=None, err_data=None, qx_data=None,
[f60a8c2]939                 qy_data=None, q_data=None, mask=None,
[2ffe241]940                 dqx_data=None, dqy_data=None):
[b99ac227]941        DataInfo.__init__(self)
[a7a5886]942        plottable_2D.__init__(self, data, err_data, qx_data,
[f60a8c2]943                              qy_data, q_data, mask, dqx_data, dqy_data)
[20522e1]944        self.y_bins = []
945        self.x_bins = []
946
[a7a5886]947        if len(self.detector) > 0:
[b99ac227]948            raise RuntimeError, "Data2D: Detector bank already filled at init"
[99d1af6]949
950    def __str__(self):
[f60a8c2]951        _str = "%s\n" % DataInfo.__str__(self)
[99d1af6]952        _str += "Data:\n"
953        _str += "   Type:         %s\n" % self.__class__.__name__
954        _str += "   X- & Y-axis:  %s\t[%s]\n" % (self._yaxis, self._yunit)
955        _str += "   Z-axis:       %s\t[%s]\n" % (self._zaxis, self._zunit)
[3cd95c8]956        _str += "   Length:       %g \n" % (len(self.data))
[45d7662]957        _str += "   Shape:        (%d, %d)\n" % (len(self.y_bins), len(self.x_bins))
[99d1af6]958        return _str
[e4f421c]959
[7d8094b]960    def clone_without_data(self, length=0, clone=None):
[442f42f]961        """
[0997158f]962        Clone the current object, without copying the data (which
963        will be filled out by a subsequent operation).
964        The data arrays will be initialized to zero.
[e4f421c]965
[0997158f]966        :param length: length of the data array to be initialized
967        :param clone: if provided, the data will be copied to clone
[442f42f]968        """
969        from copy import deepcopy
[e4f421c]970
[f60a8c2]971        if clone is None or not issubclass(clone.__class__, Data2D):
[9a5097c]972            data = np.zeros(length)
973            err_data = np.zeros(length)
974            qx_data = np.zeros(length)
975            qy_data = np.zeros(length)
976            q_data = np.zeros(length)
977            mask = np.zeros(length)
[3cd95c8]978            dqx_data = None
979            dqy_data = None
[e4f421c]980            clone = Data2D(data=data, err_data=err_data,
981                           qx_data=qx_data, qy_data=qy_data,
[0008f54]982                           q_data=q_data, mask=mask)
[3cd95c8]983
[e4f421c]984        clone.title = self.title
985        clone.run = self.run
986        clone.filename = self.filename
987        clone.instrument = self.instrument
988        clone.notes = deepcopy(self.notes)
989        clone.process = deepcopy(self.process)
990        clone.detector = deepcopy(self.detector)
991        clone.sample = deepcopy(self.sample)
992        clone.source = deepcopy(self.source)
[f60a8c2]993        clone.collimation = deepcopy(self.collimation)
[d72567e]994        clone.trans_spectrum = deepcopy(self.trans_spectrum)
[e4f421c]995        clone.meta_data = deepcopy(self.meta_data)
996        clone.errors = deepcopy(self.errors)
997
[442f42f]998        return clone
[e4f421c]999
[442f42f]1000    def _validity_check(self, other):
1001        """
[0997158f]1002        Checks that the data lengths are compatible.
1003        Checks that the x vectors are compatible.
1004        Returns errors vectors equal to original
1005        errors vectors if they were present or vectors
1006        of zeros when none was found.
[e4f421c]1007
[0997158f]1008        :param other: other data set for operation
1009        :return: dy for self, dy for other [numpy arrays]
1010        :raise ValueError: when lengths are not compatible
[442f42f]1011        """
1012        err_other = None
[1b1a1c1]1013        TOLERANCE = 0.01
[442f42f]1014        if isinstance(other, Data2D):
1015            # Check that data lengths are the same
[dcf73a4]1016            if len(self.data) != len(other.data) or \
1017                len(self.qx_data) != len(other.qx_data) or \
1018                len(self.qy_data) != len(other.qy_data):
[a7a5886]1019                msg = "Unable to perform operation: data length are not equal"
1020                raise ValueError, msg
[dcf73a4]1021            for ind in range(len(self.data)):
[1b1a1c1]1022                if math.fabs((self.qx_data[ind] - other.qx_data[ind])/self.qx_data[ind]) > TOLERANCE:
1023                    msg = "Incompatible data sets: qx-values do not match: %s %s" % (self.qx_data[ind], other.qx_data[ind])
[dcf73a4]1024                    raise ValueError, msg
[1b1a1c1]1025                if math.fabs((self.qy_data[ind] - other.qy_data[ind])/self.qy_data[ind]) > TOLERANCE:
1026                    msg = "Incompatible data sets: qy-values do not match: %s %s" % (self.qy_data[ind], other.qy_data[ind])
[dcf73a4]1027                    raise ValueError, msg
[e4f421c]1028
[442f42f]1029            # Check that the scales match
1030            err_other = other.err_data
[a7a5886]1031            if other.err_data == None or \
[dcf73a4]1032                (len(other.err_data) != len(other.data)):
[9a5097c]1033                err_other = np.zeros(len(other.data))
[e4f421c]1034
[442f42f]1035        # Check that we have errors, otherwise create zero vector
1036        err = self.err_data
[a7a5886]1037        if self.err_data == None or \
[dcf73a4]1038            (len(self.err_data) != len(self.data)):
[9a5097c]1039            err = np.zeros(len(other.data))
[442f42f]1040        return err, err_other
[e4f421c]1041
[442f42f]1042    def _perform_operation(self, other, operation):
1043        """
[0997158f]1044        Perform 2D operations between data sets
[e4f421c]1045
[0997158f]1046        :param other: other data set
1047        :param operation: function defining the operation
[442f42f]1048        """
1049        # First, check the data compatibility
1050        dy, dy_other = self._validity_check(other)
[9a5097c]1051        result = self.clone_without_data(np.size(self.data))
[e2605a5]1052        if self.dqx_data == None or self.dqy_data == None:
1053            result.dqx_data = None
1054            result.dqy_data = None
1055        else:
[9a5097c]1056            result.dqx_data = np.zeros(len(self.data))
1057            result.dqy_data = np.zeros(len(self.data))
1058        for i in range(np.size(self.data)):
[cdeed9f]1059            result.data[i] = self.data[i]
1060            if self.err_data is not None and \
[9a5097c]1061                            np.size(self.data) == np.size(self.err_data):
[e4f421c]1062                result.err_data[i] = self.err_data[i]
[cdeed9f]1063            if self.dqx_data is not None:
1064                result.dqx_data[i] = self.dqx_data[i]
1065            if self.dqy_data is not None:
1066                result.dqy_data[i] = self.dqy_data[i]
1067            result.qx_data[i] = self.qx_data[i]
1068            result.qy_data[i] = self.qy_data[i]
1069            result.q_data[i] = self.q_data[i]
1070            result.mask[i] = self.mask[i]
[e4f421c]1071
[e2605a5]1072            a = Uncertainty(self.data[i], dy[i]**2)
1073            if isinstance(other, Data2D):
1074                b = Uncertainty(other.data[i], dy_other[i]**2)
1075                if other.dqx_data is not None and \
1076                        result.dqx_data is not None:
1077                    result.dqx_data[i] *= self.dqx_data[i]
1078                    result.dqx_data[i] += (other.dqx_data[i]**2)
1079                    result.dqx_data[i] /= 2
[e4f421c]1080                    result.dqx_data[i] = math.sqrt(result.dqx_data[i])
[e2605a5]1081                if other.dqy_data is not None and \
1082                        result.dqy_data is not None:
1083                    result.dqy_data[i] *= self.dqy_data[i]
1084                    result.dqy_data[i] += (other.dqy_data[i]**2)
1085                    result.dqy_data[i] /= 2
1086                    result.dqy_data[i] = math.sqrt(result.dqy_data[i])
1087            else:
1088                b = other
1089            output = operation(a, b)
1090            result.data[i] = output.x
1091            result.err_data[i] = math.sqrt(math.fabs(output.variance))
[442f42f]1092        return result
[e4f421c]1093
[a48842a2]1094    def _validity_check_union(self, other):
1095        """
1096        Checks that the data lengths are compatible.
1097        Checks that the x vectors are compatible.
1098        Returns errors vectors equal to original
1099        errors vectors if they were present or vectors
1100        of zeros when none was found.
[e4f421c]1101
[a48842a2]1102        :param other: other data set for operation
1103        :return: bool
1104        :raise ValueError: when data types are not compatible
1105        """
1106        if not isinstance(other, Data2D):
1107            msg = "Unable to perform operation: different types of data set"
[e4f421c]1108            raise ValueError, msg
[a48842a2]1109        return True
[e4f421c]1110
[a48842a2]1111    def _perform_union(self, other):
1112        """
1113        Perform 2D operations between data sets
[e4f421c]1114
[a48842a2]1115        :param other: other data set
1116        :param operation: function defining the operation
1117        """
1118        # First, check the data compatibility
1119        self._validity_check_union(other)
[9a5097c]1120        result = self.clone_without_data(np.size(self.data) + \
1121                                         np.size(other.data))
[a48842a2]1122        result.xmin = self.xmin
1123        result.xmax = self.xmax
1124        result.ymin = self.ymin
1125        result.ymax = self.ymax
1126        if self.dqx_data == None or self.dqy_data == None or \
[e4f421c]1127                other.dqx_data == None or other.dqy_data == None:
[a48842a2]1128            result.dqx_data = None
1129            result.dqy_data = None
1130        else:
[9a5097c]1131            result.dqx_data = np.zeros(len(self.data) + \
1132                                       np.size(other.data))
1133            result.dqy_data = np.zeros(len(self.data) + \
1134                                       np.size(other.data))
1135
1136        result.data = np.append(self.data, other.data)
1137        result.qx_data = np.append(self.qx_data, other.qx_data)
1138        result.qy_data = np.append(self.qy_data, other.qy_data)
1139        result.q_data = np.append(self.q_data, other.q_data)
1140        result.mask = np.append(self.mask, other.mask)
[a48842a2]1141        if result.err_data is not None:
[9a5097c]1142            result.err_data = np.append(self.err_data, other.err_data)
[a48842a2]1143        if self.dqx_data is not None:
[9a5097c]1144            result.dqx_data = np.append(self.dqx_data, other.dqx_data)
[a48842a2]1145        if self.dqy_data is not None:
[9a5097c]1146            result.dqy_data = np.append(self.dqy_data, other.dqy_data)
[a48842a2]1147
1148        return result
[d72567e]1149
1150
1151def combine_data_info_with_plottable(data, datainfo):
1152    """
1153    A function that combines the DataInfo data in self.current_datainto with a plottable_1D or 2D data object.
1154
1155    :param data: A plottable_1D or plottable_2D data object
1156    :return: A fully specified Data1D or Data2D object
1157    """
1158
1159    final_dataset = None
1160    if isinstance(data, plottable_1D):
1161        final_dataset = Data1D(data.x, data.y)
1162        final_dataset.dx = data.dx
1163        final_dataset.dy = data.dy
1164        final_dataset.dxl = data.dxl
1165        final_dataset.dxw = data.dxw
1166        final_dataset.xaxis(data._xaxis, data._xunit)
1167        final_dataset.yaxis(data._yaxis, data._yunit)
1168    elif isinstance(data, plottable_2D):
1169        final_dataset = Data2D(data.data, data.err_data, data.qx_data, data.qy_data, data.q_data,
1170                               data.mask, data.dqx_data, data.dqy_data)
1171        final_dataset.xaxis(data._xaxis, data._xunit)
1172        final_dataset.yaxis(data._yaxis, data._yunit)
1173        final_dataset.zaxis(data._zaxis, data._zunit)
[a4deca6]1174        final_dataset.x_bins = data.x_bins
1175        final_dataset.y_bins = data.y_bins
[d72567e]1176    else:
1177        return_string = "Should Never Happen: _combine_data_info_with_plottable input is not a plottable1d or " + \
1178                        "plottable2d data object"
1179        return return_string
1180
1181    final_dataset.xmax = data.xmax
1182    final_dataset.ymax = data.ymax
1183    final_dataset.xmin = data.xmin
1184    final_dataset.ymin = data.ymin
[ad4632c]1185    final_dataset.isSesans = datainfo.isSesans
[d72567e]1186    final_dataset.title = datainfo.title
1187    final_dataset.run = datainfo.run
1188    final_dataset.run_name = datainfo.run_name
1189    final_dataset.filename = datainfo.filename
1190    final_dataset.notes = datainfo.notes
1191    final_dataset.process = datainfo.process
1192    final_dataset.instrument = datainfo.instrument
1193    final_dataset.detector = datainfo.detector
1194    final_dataset.sample = datainfo.sample
1195    final_dataset.source = datainfo.source
1196    final_dataset.collimation = datainfo.collimation
1197    final_dataset.trans_spectrum = datainfo.trans_spectrum
1198    final_dataset.meta_data = datainfo.meta_data
1199    final_dataset.errors = datainfo.errors
[a4deca6]1200    return final_dataset
Note: See TracBrowser for help on using the repository browser.