source: coopr.pysp/stable/2.1/coopr/pysp/ph_script.py @ 1952

Last change on this file since 1952 was 1952, checked in by wehart, 10 years ago

Merged revisions 1882-1951 via svnmerge from
https://software.sandia.gov/svn/public/coopr/coopr.pysp/trunk

........

r1884 | wehart | 2009-11-14 00:11:40 -0700 (Sat, 14 Nov 2009) | 2 lines


Update to current revision number.

........

r1890 | jwatson | 2009-11-14 17:08:09 -0700 (Sat, 14 Nov 2009) | 3 lines


Added option to PH to fix all discrete variables that are converged upon termination. Functionality is within the WW PH extension. Not for typical use, but useful for debugging.

........

r1895 | jwatson | 2009-11-16 10:44:48 -0700 (Mon, 16 Nov 2009) | 3 lines


Changed PySP package manager information from Bill to JPW.

........

r1911 | jwatson | 2009-11-19 10:24:07 -0700 (Thu, 19 Nov 2009) | 3 lines


Restructured PH code to allow for general breakpoint distribution strategies. Added option to runph (--breakpoint-strategy) to specify particular strategies.

........

r1912 | jwatson | 2009-11-19 13:24:04 -0700 (Thu, 19 Nov 2009) | 3 lines


Added PH breakpoint distribution strategy that uniformly concentrates pieces between the observed node-min and node-max values, as opposed to the full lb/ub range.

........

r1913 | jwatson | 2009-11-19 13:46:13 -0700 (Thu, 19 Nov 2009) | 3 lines


Added Woodruff PH breakpoint distribution strategy.

........

r1914 | jwatson | 2009-11-19 15:22:56 -0700 (Thu, 19 Nov 2009) | 3 lines


Added exponentially biased breakpoint distribution technique to PH.

........

r1922 | jwatson | 2009-11-20 15:50:27 -0700 (Fri, 20 Nov 2009) | 3 lines


Fix to make PH compliant with recent solution re-structuring.

........

r1925 | jwatson | 2009-11-20 22:42:49 -0700 (Fri, 20 Nov 2009) | 3 lines


Fixes to make PySP consistent with the variable lower/upper bound changes described in the previous commit. Fixed a few bugs in the farmer example now that bounds specified through parameters are working correctly.

........

r1928 | jwatson | 2009-11-21 11:33:10 -0700 (Sat, 21 Nov 2009) | 3 lines


Fixed an issue with construction of piecewise linear breakpoints in PH - if the average was at a bound, divide-by-zero was triggering.

........

r1929 | jwatson | 2009-11-21 11:45:34 -0700 (Sat, 21 Nov 2009) | 3 lines


Fixed another numerical issue in breakpoint computation in PH.

........

r1930 | jwatson | 2009-11-21 12:53:31 -0700 (Sat, 21 Nov 2009) | 3 lines


Adding 288 scenario forestry instance, for a serious challenge!

........

r1931 | jwatson | 2009-11-21 13:01:18 -0700 (Sat, 21 Nov 2009) | 3 lines


Added a feasible/new chile 48 instance (the old, hand-constructed instance seemed to be infeasible), and the EF for the 288 scenario instance.

........

r1932 | jwatson | 2009-11-21 13:37:45 -0700 (Sat, 21 Nov 2009) | 3 lines


Bug fix to PH when LB/UB aren't specified and you are linearizing.

........

r1933 | jwatson | 2009-11-21 14:24:35 -0700 (Sat, 21 Nov 2009) | 3 lines


Updating documentation on forestry PySP example.

........

r1934 | jwatson | 2009-11-22 15:38:31 -0700 (Sun, 22 Nov 2009) | 3 lines


Fix to profiler option in PH.

........

  • Property svn:executable set to *
File size: 24.5 KB
Line 
1#  _________________________________________________________________________
2#
3#  Coopr: A COmmon Optimization Python Repository
4#  Copyright (c) 2009 Sandia Corporation.
5#  This software is distributed under the BSD License.
6#  Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
7#  the U.S. Government retains certain rights in this software.
8#  For more information, see the Coopr README.txt file.
9#  _________________________________________________________________________
10
11
12import sys
13import os
14from optparse import OptionParser
15
16import pyutilib.services
17import pyutilib.misc
18import textwrap
19import traceback
20
21# garbage collection control.
22import gc
23
24# for profiling
25import cProfile
26import pstats
27
28# for serializing
29import pickle
30
31from coopr.pysp.convergence import *
32from coopr.pysp.scenariotree import *
33from coopr.pysp.ph import *
34from coopr.pysp.ef import *
35from coopr.opt.base import SolverFactory
36from coopr.opt.parallel import SolverManagerFactory
37
38#
39# Setup command-line options
40#
41
42parser = OptionParser()
43parser.add_option("--verbose",
44                  help="Generate verbose output for both initialization and execution.",
45                  action="store_true",
46                  dest="verbose",
47                  default=False)
48parser.add_option("--model-directory",
49                  help="The directory in which all model (reference and scenario) definitions are stored. Default is \".\".",
50                  action="store",
51                  dest="model_directory",
52                  type="string",
53                  default=".")
54parser.add_option("--instance-directory",
55                  help="The directory in which all instance (reference and scenario) definitions are stored. Default is \".\".",
56                  action="store",
57                  dest="instance_directory",
58                  type="string",
59                  default=".")
60parser.add_option("--solver",
61                  help="The type of solver used to solve scenario sub-problems. Default is cplex.",
62                  action="store",
63                  dest="solver_type",
64                  type="string",
65                  default="cplex")
66parser.add_option("--solver-manager",
67                  help="The type of solver manager used to coordinate scenario sub-problem solves. Default is serial.",
68                  action="store",
69                  dest="solver_manager_type",
70                  type="string",
71                  default="serial")
72parser.add_option("--max-iterations",
73                  help="The maximal number of PH iterations. Default is 100.",
74                  action="store",
75                  dest="max_iterations",
76                  type="int",
77                  default=100)
78parser.add_option("--default-rho",
79                  help="The default (global) rho for all blended variables. Default is 1.",
80                  action="store",
81                  dest="default_rho",
82                  type="float",
83                  default=1.0)
84parser.add_option("--rho-cfgfile",
85                  help="The name of a configuration script to compute PH rho values. Default is None.",
86                  action="store",
87                  dest="rho_cfgfile",
88                  type="string",
89                  default=None)
90parser.add_option("--bounds-cfgfile",
91                  help="The name of a configuration script to set variable bound values. Default is None.",
92                  action="store",
93                  dest="bounds_cfgfile",
94                  default=None)
95parser.add_option("--enable-termdiff-convergence",
96                  help="Terminate PH based on the termdiff convergence metric. Default is True.",
97                  action="store_true",
98                  dest="enable_termdiff_convergence",
99                  default=True)
100parser.add_option("--enable-normalized-termdiff-convergence",
101                  help="Terminate PH based on the normalized termdiff convergence metric. Default is True.",
102                  action="store_true",
103                  dest="enable_normalized_termdiff_convergence",
104                  default=False)
105parser.add_option("--termdiff-threshold",
106                  help="The convergence threshold used in the term-diff and normalized term-diff convergence criteria. Default is 0.01.",
107                  action="store",
108                  dest="termdiff_threshold",
109                  type="float",
110                  default=0.01)
111parser.add_option("--enable-free-discrete-count-convergence",
112                  help="Terminate PH based on the free discrete variable count convergence metric. Default is False.",
113                  action="store_true",
114                  dest="enable_free_discrete_count_convergence",
115                  default=False)
116parser.add_option("--free-discrete-count-threshold",
117                  help="The convergence threshold used in the criterion based on when the free discrete variable count convergence criterion. Default is 20.",
118                  action="store",
119                  dest="free_discrete_count_threshold",
120                  type="float",
121                  default=20)
122parser.add_option("--enable-ww-extensions",
123                  help="Enable the Watson-Woodruff PH extensions plugin. Default is False.",
124                  action="store_true",
125                  dest="enable_ww_extensions",
126                  default=False)
127parser.add_option("--ww-extension-cfgfile",
128                  help="The name of a configuration file for the Watson-Woodruff PH extensions plugin. Default is wwph.cfg.",
129                  action="store",
130                  dest="ww_extension_cfgfile",
131                  type="string",
132                  default="")
133parser.add_option("--ww-extension-suffixfile",
134                  help="The name of a variable suffix file for the Watson-Woodruff PH extensions plugin. Default is wwph.suffixes.",
135                  action="store",
136                  dest="ww_extension_suffixfile",
137                  type="string",
138                  default="")
139parser.add_option("--user-defined-extension",
140                  help="The name of a python module specifying a user-defined PH extension plugin.",
141                  action="store",
142                  dest="user_defined_extension",
143                  type="string",
144                  default=None)
145parser.add_option("--write-ef",
146                  help="Upon termination, write the extensive form of the model - accounting for all fixed variables.",
147                  action="store_true",
148                  dest="write_ef",
149                  default=False)
150parser.add_option("--solve-ef",
151                  help="Following write of the extensive form model, solve it.",
152                  action="store_true",
153                  dest="solve_ef",
154                  default=False)
155parser.add_option("--ef-output-file",
156                  help="The name of the extensive form output file (currently only LP format is supported), if writing of the extensive form is enabled. Default is efout.lp.",
157                  action="store",
158                  dest="ef_output_file",
159                  type="string",
160                  default="efout.lp")
161parser.add_option("--suppress-continuous-variable-output",
162                  help="Eliminate PH-related output involving continuous variables.",
163                  action="store_true",
164                  dest="suppress_continuous_variable_output",
165                  default=False)
166parser.add_option("--keep-solver-files",
167                  help="Retain temporary input and output files for scenario sub-problem solves",
168                  action="store_true",
169                  dest="keep_solver_files",
170                  default=False)
171parser.add_option("--output-solver-logs",
172                  help="Output solver logs during scenario sub-problem solves",
173                  action="store_true",
174                  dest="output_solver_logs",
175                  default=False)
176parser.add_option("--output-ef-solver-log",
177                  help="Output solver log during the extensive form solve",
178                  action="store_true",
179                  dest="output_ef_solver_log",
180                  default=False)
181parser.add_option("--output-solver-results",
182                  help="Output solutions obtained after each scenario sub-problem solve",
183                  action="store_true",
184                  dest="output_solver_results",
185                  default=False)
186parser.add_option("--output-times",
187                  help="Output timing statistics for various PH components",
188                  action="store_true",
189                  dest="output_times",
190                  default=False)
191parser.add_option("--disable-warmstarts",
192                  help="Disable warm-start of scenario sub-problem solves in PH iterations >= 1",
193                  action="store_true",
194                  dest="disable_warmstarts",
195                  default=False)
196parser.add_option("--drop-proximal-terms",
197                  help="Eliminate proximal terms (i.e., the quadratic penalty terms) from the weighted PH objective",
198                  action="store_true",
199                  dest="drop_proximal_terms",
200                  default=False)
201parser.add_option("--retain-quadratic-binary-terms",
202                  help="Do not linearize PH objective terms involving binary decision variables",
203                  action="store_true",
204                  dest="retain_quadratic_binary_terms",
205                  default=False)
206parser.add_option("--linearize-nonbinary-penalty-terms",
207                  help="Approximate the PH quadratic term for non-binary variables with a piece-wise linear function, using the supplied number of equal-length pieces from each bound to the average",
208                  action="store",
209                  dest="linearize_nonbinary_penalty_terms",
210                  type="int",
211                  default=0)
212parser.add_option("--breakpoint-strategy",
213                  help="Specify the strategy to distribute breakpoints on the [lb, ub] interval of each variable when linearizing. 0 indicates uniform distribution. 1 indicates breakpoints at the node min and max, uniformly in-between. 2 indicates more aggressive concentration of breakpoints near the observed node min/max.",
214                  action="store",
215                  dest="breakpoint_strategy",
216                  type="int",
217                  default=0)
218parser.add_option("--checkpoint-interval",
219                  help="The number of iterations between writing of a checkpoint file. Default is 0, indicating never.",
220                  action="store",
221                  dest="checkpoint_interval",
222                  type="int",
223                  default=0)
224parser.add_option("--restore-from-checkpoint",
225                  help="The name of the checkpoint file from which PH should be initialized. Default is "", indicating no checkpoint restoration",
226                  action="store",
227                  dest="restore_from_checkpoint",
228                  type="string",
229                  default="")
230parser.add_option("--profile",
231                  help="Enable profiling of Python code.  The value of this option is the number of functions that are summarized.",
232                  action="store",
233                  dest="profile",
234                  type="int",
235                  default=0)
236parser.add_option("--enable-gc",
237                  help="Enable the python garbage collecter.",
238                  action="store_true",
239                  dest="enable_gc",
240                  default=False)
241
242
243parser.usage="runph [options]"
244
245#
246# The main PH initialization / runner routine.
247#
248
249def run_ph(options, args):
250
251   start_time = time.time()
252
253   ph = None
254
255   # if we are restoring from a checkpoint file, do so - otherwise, construct PH from scratch.
256   if len(options.restore_from_checkpoint) > 0:
257
258      # we need to load the reference model, as pickle doesn't save contents of .py files!
259      try:
260         reference_model_filename = options.model_directory+os.sep+"ReferenceModel.py"
261         if options.verbose is True:
262            print "Scenario reference model filename="+reference_model_filename
263         model_import = pyutilib.misc.import_file(reference_model_filename)
264         if "model" not in dir(model_import):
265            print ""
266            print "***ERROR: Exiting test driver: No 'model' object created in module "+reference_model_filename
267            return
268
269         if model_import.model is None:
270            print ""
271            print "***ERROR: Exiting test driver: 'model' object equals 'None' in module "+reference_model_filename
272            return
273 
274         reference_model = model_import.model
275      except IOError:
276         print "***ERROR: Failed to load scenario reference model from file="+reference_model_filename
277         return     
278
279      # import the saved state
280     
281      try:
282         checkpoint_file = open(options.restore_from_checkpoint,"r")
283         ph = pickle.load(checkpoint_file)
284         checkpoint_file.close()
285         
286#         print "PH=",ph
287#         ph._scenario_tree.pprint()         
288      except IOError, msg:
289         raise RuntimeError, msg
290
291      # tell PH to build the right solver manager and solver TBD - AND PLUGINS, BUT LATER
292
293      raise RuntimeError, "Checkpoint restoration is not fully supported/tested yet!"
294     
295   else:
296      #
297      # create and populate the reference model/instance pair.
298      #
299
300      reference_model = None
301      reference_instance = None
302
303      try:
304         reference_model_filename = options.model_directory+os.sep+"ReferenceModel.py"
305         if options.verbose is True:
306            print "Scenario reference model filename="+reference_model_filename
307         model_import = pyutilib.misc.import_file(reference_model_filename)
308         if "model" not in dir(model_import):
309            print ""
310            print "***ERROR: Exiting test driver: No 'model' object created in module "+reference_model_filename
311            return
312
313         if model_import.model is None:
314            print ""
315            print "***ERROR: Exiting test driver: 'model' object equals 'None' in module "+reference_model_filename
316            return
317 
318         reference_model = model_import.model
319      except IOError:
320         print "***ERROR: Failed to load scenario reference model from file="+reference_model_filename
321         return
322
323      try:
324         reference_instance_filename = options.instance_directory+os.sep+"ReferenceModel.dat"
325         if options.verbose is True:
326            print "Scenario reference instance filename="+reference_instance_filename
327         reference_instance = reference_model.create(reference_instance_filename)
328      except IOError:
329         print "***ERROR: Failed to load scenario reference instance data from file="+reference_instance_filename
330         return     
331
332      #
333      # create and populate the scenario tree model
334      #
335
336      scenario_tree_model = None
337      scenario_tree_instance = None
338
339      try:
340         scenario_tree_model_filename = options.model_directory+os.sep+"ScenarioStructure.py"
341         if options.verbose is True:
342            print "Scenario tree model filename="+scenario_tree_model_filename
343         scenario_tree_import = pyutilib.misc.import_file(scenario_tree_model_filename)
344         scenario_tree_model = scenario_tree_import.model
345      except IOError:
346         print "***ERROR: Failed to load scenario tree reference model from file="+scenario_tree_model_filename
347         return   
348
349      try:
350         scenario_tree_instance_filename = options.instance_directory+os.sep+"ScenarioStructure.dat"
351         if options.verbose is True:
352            print "Scenario tree instance filename="+scenario_tree_instance_filename
353         scenario_tree_instance = scenario_tree_model.create(scenario_tree_instance_filename)
354      except IOError:
355         print "***ERROR: Failed to load scenario tree reference instance data from file="+scenario_tree_instance_filename
356         return
357   
358      #
359      # construct the scenario tree
360      #
361      scenario_tree = ScenarioTree(model=reference_instance,
362                                   nodes=scenario_tree_instance.Nodes,
363                                   nodechildren=scenario_tree_instance.Children,
364                                   nodestages=scenario_tree_instance.NodeStage,
365                                   nodeprobabilities=scenario_tree_instance.ConditionalProbability,
366                                   stages=scenario_tree_instance.Stages,
367                                   stagevariables=scenario_tree_instance.StageVariables,
368                                   stagecostvariables=scenario_tree_instance.StageCostVariable,
369                                   scenarios=scenario_tree_instance.Scenarios,
370                                   scenarioleafs=scenario_tree_instance.ScenarioLeafNode,
371                                   scenariobaseddata=scenario_tree_instance.ScenarioBasedData)
372
373      #
374      # print the input tree for validation/information purposes.
375      #
376      if options.verbose is True:
377         scenario_tree.pprint()
378
379      #
380      # validate the tree prior to doing anything serious
381      #
382      print ""
383      if scenario_tree.validate() is False:
384         print "***ERROR: Scenario tree is invalid****"
385         return
386      else:
387         if options.verbose is True:
388            print "Scenario tree is valid!"
389      print ""
390
391      #
392      # if any of the ww extension configuration options are specified without the
393      # ww extension itself being enabled, halt and warn the user - this has led
394      # to confusion in the past, and will save user support time.
395      #
396      if len(options.ww_extension_cfgfile) > 0 and options.enable_ww_extensions is False:
397         print "***ERROR: A configuration file was specified for the WW extension module, but the WW extensions are not enabled!"
398         return
399
400      if len(options.ww_extension_suffixfile) > 0 and options.enable_ww_extensions is False:
401         print "***ERROR: A suffix file was specified for the WW extension module, but the WW extensions are not enabled!"         
402         return
403
404      #
405      # if a breakpoint strategy is specified without linearization eanbled, halt and warn the user.
406      #
407      if (options.breakpoint_strategy > 0) and (options.linearize_nonbinary_penalty_terms == 0):
408         print "***ERROR: A breakpoint distribution strategy was specified, but linearization is not enabled!"
409         return         
410
411      #
412      # deal with any plugins. ww extension comes first currently, followed by an option user-defined plugin.
413      # order only matters if both are specified.
414      #
415      if options.enable_ww_extensions is True:
416
417         from coopr.pysp import wwphextension
418
419         plugin = ExtensionPoint(IPHExtension)
420         if len(options.ww_extension_cfgfile) > 0:
421            plugin.service()._configuration_filename = options.ww_extension_cfgfile
422         if len(options.ww_extension_suffixfile) > 0:
423            plugin.service()._suffix_filename = options.ww_extension_suffixfile
424
425      if options.user_defined_extension is not None:
426         print "User-defined PH extension specified - module name="+options.user_defined_extension
427         try:
428            __import__(options.user_defined_extension)
429         except:
430            raise RuntimeError, "Failed to load user-defined module="+options.user_defined_extension
431
432      #
433      # construct the convergence "computer" class.
434      #
435      converger = None
436      # go with the non-defaults first, and then with the default.
437      if options.enable_free_discrete_count_convergence is True:
438         converger = NumFixedDiscreteVarConvergence(convergence_threshold=options.free_discrete_count_threshold)
439      elif options.enable_normalized_termdiff_convergence is True:
440         converger = NormalizedTermDiffConvergence(convergence_threshold=options.termdiff_threshold)     
441      else:
442         converger = TermDiffConvergence(convergence_threshold=options.termdiff_threshold)     
443
444     
445      #
446      # construct and initialize PH
447      #
448      ph = ProgressiveHedging(max_iterations=options.max_iterations, \
449                              rho=options.default_rho, \
450                              rho_setter=options.rho_cfgfile, \
451                              bounds_setter=options.bounds_cfgfile, \
452                              solver=options.solver_type, \
453                              solver_manager=options.solver_manager_type, \
454                              keep_solver_files=options.keep_solver_files, \
455                              output_solver_log=options.output_solver_logs, \
456                              output_solver_results=options.output_solver_results, \
457                              verbose=options.verbose, \
458                              output_times=options.output_times, \
459                              disable_warmstarts=options.disable_warmstarts,
460                              drop_proximal_terms=options.drop_proximal_terms,
461                              retain_quadratic_binary_terms=options.retain_quadratic_binary_terms, \
462                              linearize_nonbinary_penalty_terms=options.linearize_nonbinary_penalty_terms, \
463                              breakpoint_strategy=options.breakpoint_strategy, \
464                              checkpoint_interval=options.checkpoint_interval)
465   
466      ph.initialize(scenario_data_directory_name=options.instance_directory, \
467                    model=reference_model, \
468                    model_instance=reference_instance, \
469                    scenario_tree=scenario_tree, \
470                    converger=converger)
471
472      if options.suppress_continuous_variable_output is True:
473         ph._output_continuous_variable_stats = False # clutters up the screen, when we really only care about the binaries.     
474
475   #
476   # at this point, we have a PH object by some means.
477   #
478
479   #
480   # kick off the solve
481   #
482   ph.solve()
483
484   print ""
485   print "DONE..."
486
487   end_time = time.time()
488
489   print ""
490   print "Total execution time=%8.2f seconds" %(end_time - start_time)
491   print ""
492   if options.output_times is True:
493      ph.print_time_stats()
494
495   #
496   # write the extensive form, accounting for any fixed variables.
497   #
498   if (options.write_ef is True) or (options.solve_ef is True):
499      print ""
500      print "Writing EF for remainder problem"
501      print ""
502      write_ef(ph._scenario_tree, ph._instances, options.ef_output_file)
503
504   #
505   # solve the extensive form.
506   #
507   if options.solve_ef is True:
508      print ""
509      print "Solving extensive form written to file="+options.ef_output_file
510      print ""
511
512      ef_solver = SolverFactory(options.solver_type)
513      if ef_solver is None:
514         raise ValueError, "Failed to create solver of type="+options.solver_type+" for use in extensive form solve"
515
516      ef_solver_manager = SolverManagerFactory(options.solver_manager_type)
517      if ef_solver is None:
518         raise ValueError, "Failed to create solver manager of type="+options.solver_type+" for use in extensive form solve"
519
520      print "Queuing extensive form solve"
521      ef_action_handle = ef_solver_manager.queue(options.ef_output_file, opt=ef_solver, warmstart=False, tee=options.output_ef_solver_log)
522      print "Waiting for extensive form solve"
523      ef_results = ef_solver_manager.wait_for(ef_action_handle)
524      print "Extensive form solve results:"
525      print ef_results
526     
527
528def run(args=None):
529
530    #
531    # Top-level command that executes the extensive form writer.
532    # This is segregated from run_ef_writer to enable profiling.
533    #
534
535    #
536    # Parse command-line options.
537    #
538    try:
539       (options, args) = parser.parse_args(args=args)
540    except SystemExit:
541       # the parser throws a system exit if "-h" is specified - catch
542       # it to exit gracefully.
543       return
544
545    # for a one-pass execution, garbage collection doesn't make
546    # much sense - so it is disabled by default. Because: It drops
547    # the run-time by a factor of 3-4 on bigger instances.
548    if options.enable_gc is False:
549       gc.disable()
550    else:
551       gc.enable()
552
553    if options.profile > 0:
554        #
555        # Call the main PH routine with profiling.
556        #
557        tfile = pyutilib.services.TempfileManager.create_tempfile(suffix=".profile")
558        tmp = cProfile.runctx('run_ph(options,args)',globals(),locals(),tfile)
559        p = pstats.Stats(tfile).strip_dirs()
560        p.sort_stats('time', 'cum')
561        p = p.print_stats(options.profile)
562        p.print_callers(options.profile)
563        p.print_callees(options.profile)
564        p = p.sort_stats('cum','calls')
565        p.print_stats(options.profile)
566        p.print_callers(options.profile)
567        p.print_callees(options.profile)
568        p = p.sort_stats('calls')
569        p.print_stats(options.profile)
570        p.print_callers(options.profile)
571        p.print_callees(options.profile)
572        pyutilib.services.TempfileManager.clear_tempfiles()
573        ans = [tmp, None]
574    else:
575        #
576        # Call the main PH routine without profiling.
577        #
578        ans = run_ph(options, args)
579
580    gc.enable()
581   
582    return ans
583
Note: See TracBrowser for help on using the repository browser.