source: sasview/src/sas/pr/invertor.py @ bc3e38c

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

The way the Invertor interacts with its C component should be cleaned up

  • Property mode set to 100644
File size: 25.1 KB
Line 
1# pylint: disable=invalid-name
2"""
3Module to perform P(r) inversion.
4The module contains the Invertor class.
5
6FIXME: The way the Invertor interacts with its C component should be cleaned up
7"""
8
9import numpy
10import sys
11import math
12import time
13import copy
14import os
15import re
16import logging
17from numpy.linalg import lstsq
18from scipy import optimize
19from sas.pr.core.pr_inversion import Cinvertor
20
21def help():
22    """
23    Provide general online help text
24    Future work: extend this function to allow topic selection
25    """
26    info_txt = "The inversion approach is based on Moore, J. Appl. Cryst. "
27    info_txt += "(1980) 13, 168-175.\n\n"
28    info_txt += "P(r) is set to be equal to an expansion of base functions "
29    info_txt += "of the type "
30    info_txt += "phi_n(r) = 2*r*sin(pi*n*r/D_max). The coefficient of each "
31    info_txt += "base functions "
32    info_txt += "in the expansion is found by performing a least square fit "
33    info_txt += "with the "
34    info_txt += "following fit function:\n\n"
35    info_txt += "chi**2 = sum_i[ I_meas(q_i) - I_th(q_i) ]**2/error**2 +"
36    info_txt += "Reg_term\n\n"
37    info_txt += "where I_meas(q) is the measured scattering intensity and "
38    info_txt += "I_th(q) is "
39    info_txt += "the prediction from the Fourier transform of the P(r) "
40    info_txt += "expansion. "
41    info_txt += "The Reg_term term is a regularization term set to the second"
42    info_txt += " derivative "
43    info_txt += "d**2P(r)/dr**2 integrated over r. It is used to produce "
44    info_txt += "a smooth P(r) output.\n\n"
45    info_txt += "The following are user inputs:\n\n"
46    info_txt += "   - Number of terms: the number of base functions in the P(r)"
47    info_txt += " expansion.\n\n"
48    info_txt += "   - Regularization constant: a multiplicative constant "
49    info_txt += "to set the size of "
50    info_txt += "the regularization term.\n\n"
51    info_txt += "   - Maximum distance: the maximum distance between any "
52    info_txt += "two points in the system.\n"
53
54    return info_txt
55
56
57class Invertor(Cinvertor):
58    """
59    Invertor class to perform P(r) inversion
60
61    The problem is solved by posing the problem as  Ax = b,
62    where x is the set of coefficients we are looking for.
63
64    Npts is the number of points.
65
66    In the following i refers to the ith base function coefficient.
67    The matrix has its entries j in its first Npts rows set to ::
68
69        A[j][i] = (Fourier transformed base function for point j)
70
71    We them choose a number of r-points, n_r, to evaluate the second
72    derivative of P(r) at. This is used as our regularization term.
73    For a vector r of length n_r, the following n_r rows are set to ::
74
75        A[j+Npts][i] = (2nd derivative of P(r), d**2(P(r))/d(r)**2,
76        evaluated at r[j])
77
78    The vector b has its first Npts entries set to ::
79
80        b[j] = (I(q) observed for point j)
81
82    The following n_r entries are set to zero.
83
84    The result is found by using scipy.linalg.basic.lstsq to invert
85    the matrix and find the coefficients x.
86
87    Methods inherited from Cinvertor:
88
89    * ``get_peaks(pars)``: returns the number of P(r) peaks
90    * ``oscillations(pars)``: returns the oscillation parameters for the output P(r)
91    * ``get_positive(pars)``: returns the fraction of P(r) that is above zero
92    * ``get_pos_err(pars)``: returns the fraction of P(r) that is 1-sigma above zero
93    """
94    ## Chisqr of the last computation
95    chi2 = 0
96    ## Time elapsed for last computation
97    elapsed = 0
98    ## Alpha to get the reg term the same size as the signal
99    suggested_alpha = 0
100    alpha = 0
101    ## Last number of base functions used
102    nfunc = 10
103    ## Last output values
104    out = None
105    ## Last errors on output values
106    cov = None
107    ## Background value
108    background = 0
109    ## Information dictionary for application use
110    info = {}
111
112    def __init__(self):
113        Cinvertor.__init__(self)
114
115    def __setstate__(self, state):
116        """
117        restore the state of invertor for pickle
118        """
119        (self.__dict__, self.alpha, self.d_max,
120         self.q_min, self.q_max,
121         self.x, self.y,
122         self.err, self.has_bck,
123         self.slit_height, self.slit_width) = state
124
125    def __reduce_ex__(self, proto):
126        """
127        Overwrite the __reduce_ex__
128        """
129
130        state = (self.__dict__,
131                 self.alpha, self.d_max,
132                 self.q_min, self.q_max,
133                 self.x, self.y,
134                 self.err, self.has_bck,
135                 self.slit_height, self.slit_width,
136                )
137        return (Invertor, tuple(), state, None, None)
138
139    def __setattr__(self, name, value):
140        """
141        Set the value of an attribute.
142        Access the parent class methods for
143        x, y, err, d_max, q_min, q_max and alpha
144        """
145        if   name == 'x':
146            if 0.0 in value:
147                msg = "Invertor: one of your q-values is zero. "
148                msg += "Delete that entry before proceeding"
149                raise ValueError, msg
150            return self.set_x(value)
151        elif name == 'y':
152            return self.set_y(value)
153        elif name == 'err':
154            value2 = abs(value)
155            return self.set_err(value2)
156        elif name == 'd_max':
157            return self.set_dmax(value)
158        elif name == 'q_min':
159            if value == None:
160                return self.set_qmin(-1.0)
161            return self.set_qmin(value)
162        elif name == 'q_max':
163            if value == None:
164                return self.set_qmax(-1.0)
165            return self.set_qmax(value)
166        elif name == 'alpha':
167            return self.set_alpha(value)
168        elif name == 'slit_height':
169            return self.set_slit_height(value)
170        elif name == 'slit_width':
171            return self.set_slit_width(value)
172        elif name == 'has_bck':
173            if value == True:
174                return self.set_has_bck(1)
175            elif value == False:
176                return self.set_has_bck(0)
177            else:
178                raise ValueError, "Invertor: has_bck can only be True or False"
179
180        return Cinvertor.__setattr__(self, name, value)
181
182    def __getattr__(self, name):
183        """
184        Return the value of an attribute
185        """
186        #import numpy
187        if name == 'x':
188            out = numpy.ones(self.get_nx())
189            self.get_x(out)
190            return out
191        elif name == 'y':
192            out = numpy.ones(self.get_ny())
193            self.get_y(out)
194            return out
195        elif name == 'err':
196            out = numpy.ones(self.get_nerr())
197            self.get_err(out)
198            return out
199        elif name == 'd_max':
200            return self.get_dmax()
201        elif name == 'q_min':
202            qmin = self.get_qmin()
203            if qmin < 0:
204                return None
205            return qmin
206        elif name == 'q_max':
207            qmax = self.get_qmax()
208            if qmax < 0:
209                return None
210            return qmax
211        elif name == 'alpha':
212            return self.get_alpha()
213        elif name == 'slit_height':
214            return self.get_slit_height()
215        elif name == 'slit_width':
216            return self.get_slit_width()
217        elif name == 'has_bck':
218            value = self.get_has_bck()
219            if value == 1:
220                return True
221            else:
222                return False
223        elif name in self.__dict__:
224            return self.__dict__[name]
225        return None
226
227    def clone(self):
228        """
229        Return a clone of this instance
230        """
231        #import copy
232
233        invertor = Invertor()
234        invertor.chi2 = self.chi2
235        invertor.elapsed = self.elapsed
236        invertor.nfunc = self.nfunc
237        invertor.alpha = self.alpha
238        invertor.d_max = self.d_max
239        invertor.q_min = self.q_min
240        invertor.q_max = self.q_max
241
242        invertor.x = self.x
243        invertor.y = self.y
244        invertor.err = self.err
245        invertor.has_bck = self.has_bck
246        invertor.slit_height = self.slit_height
247        invertor.slit_width = self.slit_width
248
249        invertor.info = copy.deepcopy(self.info)
250
251        return invertor
252
253    def invert(self, nfunc=10, nr=20):
254        """
255        Perform inversion to P(r)
256
257        The problem is solved by posing the problem as  Ax = b,
258        where x is the set of coefficients we are looking for.
259
260        Npts is the number of points.
261
262        In the following i refers to the ith base function coefficient.
263        The matrix has its entries j in its first Npts rows set to ::
264
265            A[i][j] = (Fourier transformed base function for point j)
266
267        We them choose a number of r-points, n_r, to evaluate the second
268        derivative of P(r) at. This is used as our regularization term.
269        For a vector r of length n_r, the following n_r rows are set to ::
270
271            A[i+Npts][j] = (2nd derivative of P(r), d**2(P(r))/d(r)**2, evaluated at r[j])
272
273        The vector b has its first Npts entries set to ::
274
275            b[j] = (I(q) observed for point j)
276
277        The following n_r entries are set to zero.
278
279        The result is found by using scipy.linalg.basic.lstsq to invert
280        the matrix and find the coefficients x.
281
282        :param nfunc: number of base functions to use.
283        :param nr: number of r points to evaluate the 2nd derivative at for the reg. term.
284        :return: c_out, c_cov - the coefficients with covariance matrix
285        """
286        # Reset the background value before proceeding
287        self.background = 0.0
288        return self.lstsq(nfunc, nr=nr)
289
290    def iq(self, out, q):
291        """
292        Function to call to evaluate the scattering intensity
293
294        :param args: c-parameters, and q
295        :return: I(q)
296
297        """
298        return Cinvertor.iq(self, out, q) + self.background
299
300    def invert_optimize(self, nfunc=10, nr=20):
301        """
302        Slower version of the P(r) inversion that uses scipy.optimize.leastsq.
303
304        This probably produce more reliable results, but is much slower.
305        The minimization function is set to
306        sum_i[ (I_obs(q_i) - I_theo(q_i))/err**2 ] + alpha * reg_term,
307        where the reg_term is given by Svergun: it is the integral of
308        the square of the first derivative
309        of P(r), d(P(r))/dr, integrated over the full range of r.
310
311        :param nfunc: number of base functions to use.
312        :param nr: number of r points to evaluate the 2nd derivative at
313            for the reg. term.
314
315        :return: c_out, c_cov - the coefficients with covariance matrix
316
317        """
318        self.nfunc = nfunc
319        # First, check that the current data is valid
320        if self.is_valid() <= 0:
321            msg = "Invertor.invert: Data array are of different length"
322            raise RuntimeError, msg
323
324        p = numpy.ones(nfunc)
325        t_0 = time.time()
326        out, cov_x, _, _, _ = optimize.leastsq(self.residuals, p, full_output=1)
327
328        # Compute chi^2
329        res = self.residuals(out)
330        chisqr = 0
331        for i in range(len(res)):
332            chisqr += res[i]
333
334        self.chi2 = chisqr
335
336        # Store computation time
337        self.elapsed = time.time() - t_0
338
339        if cov_x is None:
340            cov_x = numpy.ones([nfunc, nfunc])
341            cov_x *= math.fabs(chisqr)
342        return out, cov_x
343
344    def pr_fit(self, nfunc=5):
345        """
346        This is a direct fit to a given P(r). It assumes that the y data
347        is set to some P(r) distribution that we are trying to reproduce
348        with a set of base functions.
349
350        This method is provided as a test.
351        """
352        # First, check that the current data is valid
353        if self.is_valid() <= 0:
354            msg = "Invertor.invert: Data arrays are of different length"
355            raise RuntimeError, msg
356
357        p = numpy.ones(nfunc)
358        t_0 = time.time()
359        out, cov_x, _, _, _ = optimize.leastsq(self.pr_residuals, p, full_output=1)
360
361        # Compute chi^2
362        res = self.pr_residuals(out)
363        chisqr = 0
364        for i in range(len(res)):
365            chisqr += res[i]
366
367        self.chisqr = chisqr
368
369        # Store computation time
370        self.elapsed = time.time() - t_0
371
372        return out, cov_x
373
374    def pr_err(self, c, c_cov, r):
375        """
376        Returns the value of P(r) for a given r, and base function
377        coefficients, with error.
378
379        :param c: base function coefficients
380        :param c_cov: covariance matrice of the base function coefficients
381        :param r: r-value to evaluate P(r) at
382
383        :return: P(r)
384
385        """
386        return self.get_pr_err(c, c_cov, r)
387
388    def _accept_q(self, q):
389        """
390        Check q-value against user-defined range
391        """
392        if not self.q_min == None and q < self.q_min:
393            return False
394        if not self.q_max == None and q > self.q_max:
395            return False
396        return True
397
398    def lstsq(self, nfunc=5, nr=20):
399        """
400        The problem is solved by posing the problem as  Ax = b,
401        where x is the set of coefficients we are looking for.
402
403        Npts is the number of points.
404
405        In the following i refers to the ith base function coefficient.
406        The matrix has its entries j in its first Npts rows set to ::
407
408            A[i][j] = (Fourier transformed base function for point j)
409
410        We them choose a number of r-points, n_r, to evaluate the second
411        derivative of P(r) at. This is used as our regularization term.
412        For a vector r of length n_r, the following n_r rows are set to ::
413
414            A[i+Npts][j] = (2nd derivative of P(r), d**2(P(r))/d(r)**2,
415            evaluated at r[j])
416
417        The vector b has its first Npts entries set to ::
418
419            b[j] = (I(q) observed for point j)
420
421        The following n_r entries are set to zero.
422
423        The result is found by using scipy.linalg.basic.lstsq to invert
424        the matrix and find the coefficients x.
425
426        :param nfunc: number of base functions to use.
427        :param nr: number of r points to evaluate the 2nd derivative at for the reg. term.
428
429        If the result does not allow us to compute the covariance matrix,
430        a matrix filled with zeros will be returned.
431
432        """
433        # Note: To make sure an array is contiguous:
434        # blah = numpy.ascontiguousarray(blah_original)
435        # ... before passing it to C
436
437        if self.is_valid() < 0:
438            msg = "Invertor: invalid data; incompatible data lengths."
439            raise RuntimeError, msg
440
441        self.nfunc = nfunc
442        # a -- An M x N matrix.
443        # b -- An M x nrhs matrix or M vector.
444        npts = len(self.x)
445        nq = nr
446        sqrt_alpha = math.sqrt(math.fabs(self.alpha))
447        if sqrt_alpha < 0.0:
448            nq = 0
449
450        # If we need to fit the background, add a term
451        if self.has_bck == True:
452            nfunc_0 = nfunc
453            nfunc += 1
454
455        a = numpy.zeros([npts + nq, nfunc])
456        b = numpy.zeros(npts + nq)
457        err = numpy.zeros([nfunc, nfunc])
458
459        # Construct the a matrix and b vector that represent the problem
460        t_0 = time.time()
461        try:
462            self._get_matrix(nfunc, nq, a, b)
463        except:
464            raise RuntimeError, "Invertor: could not invert I(Q)\n  %s" % sys.exc_value
465
466        # Perform the inversion (least square fit)
467        c, chi2, _, _ = lstsq(a, b)
468        # Sanity check
469        try:
470            float(chi2)
471        except:
472            chi2 = -1.0
473        self.chi2 = chi2
474
475        inv_cov = numpy.zeros([nfunc, nfunc])
476        # Get the covariance matrix, defined as inv_cov = a_transposed * a
477        self._get_invcov_matrix(nfunc, nr, a, inv_cov)
478
479        # Compute the reg term size for the output
480        sum_sig, sum_reg = self._get_reg_size(nfunc, nr, a)
481
482        if math.fabs(self.alpha) > 0:
483            new_alpha = sum_sig / (sum_reg / self.alpha)
484        else:
485            new_alpha = 0.0
486        self.suggested_alpha = new_alpha
487
488        try:
489            cov = numpy.linalg.pinv(inv_cov)
490            err = math.fabs(chi2 / float(npts - nfunc)) * cov
491        except:
492            # We were not able to estimate the errors
493            # Return an empty error matrix
494            logging.error(sys.exc_value)
495
496        # Keep a copy of the last output
497        if self.has_bck == False:
498            self.background = 0
499            self.out = c
500            self.cov = err
501        else:
502            self.background = c[0]
503
504            err_0 = numpy.zeros([nfunc, nfunc])
505            c_0 = numpy.zeros(nfunc)
506
507            for i in range(nfunc_0):
508                c_0[i] = c[i + 1]
509                for j in range(nfunc_0):
510                    err_0[i][j] = err[i + 1][j + 1]
511
512            self.out = c_0
513            self.cov = err_0
514
515        # Store computation time
516        self.elapsed = time.time() - t_0
517
518        return self.out, self.cov
519
520    def estimate_numterms(self, isquit_func=None):
521        """
522        Returns a reasonable guess for the
523        number of terms
524
525        :param isquit_func:
526          reference to thread function to call to check whether the computation needs to
527          be stopped.
528
529        :return: number of terms, alpha, message
530
531        """
532        from num_term import NTermEstimator
533        estimator = NTermEstimator(self.clone())
534        try:
535            return estimator.num_terms(isquit_func)
536        except:
537            # If we fail, estimate alpha and return the default
538            # number of terms
539            best_alpha, _, _ = self.estimate_alpha(self.nfunc)
540            return self.nfunc, best_alpha, "Could not estimate number of terms"
541
542    def estimate_alpha(self, nfunc):
543        """
544        Returns a reasonable guess for the
545        regularization constant alpha
546
547        :param nfunc: number of terms to use in the expansion.
548
549        :return: alpha, message, elapsed
550
551        where alpha is the estimate for alpha,
552        message is a message for the user,
553        elapsed is the computation time
554        """
555        #import time
556        try:
557            pr = self.clone()
558
559            # T_0 for computation time
560            starttime = time.time()
561            elapsed = 0
562
563            # If the current alpha is zero, try
564            # another value
565            if pr.alpha <= 0:
566                pr.alpha = 0.0001
567
568            # Perform inversion to find the largest alpha
569            out, _ = pr.invert(nfunc)
570            elapsed = time.time() - starttime
571            initial_alpha = pr.alpha
572            initial_peaks = pr.get_peaks(out)
573
574            # Try the inversion with the estimated alpha
575            pr.alpha = pr.suggested_alpha
576            out, _ = pr.invert(nfunc)
577
578            npeaks = pr.get_peaks(out)
579            # if more than one peak to start with
580            # just return the estimate
581            if npeaks > 1:
582                #message = "Your P(r) is not smooth,
583                #please check your inversion parameters"
584                message = None
585                return pr.suggested_alpha, message, elapsed
586            else:
587
588                # Look at smaller values
589                # We assume that for the suggested alpha, we have 1 peak
590                # if not, send a message to change parameters
591                alpha = pr.suggested_alpha
592                best_alpha = pr.suggested_alpha
593                found = False
594                for i in range(10):
595                    pr.alpha = (0.33) ** (i + 1) * alpha
596                    out, _ = pr.invert(nfunc)
597
598                    peaks = pr.get_peaks(out)
599                    if peaks > 1:
600                        found = True
601                        break
602                    best_alpha = pr.alpha
603
604                # If we didn't find a turning point for alpha and
605                # the initial alpha already had only one peak,
606                # just return that
607                if not found and initial_peaks == 1 and \
608                    initial_alpha < best_alpha:
609                    best_alpha = initial_alpha
610
611                # Check whether the size makes sense
612                message = ''
613
614                if not found:
615                    message = None
616                elif best_alpha >= 0.5 * pr.suggested_alpha:
617                    # best alpha is too big, return a
618                    # reasonable value
619                    message = "The estimated alpha for your system is too "
620                    message += "large. "
621                    message += "Try increasing your maximum distance."
622
623                return best_alpha, message, elapsed
624
625        except:
626            message = "Invertor.estimate_alpha: %s" % sys.exc_value
627            return 0, message, elapsed
628
629    def to_file(self, path, npts=100):
630        """
631        Save the state to a file that will be readable
632        by SliceView.
633
634        :param path: path of the file to write
635        :param npts: number of P(r) points to be written
636
637        """
638        file = open(path, 'w')
639        file.write("#d_max=%g\n" % self.d_max)
640        file.write("#nfunc=%g\n" % self.nfunc)
641        file.write("#alpha=%g\n" % self.alpha)
642        file.write("#chi2=%g\n" % self.chi2)
643        file.write("#elapsed=%g\n" % self.elapsed)
644        file.write("#qmin=%s\n" % str(self.q_min))
645        file.write("#qmax=%s\n" % str(self.q_max))
646        file.write("#slit_height=%g\n" % self.slit_height)
647        file.write("#slit_width=%g\n" % self.slit_width)
648        file.write("#background=%g\n" % self.background)
649        if self.has_bck == True:
650            file.write("#has_bck=1\n")
651        else:
652            file.write("#has_bck=0\n")
653        file.write("#alpha_estimate=%g\n" % self.suggested_alpha)
654        if not self.out == None:
655            if len(self.out) == len(self.cov):
656                for i in range(len(self.out)):
657                    file.write("#C_%i=%s+-%s\n" % (i, str(self.out[i]),
658                                                   str(self.cov[i][i])))
659        file.write("<r>  <Pr>  <dPr>\n")
660        r = numpy.arange(0.0, self.d_max, self.d_max / npts)
661
662        for r_i in r:
663            (value, err) = self.pr_err(self.out, self.cov, r_i)
664            file.write("%g  %g  %g\n" % (r_i, value, err))
665
666        file.close()
667
668    def from_file(self, path):
669        """
670        Load the state of the Invertor from a file,
671        to be able to generate P(r) from a set of
672        parameters.
673
674        :param path: path of the file to load
675
676        """
677        #import os
678        #import re
679        if os.path.isfile(path):
680            try:
681                fd = open(path, 'r')
682
683                buff = fd.read()
684                lines = buff.split('\n')
685                for line in lines:
686                    if line.startswith('#d_max='):
687                        toks = line.split('=')
688                        self.d_max = float(toks[1])
689                    elif line.startswith('#nfunc='):
690                        toks = line.split('=')
691                        self.nfunc = int(toks[1])
692                        self.out = numpy.zeros(self.nfunc)
693                        self.cov = numpy.zeros([self.nfunc, self.nfunc])
694                    elif line.startswith('#alpha='):
695                        toks = line.split('=')
696                        self.alpha = float(toks[1])
697                    elif line.startswith('#chi2='):
698                        toks = line.split('=')
699                        self.chi2 = float(toks[1])
700                    elif line.startswith('#elapsed='):
701                        toks = line.split('=')
702                        self.elapsed = float(toks[1])
703                    elif line.startswith('#alpha_estimate='):
704                        toks = line.split('=')
705                        self.suggested_alpha = float(toks[1])
706                    elif line.startswith('#qmin='):
707                        toks = line.split('=')
708                        try:
709                            self.q_min = float(toks[1])
710                        except:
711                            self.q_min = None
712                    elif line.startswith('#qmax='):
713                        toks = line.split('=')
714                        try:
715                            self.q_max = float(toks[1])
716                        except:
717                            self.q_max = None
718                    elif line.startswith('#slit_height='):
719                        toks = line.split('=')
720                        self.slit_height = float(toks[1])
721                    elif line.startswith('#slit_width='):
722                        toks = line.split('=')
723                        self.slit_width = float(toks[1])
724                    elif line.startswith('#background='):
725                        toks = line.split('=')
726                        self.background = float(toks[1])
727                    elif line.startswith('#has_bck='):
728                        toks = line.split('=')
729                        if int(toks[1]) == 1:
730                            self.has_bck = True
731                        else:
732                            self.has_bck = False
733
734                    # Now read in the parameters
735                    elif line.startswith('#C_'):
736                        toks = line.split('=')
737                        p = re.compile('#C_([0-9]+)')
738                        m = p.search(toks[0])
739                        toks2 = toks[1].split('+-')
740                        i = int(m.group(1))
741                        self.out[i] = float(toks2[0])
742
743                        self.cov[i][i] = float(toks2[1])
744
745            except:
746                msg = "Invertor.from_file: corrupted file\n%s" % sys.exc_value
747                raise RuntimeError, msg
748        else:
749            msg = "Invertor.from_file: '%s' is not a file" % str(path)
750            raise RuntimeError, msg
Note: See TracBrowser for help on using the repository browser.