source: orange-bioinformatics/_bioinformatics/obiGeneSetSig.py @ 1772:58718fc3f736

Revision 1772:58718fc3f736, 21.3 KB checked in by markotoplak, 12 months ago (diff)

obiGeneSetSig: CORG were build a gene too large (fixed). SetSig has the option to check if the examples are exactly alike and discard the distance between them.

RevLine 
[1632]1from __future__ import absolute_import
2
3import math
4from collections import defaultdict
5
6import scipy.stats
7
[1572]8import numpy
[1632]9
10import Orange, Orange.utils, statc
11
[1707]12if __name__ == "__main__":
13    __package__ = "Orange.bio"
14
[1632]15from .obiGsea import takeClasses
16from .obiAssess import pca, PLSCall, corgs_activity_score
17from . import obiExpression, obiGene, obiGeneSets, obiGsea, stats
[1597]18
[1607]19class GeneSetTrans(object):
[1572]20
[1607]21    __new__ = Orange.utils._orange__new__(object)
22
23    def _mat_ni(self, data):
24        """ With cached gene matchers. """
25        if data.domain not in self._cache:
26            self._cache[data.domain] = mat_ni(data, self.matcher)
27        return self._cache[data.domain]
28
29    def _match_instance(self, instance, geneset, takegenes=None):
30        nm, name_ind = self._mat_ni(instance)
31        genes = [ nm.umatch(gene) for gene in geneset ]
32        if takegenes:
33            genes = [ genes[i] for i in takegenes ]
34        return nm, name_ind, genes
35
36    def _match_data(self, data, geneset, odic=False):
37        nm, name_ind = self._mat_ni(data)
38        genes = [ nm.umatch(gene) for gene in geneset ]
39        if odic:
40            to_geneset = dict(zip(genes, geneset))
41        takegenes = [ i for i,a in enumerate(genes) if a != None ]
42        genes = [ genes[i] for i in takegenes ]
43        if odic:
44            return nm, name_ind, genes, takegenes, to_geneset
45        else:
46            return nm, name_ind, genes, takegenes
47
[1691]48    def __init__(self, matcher=None, gene_sets=None, min_size=3, max_size=1000, min_part=0.1, class_values=None, cv=False):
[1607]49        self.matcher = matcher
50        self.gene_sets = gene_sets
51        self.min_size = min_size
52        self.max_size = max_size
53        self.min_part = min_part
54        self.class_values = class_values
55        self._cache = {}
[1691]56        self.cv = cv
[1607]57
58    def __call__(self, data, weight_id=None):
59
60        #selection of classes and gene sets
61        data = takeClasses(data, classValues=self.class_values)
62        nm,_ =  self._mat_ni(data)
63        gene_sets = select_genesets(nm, self.gene_sets, self.min_size, self.max_size, self.min_part)
64
65        #build a new domain
66        newfeatures = self.build_features(data, gene_sets)
67        newdomain = Orange.data.Domain(newfeatures, data.domain.class_var)
[1691]68
69        #build a data set with cross validation
70        if self.cv == False:
71            return Orange.data.Table(newdomain, data)
72        else:
73            # The domain has the transformer that is build on all samples,
74            # while the transformed data table uses cross-validation
75            # internally
[1772]76            if self.cv == True:
77                cvi = Orange.data.sample.SubsetIndicesCV(data, 5)
78            elif self.cv != False:
79                cvi = self.cv(data)
[1691]80            data_cv = [ [] for _ in range(len(data)) ]
[1772]81            for f in set(cvi):
[1691]82                learn = data.select(cvi, f, negate=True)
83                test = data.select(cvi, f)
84                lf = self.build_features(learn, gene_sets)
85                transd = Orange.data.Domain(lf, data.domain.class_var)
86                trans_test = Orange.data.Table(transd, test)
87                for ex, pos in \
88                    zip(trans_test, [ i for i,n in enumerate(cvi) if n == f ]):
89                    data_cv[pos] = ex.native(0)
90            return Orange.data.Table(newdomain, data_cv)
[1607]91
92    def build_features(self, data, gene_sets):
93        return [ self.build_feature(data, gs) for gs in gene_sets ]
94
95def normcdf(x, mi, st):
96    return 0.5*(2. - stats.erfcc((x - mi)/(st*math.sqrt(2))))
97
98class AT_edelmanParametric(object):
99
100    def __init__(self, **kwargs):
101        for a,b in kwargs.items():
102            setattr(self, a, b)
103
104    def __call__(self, nval):
105
106        if self.mi1 == None or self.mi2 == None or self.st1 == None or self.st2 == None:
107            return 0.0 
108
109        val = nval
110
111        try:
112            if val >= self.mi1:
113                p1 = 1 - normcdf(val, self.mi1, self.st1)
114            else:
115                p1 = normcdf(val, self.mi1, self.st1)
116
117            if val >= self.mi2:
118                p2 = 1 - normcdf(val, self.mi2, self.st2)
119            else:
120                p2 = normcdf(val, self.mi2, self.st2)
121
122            #print p1, p2
123            return math.log(p1/p2)
124        except:
125            #print p1, p2, "exception"
126            return 0
127
128class AT_edelmanParametricLearner(object):
129    """
130    Returns attribute transfromer for Edelman parametric measure for a
131    given attribute in the dataset.  Edelman et al, 06. Modified a bit.
132    """
133
134    def __init__(self, a=None, b=None):
135        """
136        a and b are choosen class values.
137        """
138        self.a = a
139        self.b = b
140
141    def __call__(self, i, data):
142        cv = data.domain.classVar
143        #print data.domain
144
145        if self.a == None: self.a = cv.values[0]
146        if self.b == None: self.b = cv.values[1]
147
148        def avWCVal(value):
149            return [ex[i].value for ex in data if ex[-1].value == value and not ex[i].isSpecial() ]
150
151        list1 = avWCVal(self.a)
152        list2 = avWCVal(self.b)
153
154        mi1 = mi2 = st1 = st2 = None
155
156        try:
157            mi1 = statc.mean(list1)
158            st1 = statc.std(list1)
159        except:
160            pass
161   
162        try:
163            mi2 = statc.mean(list2)
164            st2 = statc.std(list2)
165        except:
166            pass
167
168        return AT_edelmanParametric(mi1=mi1, mi2=mi2, st1=st1, st2=st2)
169
170class AT_loess(object):
171
172    def __init__(self, **kwargs):
173        for a,b in kwargs.items():
174            setattr(self, a, b)
175
176    def __call__(self, nval):
177
178        val = nval
179
180        def saveplog(a,b):
181            try:
182                return math.log(a/b)
183            except:
184                if a < b:
185                    return -10
186                else:
187                    return +10
188
189        try:
190            ocene = self.condprob(val)
191            if sum(ocene) < 0.01:
192                return 0.0
193            return saveplog(ocene[0], ocene[1])
194
195        except:
196            return 0.0
197
198class AT_loessLearner(object):
199
200    def __call__(self, i, data):
201        try:
202            ca = Orange.statistics.contingency.VarClass(data.domain.attributes[i], data)
203            a =  Orange.statistics.estimate.ConditionalLoess(ca, nPoints=5)
204            return AT_loess(condprob=a)
205        except:
206            return AT_loess(condprob=None)
207
208def nth(l, n):
209    return [a[n] for a in l]
210
211class Assess(GeneSetTrans):
212    """
213    Uses the underlying GSEA code to select genes.
214    Takes data and creates attribute transformations.
215    """
216
217    def __init__(self, rankingf=None, **kwargs):
218        self.rankingf = rankingf
219        if self.rankingf == None:
220            self.rankingf = AT_edelmanParametricLearner()
[1711]221        self.example_buffer = {}
222        self.attransv = 0
[1728]223        self.ignore_unmatchable_context = True
[1607]224        super(Assess, self).__init__(**kwargs)
225
[1711]226    def _ordered_and_lcor(self, ex, nm, name_ind, attrans, attransv):
227        """ Buffered! It should be computed only once per example. """ 
228        #name_ind and nm are always co-created, so I need to have only one as a key
229        key = (ex, nm, attransv)
230        if key not in self.example_buffer:
231            ex_atts = [ at.name for at in ex.domain.attributes ]
[1728]232            new_atts = [ name_ind[nm.umatch(an)] if nm.umatch(an) != None else (None if self.ignore_unmatchable_context else i)
233                for i,an in enumerate(ex_atts) ]
[1711]234
235            #new_atts: indices of genes in original data for that sample
236            #POSSIBLE REVERSE IMPLEMENTATION (slightly different
237            #for data from different chips):
238            #save pairs together and sort (or equiv. dictionary transformation)
239
240            indexes = filter(lambda x: x[0] != None, zip(new_atts, range(len(ex_atts))))
241
242            lcor = [ attrans[index_in_data](ex[index_in_ex].value) 
243                for index_in_data, index_in_ex in indexes if
244                ex[index_in_ex].value != '?' ]
[1725]245
246            indices_to_lcori = dict( (index_in_ex, i) for i,(_, index_in_ex) in enumerate(indexes) 
247                if ex[index_in_ex].value != '?')
248
[1711]249            #indexes in original lcor, sorted from higher to lower values
250            ordered = obiGsea.orderedPointersCorr(lcor)
251            rev2 = numpy.argsort(ordered)
[1725]252            self.example_buffer[key] = lcor, ordered, rev2, indices_to_lcori
[1711]253        return self.example_buffer[key]
254
[1607]255    def build_features(self, data, gene_sets):
256
257        attributes = []
258
259        #attrans: { i_orig: ranking_function }
260        attrans = [ self.rankingf(iat, data) for iat, at in enumerate(data.domain.attributes) ]
[1711]261        attransv = self.attransv
262        self.attransv += 1
[1607]263
264        nm_all, _ =  self._mat_ni(data)
265
266        for gs in gene_sets:
267
268            at = Orange.feature.Continuous(name=str(gs))
269
270            geneset = list(gs.genes)
271            nm, name_ind, genes, takegenes, to_geneset = self._match_data(data, geneset, odic=True)
[1711]272            takegenes = [ geneset[i] for i in takegenes ]
[1607]273            genes = set(genes)
274
[1711]275            def t(ex, w, takegenes=takegenes, nm=nm, attrans=attrans, attransv=attransv):
276                nm2, name_ind2, genes2 = self._match_instance(ex, takegenes)
[1725]277                lcor, ordered, rev2, indices_to_lcori = \
278                    self._ordered_and_lcor(ex, nm, name_ind, attrans, attransv)
[1728]279
280           
[1607]281                #subset = list of indices, lcor = correlations, ordered = order
[1725]282                #make it compatible with lcor, if some are missing in lcor
283                subset = filter(None,
284                    [ indices_to_lcori.get(name_ind2[g], None) for g in genes2 ] )
[1728]285
[1711]286                return obiGsea.enrichmentScoreRanked(subset, lcor, ordered, rev2=rev2)[0] 
[1607]287
288            at.get_value_from = t
289            attributes.append(at)
290
291        return attributes
292   
[1772]293def setSig_example_geneset(ex, data, no_unknowns, check_same=False):
[1583]294    """ Gets learning data and example with the same domain, both
295    containing only genes from the gene set. """
296
297    distances = [ [], [] ]   
298
[1707]299    def pearson(ex1, ex2):
300        vals1 = ex1.native(0)[:-1]
301        vals2 = ex2.native(0)[:-1]
[1583]302
[1772]303        if check_same and vals1 == vals2:
304            return 10 #they are the same
305
[1583]306        #leaves undefined elements out
[1707]307        if not no_unknowns:
308            common = [ True if v1 != "?" and v2 != "?" else False \
309                for v1,v2 in zip(vals1,vals2) ]
310            vals1 = [ v for v,c in zip(vals1, common) if c ]
311            vals2 = [ v for v,c in zip(vals2, common) if c ]
[1583]312
[1723]313        #statc correlation is from 5-10 times faster than numpy!
314        try:
315            return statc.pearsonr(vals1, vals2)[0]
316        except:
317            return numpy.corrcoef([vals1, vals2])[0,1] 
318       
[1583]319
320    def ttest(ex1, ex2):
321        try:
322            return stats.lttest_ind(ex1, ex2)[0]
323        except:
324            return 0.0
325   
326    #maps class value to its index
327    classValueMap = dict( [ (val,i) for i,val in enumerate(data.domain.class_var.values) ])
328 
329    #create distances to all learning data - save or other class
330    for c in data:
[1772]331        p = pearson(c, ex)
332        if p != 10:
333             distances[classValueMap[c[-1].value]].append(pearson(c, ex))
[1583]334
335    return ttest(distances[0], distances[1])
336
337def mat_ni(data, matcher):
338    nm = matcher([at.name for at in data.domain.attributes])
339    name_ind = dict((n.name,i) for i,n in enumerate(data.domain.attributes))
340    return nm, name_ind
341
342def select_genesets(nm, gene_sets, min_size=3, max_size=1000, min_part=0.1):
343    """ Returns a list of gene sets that have sizes in limits """
344
345    def ok_sizes(gs):
346        """compares sizes of genesets to limitations"""
347        transl = filter(lambda x: x != None, [ nm.umatch(gene) for gene in gs.genes ])
348        if len(transl) >= min_size \
349            and len(transl) <= max_size \
350            and float(len(transl))/len(gs.genes) >= min_part:
351            return True
352        return False
353
354    return filter(ok_sizes, gene_sets) 
[1578]355
[1592]356def vou(ex, gn, indices):
357    """ returns the value or "?" for the given gene name gn"""
358    if gn not in indices:
359        return "?"
360    else:
361        return ex[indices[gn]].value
362
363class SetSig(GeneSetTrans):
364
[1707]365    def __init__(self, **kwargs):
366        self.no_unknowns = kwargs.pop("no_unknowns", False)
[1772]367        self.check_same = kwargs.pop("check_same", False)
[1707]368        super(SetSig, self).__init__(**kwargs)
369
[1594]370    def build_feature(self, data, gs):
[1592]371
[1594]372        at = Orange.feature.Continuous(name=str(gs))
[1707]373        geneset = list(gs.genes)
374        nm, name_ind, genes, takegenes = self._match_data(data, geneset)
375        indices = [ name_ind[gene] for gene in genes ]
[1708]376        takegenes = [ geneset[i] for i in takegenes ]
[1572]377
[1708]378        def t(ex, w, gs=gs, data=data, indices=indices, takegenes=takegenes):
379            nm2, name_ind2, genes2 = self._match_instance(ex, takegenes)
[1593]380
[1707]381            domain = Orange.data.Domain([data.domain.attributes[i] for i in indices], data.domain.class_var)
[1594]382            datao = Orange.data.Table(domain, data)
383           
384            #convert the example to the same domain
385            exvalues = [ vou(ex, gn, name_ind2) for gn in genes2 ] + [ "?" ]
386            example = Orange.data.Instance(domain, exvalues)
[1583]387
[1772]388            return setSig_example_geneset(example, datao, self.no_unknowns, check_same=self.check_same) #only this one is setsig specific
[1594]389     
390        at.get_value_from = t
391        return at
[1593]392
[1599]393class ParametrizedTransformation(GeneSetTrans):
[1595]394
[1599]395    def _get_par(self, datao):
[1690]396        """ Get parameters for a subset of data, that comprises only the gene set """
[1599]397        pass
398       
399    def _use_par(self, ex, constructt):
400        pass
401   
[1595]402    def build_feature(self, data, gs):
403
404        at = Orange.feature.Continuous(name=str(gs))
405
406        geneset = list(gs.genes)
[1598]407        nm, name_ind, genes, takegenes = self._match_data(data, geneset)
[1595]408        domain = Orange.data.Domain([data.domain.attributes[name_ind[gene]] for gene in genes], data.domain.class_var)
409        datao = Orange.data.Table(domain, data)
[1709]410        takegenes = [ geneset[i] for i in takegenes ]
[1595]411
[1599]412        constructt = self._get_par(datao)
[1595]413
[1709]414        def t(ex, w, constructt=constructt, takegenes=takegenes, domain=domain):
415            nm2, name_ind2, genes2 = self._match_instance(ex, takegenes)
[1595]416         
417            #convert the example to the same domain
418            exvalues = [ vou(ex, gn, name_ind2) for gn in genes2 ] + [ "?" ]
[1690]419            ex = Orange.data.Instance(domain, exvalues)
[1595]420
[1599]421            return self._use_par(ex, constructt)
[1772]422       
[1595]423        at.get_value_from = t
[1772]424        at.dbg = constructt #for debugging
425       
[1595]426        return at
427
[1599]428class PLS(ParametrizedTransformation):
[1593]429
[1599]430    def _get_par(self, datao):
431        return PLSCall(datao, nc=1, y=[datao.domain.class_var])
432       
433    def _use_par(self, ex, constructt):
[1690]434        ex = [ ex[i].value for i in range(len(ex.domain.attributes)) ]
[1599]435        xmean, W, P, _ = constructt
436        ex = ex - xmean # same input transformation
[1593]437
[1599]438        nc = W.shape[1]
[1593]439
[1599]440        TR = numpy.empty((1, nc))
441        XR = ex
[1593]442
[1599]443        dot = numpy.dot
[1593]444
[1599]445        for i in range(nc):
446           t = dot(XR, W[:,i].T)
447           XR = XR - t*numpy.array([P[:,i]])
448           TR[:,i] = t
[1593]449
[1599]450        return TR[0][0]
451       
452class PCA(ParametrizedTransformation):
[1593]453
[1599]454    def _get_par(self, datao):
455        return pca(datao)
456
457    def _use_par(self, arr, constructt):
[1690]458        arr = [ arr[i].value for i in range(len(arr.domain.attributes)) ]
[1599]459        evals, evect, xmean = constructt
460
461        arr = arr - xmean # same input transformation - a row in a matrix
462        ev0 = evect[0] #this is a row in a matrix - do a dot product
463        a = numpy.dot(arr, ev0)
464
465        return a
[1593]466
[1592]467class SimpleFun(GeneSetTrans):
468
[1594]469    def build_feature(self, data, gs):
[1592]470
[1594]471        at = Orange.feature.Continuous(name=str(gs))
[1592]472
[1594]473        def t(ex, w, gs=gs):
474            geneset = list(gs.genes)
[1598]475            nm2, name_ind2, genes2 = self._match_instance(ex, geneset)
[1594]476           
477            exvalues = [ vou(ex, gn, name_ind2) for gn in genes2 ] + [ "?" ]
478            exvalues = filter(lambda x: x != "?", exvalues)
[1592]479
[1594]480            return self.fn(exvalues)
481     
482        at.get_value_from = t
483        return at
[1592]484
485class Mean(SimpleFun):
486
487    def __init__(self, **kwargs):
488       self.fn = numpy.mean
489       super(Mean, self).__init__(**kwargs)
490
491class Median(SimpleFun):
492
493    def __init__(self, **kwargs):
494       self.fn = numpy.median
495       super(Median, self).__init__(**kwargs)
[1572]496
[1597]497class GSA(GeneSetTrans):
498
499    def build_features(self, data, gene_sets):
500
501        attributes = []
502
503        def tscorec(data, at, cache=None):
504            ma = obiExpression.MA_t_test()(at,data)
505            return ma
506
507        tscores = [ tscorec(data, at) for at in data.domain.attributes ]
508
509        def to_z_score(t):
510            return float(scipy.stats.norm.ppf(scipy.stats.t.cdf(t, len(data)-2)))
511
512        zscores = map(to_z_score, tscores)
513
514        for gs in gene_sets:
515
516            at = Orange.feature.Continuous(name=str(gs))
517
518            geneset = list(gs.genes)
[1598]519            nm, name_ind, genes, takegenes, to_geneset = self._match_data(data, geneset, odic=True)
[1597]520            #take each gene only once
521            genes = set(genes)
522
523            D = numpy.mean([max(zscores[name_ind[g]],0) for g in genes]) \
524                + numpy.mean([min(zscores[name_ind[g]],0) for g in genes])
525
526            if D >= 0:
527                consider_genes = [ to_geneset[g] for g in genes if zscores[name_ind[g]] > 0.0 ]
528            else:
529                consider_genes = [ to_geneset[g] for g in genes if zscores[name_ind[g]] < 0.0 ]
530
531            def t(ex, w, consider_genes=consider_genes):
[1598]532                nm2, name_ind2, genes2 = self._match_instance(ex, consider_genes)
[1597]533             
534                #convert the example to the same domain
535                exvalues = [ vou(ex, gn, name_ind2) for gn in genes2 ] + [ "?" ]
536                exvalues = filter(lambda x: x != "?", exvalues)
537             
538                return numpy.mean(exvalues)
539
540            at.get_value_from = t
541            attributes.append(at)
542
543        return attributes
544
[1600]545def tscorec(data, at, cache=None):
546    """ Cached attribute  tscore calculation """
547    if cache != None and at in cache: return cache[at]
548    ma = obiExpression.MA_t_test()(at,data)
549    if cache != None: cache[at] = ma
550    return ma
551
552def nth(l, n):
553    return [a[n] for a in l]
554
555def compute_corg(data, inds, tscorecache):
556    """
557    Compute CORG for this geneset specified with gene inds
558    in the example table. Output is the list of gene inds
559    in CORG.
560
561    """
562    #order member genes by their t-scores: decreasing, if av(t-score) >= 0,
563    #else increasing
564    tscores = [ tscorec(data, at, tscorecache) for at in inds ]
565    sortedinds = nth(sorted(zip(inds,tscores), key=lambda x: x[1], \
566        reverse=numpy.mean(tscores) >= 0), 0)
567
568    def S(corg):
569        """ Activity score separation - S(G) in
570        the article """
571        asv = Orange.feature.Continuous(name='AS')
572        asv.getValueFrom = lambda ex,rw: Orange.data.Value(asv, corgs_activity_score(ex, corg))
573        data2 = Orange.data.Table(Orange.data.Domain([asv], data.domain.classVar), data)
574        return abs(tscorec(data2, 0)) #FIXME absolute - nothing in the article abs()
575           
576    #greedily find CORGS procing the best separation
577    g = S(sortedinds[:1])
578    bg = 1
579    for a in range(2, len(sortedinds)+1):
580        tg = S(sortedinds[:a])
[1772]581        if tg > g: #improvement
[1600]582            g = tg
583            bg = a
584        else:
585            break
586       
[1772]587    return sortedinds[:bg] #FIXED - one too many was taken
[1600]588
589class CORGs(ParametrizedTransformation):
590    """
591    WARNING: input has to be z_ij table! each gene needs to be normalized
592    (mean=0, stdev=1) for all samples.
593    """
594
595    def __call__(self, *args, **kwargs):
596        self.tscorecache = {} #reset a cache
597        return super(CORGs, self).__call__(*args, **kwargs)
598
599    def build_feature(self, data, gs):
600
601        at = Orange.feature.Continuous(name=str(gs))
602        geneset = list(gs.genes)
603
604        nm, name_ind, genes, takegenes, to_geneset = self._match_data(data, geneset, odic=True)
605        indices = compute_corg(data, [ name_ind[g] for g in genes ], self.tscorecache)
606
607        ind_names = dict( (a,b) for b,a in name_ind.items() )
608        selected_genes = sorted(set([to_geneset[ind_names[i]] for i in indices]))
609           
610        def t(ex, w, corg=selected_genes): #copy od the data
611            nm2, name_ind2, genes2 = self._match_instance(ex, corg, None)
612            exvalues = [ vou(ex, gn, name_ind2) for gn in genes2 ]
613            return sum(v if v != '?' else 0.0 for v in exvalues)/len(corg)**0.5
614     
615        at.get_value_from = t
616        return at
617
[1572]618if __name__ == "__main__":
619
620    data = Orange.data.Table("iris")
621    gsets = obiGeneSets.collections({
[1597]622        #"ALL": ['sepal length', 'sepal width', 'petal length', 'petal width'],
[1572]623        "f3": ['sepal length', 'sepal width', 'petal length'],
624        "l3": ['sepal width', 'petal length', 'petal width'],
625        })
[1587]626    matcher = obiGene.matcher([])
627    choosen_cv = ["Iris-setosa", "Iris-versicolor"]
[1572]628
[1587]629    """
630    data = Orange.data.Table("DLBCL_200a")
631    gsets = obiGeneSets.collections((("KEGG",),"9606"))
632    matcher = obiGene.matcher([obiGene.GMKEGG("hsa")])
633    choosen_cv = None
634    """
[1572]635
636    def to_old_dic(d, data):
637        ar = defaultdict(list)
638        for ex1 in data:
639            ex = d(ex1)
640            for a,x in zip(d.attributes, ex):
641                ar[a.name].append(x.value)
642        return ar
643
644    def pp2(ar):
645        ol =  sorted(ar.items())
646        print '\n'.join([ a + ": " +str(b) for a,b in ol])
647
[1607]648    ass = Assess(data, matcher=matcher, gene_sets=gsets, class_values=choosen_cv, min_part=0.0)
[1572]649    ar = to_old_dic(ass.domain, data[:5])
650    pp2(ar)
Note: See TracBrowser for help on using the repository browser.