source: sasview/DataLoader/data_info.py @ a96d246

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

dataloader: allow for input to write to be child class. Modify clone_without_data to receive a Data1D (or child of Data1D) as an input.

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