source: trunk/anuga_core/source/anuga/structures/culvert_operator.py @ 7957

Last change on this file since 7957 was 7957, checked in by steve, 14 years ago

update of culverts

File size: 26.4 KB
Line 
1import sys
2
3from anuga.shallow_water.forcing import Inflow, General_forcing
4from anuga.structures.culvert_polygons import create_culvert_polygons
5from anuga.utilities.system_tools import log_to_file
6from anuga.geometry.polygon import inside_polygon, is_inside_polygon
7from anuga.geometry.polygon import plot_polygons, polygon_area
8
9
10from anuga.utilities.numerical_tools import mean
11from anuga.utilities.numerical_tools import ensure_numeric, sign
12       
13from anuga.config import g, epsilon
14from anuga.config import minimum_allowed_height, velocity_protection       
15import anuga.utilities.log as log
16
17import numpy as num
18from math import sqrt
19from math import sqrt
20
21class Below_interval(Exception): pass 
22class Above_interval(Exception): pass
23
24   
25
26class Generic_box_culvert:
27    """Culvert flow - transfer water from one rectngular box to another.
28    Sets up the geometry of problem
29   
30    This is the base class for culverts. Inherit from this class (and overwrite
31    compute_discharge method for specific subclasses)
32   
33    Input: Two points, pipe_size (either diameter or width, height),
34    mannings_rougness,
35    """ 
36
37    def __init__(self,
38                 domain,
39                 end_point0=None, 
40                 end_point1=None,
41                 enquiry_gap_factor=0.2,
42                 width=None,
43                 height=None,
44                 verbose=False):
45       
46        # Input check
47       
48        if height is None:
49            height = width
50
51        self.height = height
52        self.width = width
53       
54        self.domain = domain
55        self.end_points= [end_point0, end_point1]
56        self.enquiry_gap_factor=enquiry_gap_factor
57
58        self.verbose=verbose
59        self.filename = None
60       
61
62        # Create the fundamental culvert polygons from polygon
63        self.create_culvert_polygons()
64        self.compute_enquiry_indices()
65        self.check_culvert_inside_domain()
66
67
68        # Establish initial values at each enquiry point
69        self.enquiry_quantity_values = []
70        dq = domain.quantities
71        for i in [0,1]:
72            idx = self.enquiry_indices[i]
73            elevation = dq['elevation'].get_values(location='centroids',
74                                                   indices=[idx])[0]
75            stage = dq['stage'].get_values(location='centroids',
76                                           indices=[idx])[0]
77            depth = stage-elevation
78           
79            quantity_values = {'stage' : stage, 'elevation' : elevation, 'depth' : depth }
80            self.enquiry_quantity_values.append(quantity_values)
81       
82       
83        assert self.culvert_length > 0.0
84
85
86    def set_store_hydrograph_discharge(self,filename=None):
87
88        if filename is None:
89            self.filename = 'culvert_discharge_hydrograph'
90        else:
91            self.filename = filename
92
93        self.discharge_hydrograph = True
94       
95        self.timeseries_filename = self.filename + '_timeseries.csv'
96        fid = open(self.timeseries_filename, 'w')
97        fid.write('time, discharge\n')
98        fid.close()
99
100
101
102
103    def create_culvert_polygons(self):
104        """Create polygons at the end of a culvert inlet and outlet.
105        At either end two polygons will be created; one for the actual flow to pass through and one a little further away
106        for enquiring the total energy at both ends of the culvert and transferring flow.
107        """
108
109        # Calculate geometry
110        x0, y0 = self.end_points[0]
111        x1, y1 = self.end_points[1]
112
113        dx = x1-x0
114        dy = y1-y0
115
116        self.culvert_vector = num.array([dx, dy])
117        self.culvert_length = sqrt(num.sum(self.culvert_vector**2))
118
119
120        # Unit direction vector and normal
121        self.culvert_vector /= self.culvert_length                      # Unit vector in culvert direction
122        self.culvert_normal = num.array([-dy, dx])/self.culvert_length  # Normal vector
123
124        # Short hands
125        w = 0.5*width*normal # Perpendicular vector of 1/2 width
126        h = height*vector    # Vector of length=height in the
127                             # direction of the culvert
128        gap = (1 + enquiry_gap_factor)*h
129
130
131        self.exchange_polygons = []
132        self.enquiry_points = []
133
134        # Build exchange polygon and enquiry points 0 and 1
135        for i in [0,1]:
136            p0 = self.end_points[i] + w
137            p1 = self.end_point[i] - w
138            p2 = p1 - h
139            p3 = p0 - h
140            self.exchange_polygons.append(num.array([p0,p1,p2,p3]))
141            self.enquiry_points.append(end_point0 - gap)
142
143        self.polygon_areas = []
144
145        # Check that enquiry points are outside exchange polygons
146        for i in [0,1]:
147            polygon = self.exchange_polygons[i]
148            # FIXME(SR) should use area of triangles associated with polygon
149            area = polygon_area(polygon)
150            self.polygon_areas.append(area)
151
152            msg = 'Polygon %s ' %(polygon)
153            msg += ' has area = %f' % area
154            assert area > 0.0, msg
155
156            for j in [0,1]:
157                point = self.enquiry_points[j]
158                msg = 'Enquiry point falls inside a culvert polygon.'
159
160                assert not inside_polygon(point, polygon), msg
161
162       
163    def __call__(self, domain):
164
165        # Time stuff
166        time = domain.get_time()
167       
168       
169        update = False
170        if self.update_interval is None:
171            # Use next timestep as has been computed in domain.py       
172            delta_t = domain.timestep           
173            update = True
174        else:   
175            # Use update interval
176            delta_t = self.update_interval           
177            if time - self.last_update > self.update_interval or time == 0.0:
178                update = True
179           
180        if self.log_filename is not None:       
181            s = '\nTime = %.2f, delta_t = %f' %(time, delta_t)
182            log_to_file(self.log_filename, s)
183               
184                               
185        if update is True:
186            self.compute_rates(delta_t)
187           
188   
189        # Execute flow term for each opening
190        # This is where Inflow objects are evaluated using the last rate
191        # that has been calculated
192        #
193        # This will take place at every internal timestep and update the domain
194        for opening in self.openings:
195            opening(domain)
196           
197
198
199    def compute_enquiry_indices(self):
200        """Get indices for nearest centroids to self.enquiry_points
201        """
202       
203        domain = self.domain
204       
205        enquiry_indices = []                 
206        for point in self.enquiry_points:
207            # Find nearest centroid
208            N = len(domain)   
209            points = domain.get_centroid_coordinates(absolute=True)
210
211            # Calculate indices in exchange area for this forcing term
212           
213            triangle_id = min_dist = sys.maxint
214            for k in range(N):
215                x, y = points[k,:] # Centroid
216
217                c = point                               
218                distance = (x-c[0])**2+(y-c[1])**2
219                if distance < min_dist:
220                    min_dist = distance
221                    triangle_id = k
222
223                   
224            if triangle_id < sys.maxint:
225                msg = 'found triangle with centroid (%f, %f)'\
226                    %tuple(points[triangle_id, :])
227                msg += ' for point (%f, %f)' %tuple(point)
228               
229                enquiry_indices.append(triangle_id)
230            else:
231                msg = 'Triangle not found for point (%f, %f)' %point
232                raise Exception, msg
233       
234        self.enquiry_indices = enquiry_indices
235
236       
237    def check_culvert_inside_domain(self):
238        """Check that all polygons and enquiry points lie within the mesh.
239        """
240        bounding_polygon = self.domain.get_boundary_polygon()
241        P = self.culvert_polygons
242        for key in P.keys():
243            if key in ['exchange_polygon0', 
244                       'exchange_polygon1']:
245                for point in list(P[key]) + self.enquiry_points:
246                    msg = 'Point %s in polygon %s for culvert %s did not'\
247                        %(str(point), key, self.label)
248                    msg += 'fall within the domain boundary.'
249                    assert is_inside_polygon(point, bounding_polygon), msg
250           
251
252    def adjust_flow_for_available_water_at_inlet(self, Q, delta_t):
253        """Adjust Q downwards depending on available water at inlet
254
255           This is a critical step in modelling bridges and Culverts
256           the predicted flow through a structure based on an abstract
257           algorithm can at times request for water that is simply not
258           available due to any number of constrictions that limit the
259           flow approaching the structure In order to ensure that
260           there is adequate flow available certain checks are
261           required There needs to be a check using the Static Water
262           Volume sitting infront of the structure, In addition if the
263           water is moving the available water will be larger than the
264           static volume
265           
266           NOTE To temporarily switch this off for Debugging purposes
267           rem out line in function def compute_rates below
268        """
269   
270        if delta_t < epsilon:
271            # No need to adjust if time step is very small or zero
272            # In this case the possible flow will be very large
273            # anyway.
274            return Q
275       
276        # Short hands
277        domain = self.domain       
278        dq = domain.quantities               
279        time = domain.get_time()
280        I = self.inlet
281        idx = I.exchange_indices   
282
283        # Find triangle with the smallest depth
284        stage = dq['stage'].get_values(location='centroids', 
285                                               indices=[idx])
286        elevation = dq['elevation'].get_values(location='centroids', 
287                                               indices=[idx])       
288        depth = stage-elevation
289        min_depth = min(depth.flat)  # This may lead to errors if edge of area is at a higher level !!!!
290        avg_depth = mean(depth.flat) # Yes, but this one violates the conservation unit tests
291
292
293
294        # FIXME (Ole): If you want these, use log.critical() and
295        # make the statements depend on verbose
296        #print I.depth
297        #print I.velocity
298        #print self.width
299
300        # max_Q Based on Volume Calcs
301
302
303        depth_term = min_depth*I.exchange_area/delta_t
304        if min_depth < 0.2:
305            # Only add velocity term in shallow waters (< 20 cm)
306            # This is a little ad hoc, but maybe it is reasonable
307            velocity_term = self.width*min_depth*I.velocity
308        else:
309            velocity_term = 0.0
310
311        # This one takes approaching water into account   
312        max_Q = max(velocity_term, depth_term)
313
314        # This one preserves Volume
315        #max_Q = depth_term
316
317
318        if self.verbose is True:
319            log.critical('Max_Q = %f' % max_Q)           
320            msg = 'Width = %.2fm, Depth at inlet = %.2f m, Velocity = %.2f m/s.      ' % (self.width, I.depth, I.velocity)
321            msg += 'Max Q = %.2f m^3/s' %(max_Q)
322            log.critical(msg)
323
324        if self.log_filename is not None:               
325            log_to_file(self.log_filename, msg)
326        # New Procedure for assessing the flow available to the Culvert
327        # This routine uses the GET FLOW THROUGH CROSS SECTION
328        #   Need to check Several Polyline however
329        # Firstly 3 sides of the exchange Poly
330        # then only the Line Directly infront of the Polygon
331        # Access polygon Points from   self.inlet.polygon
332     
333        #  The Following computes the flow crossing over 3 sides of the exchange polygon for the structure
334        # Clearly the flow in the culvert can not be more than that flowing toward it through the exhange polygon
335       
336        #q1 = domain.get_flow_through_cross_section(self.culvert_polygons['exchange_polygon0'][1:3]) # First Side Segment
337        #q2 = domain.get_flow_through_cross_section(self.culvert_polygons['exchange_polygon0'][2:])   # Second Face Segment
338        #q3 =domain.get_flow_through_cross_section(self.culvert_polygons['exchange_polygon0'].take([3,0], axis=0)) # Third Side Segment
339        # q4 = domain.get_flow_through_cross_section([self.culvert_polygons['exchange_polygon0'][1:4]][0])
340        #q4=max(q1,0.0)+max(q2,0.0)+max(q3,0.0)
341        # To use only the Flow crossing the 3 sides of the Exchange Polygon use the following Line Only
342        #max_Q=max(q1,q2,q3,q4)
343        # Try Simple Smoothing using Average of 2 approaches
344        #max_Q=(max(q1,q2,q3,q4)+max_Q)/2.0
345        # Calculate the minimum in absolute terms of
346        # the requsted flow and the possible flow
347        Q_reduced = sign(Q)*min(abs(Q), abs(max_Q))
348        if self.verbose is True:
349            msg = 'Initial Q Reduced = %.2f m3/s.      ' % (Q_reduced)
350            log.critical(msg)
351
352        if self.log_filename is not None:               
353            log_to_file(self.log_filename, msg)
354        # Now Keep Rolling Average of Computed Discharge to Reduce / Remove Oscillations
355        #  can use delta_t if we want to averageover a time frame for example
356        # N = 5.0/delta_t  Will provide the average over 5 seconds
357
358        self.i=(self.i+1)%self.N
359        self.Q_list[self.i]=Q_reduced
360        Q_reduced = sum(self.Q_list)/len(self.Q_list)
361
362        if self.verbose is True:
363            msg = 'Final Q Reduced = %.2f m3/s.      ' % (Q_reduced)
364            log.critical(msg)
365
366        if self.log_filename is not None:               
367            log_to_file(self.log_filename, msg)
368
369
370        if abs(Q_reduced) < abs(Q): 
371            msg = '%.2fs: Requested flow is ' % time
372            msg += 'greater than what is supported by the smallest '
373            msg += 'depth at inlet exchange area:\n        '
374            msg += 'inlet exchange area: %.2f '% (I.exchange_area) 
375            msg += 'velocity at inlet :%.2f '% (I.velocity)
376            msg += 'Vel* Exch Area = : %.2f '% (I.velocity*avg_depth*self.width)
377            msg += 'h_min*inlet_area/delta_t = %.2f*%.2f/%.2f '\
378                % (avg_depth, I.exchange_area, delta_t)
379            msg += ' = %.2f m^3/s\n        ' % Q_reduced
380            msg += 'Q will be reduced from %.2f m^3/s to %.2f m^3/s.' % (Q, Q_reduced)
381            msg += 'Note calculate max_Q from V %.2f m^3/s ' % (max_Q)
382            if self.verbose is True:
383                log.critical(msg)
384               
385            if self.log_filename is not None:               
386                log_to_file(self.log_filename, msg)
387       
388        return Q_reduced   
389                       
390           
391    def compute_rates(self, delta_t):
392        """Compute new rates for inlet and outlet
393        """
394
395        # Short hands
396        domain = self.domain       
397        dq = domain.quantities               
398       
399        # Time stuff
400        time = domain.get_time()
401        self.last_update = time
402
403           
404        if hasattr(self, 'log_filename'):
405            log_filename = self.log_filename
406           
407        # Compute stage, energy and velocity at the
408        # enquiry points at each end of the culvert
409        openings = self.openings
410        for i, opening in enumerate(openings):
411            idx = self.enquiry_indices[i]               
412           
413            stage = dq['stage'].get_values(location='centroids',
414                                           indices=[idx])[0]
415            depth = h = stage-opening.elevation
416                                                           
417           
418            # Get velocity                                 
419            xmomentum = dq['xmomentum'].get_values(location='centroids',
420                                                   indices=[idx])[0]
421            ymomentum = dq['xmomentum'].get_values(location='centroids',
422                                                   indices=[idx])[0]
423
424            if h > minimum_allowed_height:
425                u = xmomentum/(h + velocity_protection/h)
426                v = ymomentum/(h + velocity_protection/h)
427            else:
428                u = v = 0.0
429               
430            v_squared = u*u + v*v
431           
432            if self.use_velocity_head is True:
433                velocity_head = 0.5*v_squared/g   
434            else:
435                velocity_head = 0.0
436           
437            opening.total_energy = velocity_head + stage
438            opening.specific_energy = velocity_head + depth
439            opening.stage = stage
440            opening.depth = depth
441            opening.velocity = sqrt(v_squared)
442           
443
444        # We now need to deal with each opening individually
445        # Determine flow direction based on total energy difference
446        delta_total_energy = openings[0].total_energy - openings[1].total_energy
447        if delta_total_energy > 0:
448            inlet = openings[0]
449            outlet = openings[1]
450
451            # FIXME: I think this whole momentum jet thing could be a bit more elegant
452            inlet.momentum = self.opening_momentum[0]
453            outlet.momentum = self.opening_momentum[1]
454        else:
455            inlet = openings[1]
456            outlet = openings[0]
457           
458            inlet.momentum = self.opening_momentum[1]
459            outlet.momentum = self.opening_momentum[0]
460
461            delta_total_energy = -delta_total_energy
462
463        self.inlet = inlet
464        self.outlet = outlet
465           
466        msg = 'Total energy difference is negative'
467        assert delta_total_energy >= 0.0, msg
468
469        # Recompute slope and issue warning if flow is uphill
470        # These values do not enter the computation
471        delta_z = inlet.elevation - outlet.elevation
472        culvert_slope = (delta_z/self.length)
473        if culvert_slope < 0.0:
474            # Adverse gradient - flow is running uphill
475            # Flow will be purely controlled by uphill outlet face
476            if self.verbose is True:
477                log.critical('%.2fs - WARNING: Flow is running uphill.' % time)
478           
479        if self.log_filename is not None:
480            s = 'Time=%.2f, inlet stage = %.2f, outlet stage = %.2f'\
481                %(time, self.inlet.stage, self.outlet.stage)
482            log_to_file(self.log_filename, s)
483            s = 'Delta total energy = %.3f' %(delta_total_energy)
484            log_to_file(log_filename, s)
485
486           
487        # Determine controlling energy (driving head) for culvert
488        if inlet.specific_energy > delta_total_energy:
489            # Outlet control
490            driving_head = delta_total_energy
491        else:
492            # Inlet control
493            driving_head = inlet.specific_energy
494           
495
496
497        if self.inlet.depth <= self.trigger_depth:
498            Q = 0.0
499        else:
500            # Calculate discharge for one barrel and
501            # set inlet.rate and outlet.rate
502           
503            if self.culvert_description_filename is not None:
504                try:
505                    Q = interpolate_linearly(driving_head, 
506                                             self.rating_curve[:,0], 
507                                             self.rating_curve[:,1]) 
508                except Below_interval, e:
509                    Q = self.rating_curve[0,1]             
510                    msg = '%.2fs: ' % time
511                    msg += 'Delta head smaller than rating curve minimum: '
512                    msg += str(e)
513                    msg += '\n        '
514                    msg += 'I will use minimum discharge %.2f m^3/s ' % Q
515                    msg += 'for culvert "%s"' % self.label
516                   
517                    if hasattr(self, 'log_filename'):                   
518                        log_to_file(self.log_filename, msg)
519                except Above_interval, e:
520                    Q = self.rating_curve[-1,1]         
521                    msg = '%.2fs: ' % time                 
522                    msg += 'Delta head greater than rating curve maximum: '
523                    msg += str(e)
524                    msg += '\n        '
525                    msg += 'I will use maximum discharge %.2f m^3/s ' % Q
526                    msg += 'for culvert "%s"' % self.label
527                   
528                    if self.log_filename is not None:                   
529                        log_to_file(self.log_filename, msg)
530            else:
531                # User culvert routine
532                Q, barrel_velocity, culvert_outlet_depth =\
533                    self.culvert_routine(inlet.depth,
534                                         outlet.depth,
535                                         inlet.velocity,
536                                         outlet.velocity,
537                                         inlet.specific_energy, 
538                                         delta_total_energy, 
539                                         g,
540                                         culvert_length=self.length,
541                                         culvert_width=self.width,
542                                         culvert_height=self.height,
543                                         culvert_type=self.culvert_type,
544                                         manning=self.manning,
545                                         sum_loss=self.sum_loss,
546                                         log_filename=self.log_filename)
547           
548           
549       
550        # Adjust discharge for multiple barrels
551        Q *= self.number_of_barrels
552
553        # Adjust discharge for available water at the inlet
554        Q = self.adjust_flow_for_available_water_at_inlet(Q, delta_t)
555       
556        self.inlet.rate = -Q
557        self.outlet.rate = Q
558
559
560        # Momentum jet stuff
561        if self.use_momentum_jet is True:
562
563
564            # Compute barrel momentum
565            barrel_momentum = barrel_velocity*culvert_outlet_depth
566
567            if self.log_filename is not None:                                   
568                s = 'Barrel velocity = %f' %barrel_velocity
569                log_to_file(self.log_filename, s)
570
571            # Compute momentum vector at outlet
572            outlet_mom_x, outlet_mom_y = self.vector * barrel_momentum
573               
574            if self.log_filename is not None:               
575                s = 'Directional momentum = (%f, %f)' %(outlet_mom_x, outlet_mom_y)
576                log_to_file(self.log_filename, s)
577
578
579            # Update momentum       
580            if delta_t > 0.0:
581                xmomentum_rate = outlet_mom_x - outlet.momentum[0].value
582                xmomentum_rate /= delta_t
583                   
584                ymomentum_rate = outlet_mom_y - outlet.momentum[1].value
585                ymomentum_rate /= delta_t
586                       
587                if self.log_filename is not None:               
588                    s = 'X Y MOM_RATE = (%f, %f) ' %(xmomentum_rate, ymomentum_rate)
589                    log_to_file(self.log_filename, s)                   
590            else:
591                xmomentum_rate = ymomentum_rate = 0.0
592
593
594            # Set momentum rates for outlet jet
595            outlet.momentum[0].rate = xmomentum_rate
596            outlet.momentum[1].rate = ymomentum_rate
597
598            # Remember this value for next step (IMPORTANT)
599            outlet.momentum[0].value = outlet_mom_x
600            outlet.momentum[1].value = outlet_mom_y                   
601
602            if int(domain.time*100) % 100 == 0:
603
604                if self.log_filename is not None:               
605                    s = 'T=%.5f, Culvert Discharge = %.3f f'\
606                        %(time, Q)
607                    s += ' Depth= %0.3f  Momentum = (%0.3f, %0.3f)'\
608                        %(culvert_outlet_depth, outlet_mom_x,outlet_mom_y)
609                    s += ' Momentum rate: (%.4f, %.4f)'\
610                        %(xmomentum_rate, ymomentum_rate)                   
611                    s+='Outlet Vel= %.3f'\
612                        %(barrel_velocity)
613                    log_to_file(self.log_filename, s)
614
615
616            # Execute momentum terms
617            # This is where Inflow objects are evaluated and update the domain
618                self.outlet.momentum[0](domain)
619                self.outlet.momentum[1](domain)       
620           
621
622           
623        # Log timeseries to file
624        try:
625            fid = open(self.timeseries_filename, 'a')       
626        except:
627            pass
628        else:   
629            fid.write('%.2f, %.2f\n' %(time, Q))
630            fid.close()
631
632        # Store value of time
633        self.last_time = time
634
635
636# FIXME(Ole): Write in C and reuse this function by similar code
637# in interpolate.py
638def interpolate_linearly(x, xvec, yvec):
639
640    msg = 'Input to function interpolate_linearly could not be converted '
641    msg += 'to numerical scalar: x = %s' % str(x)
642    try:
643        x = float(x)
644    except:
645        raise Exception, msg
646
647
648    # Check bounds
649    if x < xvec[0]:
650        msg = 'Value provided = %.2f, interpolation minimum = %.2f.'\
651            % (x, xvec[0])
652        raise Below_interval, msg
653
654    if x > xvec[-1]:
655        msg = 'Value provided = %.2f, interpolation maximum = %.2f.'\
656            %(x, xvec[-1])
657        raise Above_interval, msg
658
659
660    # Find appropriate slot within bounds
661    i = 0
662    while x > xvec[i]: i += 1
663
664
665    x0 = xvec[i-1]
666    x1 = xvec[i]
667    alpha = (x - x0)/(x1 - x0)
668
669    y0 = yvec[i-1]
670    y1 = yvec[i]
671    y = alpha*y1 + (1-alpha)*y0
672
673    return y
674
675
676
677def read_culvert_description(culvert_description_filename):
678
679    # Read description file
680    fid = open(culvert_description_filename)
681
682    read_rating_curve_data = False
683    rating_curve = []
684    for i, line in enumerate(fid.readlines()):
685
686        if read_rating_curve_data is True:
687            fields = line.split(',')
688            head_difference = float(fields[0].strip())
689            flow_rate = float(fields[1].strip())
690            barrel_velocity = float(fields[2].strip())
691
692            rating_curve.append([head_difference, flow_rate, barrel_velocity])
693
694        if i == 0:
695            # Header
696            continue
697        if i == 1:
698            # Metadata
699            fields = line.split(',')
700            label=fields[0].strip()
701            type=fields[1].strip().lower()
702            assert type in ['box', 'pipe']
703
704            width=float(fields[2].strip())
705            height=float(fields[3].strip())
706            length=float(fields[4].strip())
707            number_of_barrels=int(fields[5].strip())
708            #fields[6] refers to losses
709            description=fields[7].strip()
710
711        if line.strip() == '': continue # Skip blanks
712
713        if line.startswith('Rating'):
714            read_rating_curve_data = True
715            # Flow data follows
716
717    fid.close()
718
719    return label, type, width, height, length, number_of_barrels, description, rating_curve
720
721           
722
723Culvert_flow = Culvert_flow_general       
Note: See TracBrowser for help on using the repository browser.