source: sasview/DataLoader/data_info.py @ 5859862

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 5859862 was fe78c7b, checked in by Mathieu Doucet <doucetm@…>, 15 years ago

DataLoader?: exception no longer raised when units are wrong for an entry; the entry is not loaded and an error is logged instead. Loader info is now stored.

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