[Bio] / FigKernelScripts / dose_response_sheets.pl Repository:
ViewVC logotype

Annotation of /FigKernelScripts/dose_response_sheets.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.1 - (view) (download) (as text)

1 : parrello 1.1 #!/usr/bin/env perl
2 :     #
3 :     # Copyright (c) 2003-2015 University of Chicago and Fellowship
4 :     # for Interpretations of Genomes. All Rights Reserved.
5 :     #
6 :     # This file is part of the SEED Toolkit.
7 :     #
8 :     # The SEED Toolkit is free software. You can redistribute
9 :     # it and/or modify it under the terms of the SEED Toolkit
10 :     # Public License.
11 :     #
12 :     # You should have received a copy of the SEED Toolkit Public License
13 :     # along with this program; if not write to the University of Chicago
14 :     # at info@ci.uchicago.edu or the Fellowship for Interpretation of
15 :     # Genomes at veronika@thefig.info or download a copy from
16 :     # http://www.theseed.org/LICENSE.TXT.
17 :     #
18 :    
19 :    
20 :     use strict;
21 :     use warnings;
22 :     use FIG_Config;
23 :     use ScriptUtils;
24 :     use Stats;
25 :     use IC50;
26 :     my $mode;
27 :     eval {
28 :     require Excel::Writer::XLSX;
29 :     $mode = 'x';
30 :     print "Excel::Writer loaded.\n";
31 :     };
32 :     if ($@) {
33 :     require Spreadsheet::WriteExcel;
34 :     $mode = '';
35 :     print "WriteExcel loaded.\n";
36 :     }
37 :    
38 :     =head1 Extract Dose Response Data from PATRIC Files Into a Spreadsheet
39 :    
40 :     dose_response_sheets.pl [ options ] pDir drugFile lineFile
41 :    
42 :     This script will access a PATRIC dose-response directory and extract data for specified cell lines and drugs.
43 :     Each basic source type (e.g. C<CCLE>, C<NCI60>) has files in the C<drugs>, C<dose_response>, and C<cell_lines>
44 :     directories. All files are tab-delimited with headers. Cell Line name mappings have the file name I<XXXX>C<_cl>
45 :     in the C<cell_lines> directory. The first column is the ID used in the dose-response file and the third is the
46 :     clean name expected as input. Drug name mappings have the file name I<XXXX>C<_drugs> in the C<drugs> directory.
47 :     Again, the first column is the ID used in the dose-response file and the third is the clean name. The actual data
48 :     is in a file called C<combined_single_drug_growth> in the C<dose_response> directory. Each line contains the source
49 :     in the first column, the drug ID in the second column, the cell line ID in the third column, the concentration unit
50 :     in the fourth column, the log of the concentration in the fifth, and the percent growth in the seventh column.
51 :    
52 :     Dose-reponse lines that contain both a desired drug and a desired cell line will be output into an Excel Workbook.
53 :     There will be one sheet for each cell/drug pair. The A column will be the log dosage and each successive column
54 :     will contain the growth rate for a single source. Only one of the growth rate columns will have a value. This
55 :     makes it easier to generate Excel charts from the output.
56 :    
57 :     =head2 Parameters
58 :    
59 :     The positional parameters are the name of the PATRIC data directory, the name of the file containing the cleaned drug
60 :     names, and the name of the file containing the cleaned cell line names. The name files should contain one name per line.
61 :    
62 :     The command-line options are the following.
63 :    
64 :     =over 4
65 :    
66 :     =item workbook
67 :    
68 :     The name of the output spreadsheet. If the file exists, it will be destroyed. The default is C<growth.xls> in the
69 :     input directory.
70 :    
71 :     =item prob
72 :    
73 :     The name of the probability file. This file is tab-delimited, with two header rows. Each data row starts with a CCLE
74 :     cell line ID. For each drug, there are three columns of data, headed by a drug ID. The first such column
75 :     (with a sub-head of C<Prob>) contains the probability rating of the drug / cell-line combination. This value will be
76 :     put on the IC50 page in a column headed by a source type followed by C<-R>.
77 :    
78 :     =item predictions
79 :    
80 :     The name of the predictions file. This file is tab-delimited, with one header row. Each data row starts with a CCLE
81 :     cell line ID. For each drug, there is a column containing a prediction of growth modification for the drug / cell line
82 :     combination. This value will be put on the IC50 page in a column headed by a source type followed by C<-P>.
83 :    
84 :     =item ic50only
85 :    
86 :     If specified, the individual pages will not be produced, only the IC50 page. Use this when there are a lot of drug/line
87 :     combinations.
88 :    
89 :     =back
90 :    
91 :     =cut
92 :    
93 :     # list of report types
94 :     use constant TYPES => [qw(CCLE CTRP GDSC SCLC NCI60 gCSI)];
95 :     use constant TYPEH => { CCLE => 1, CTRP => 2, GDSC => 3, SCLC => 4, NCI60 => 5, ALMANAC => 6, gCSI => 7, NCI60M => 8, ALMANACM => 9 };
96 :     use constant IC50H => { GDSC => 12, NCI60 => 13 };
97 :     use constant IC50L => 10;
98 :    
99 :     $| = 1;
100 :     # Get the command-line parameters.
101 :     my $opt = ScriptUtils::Opts('pDir drugFile lineFile',
102 :     ['workbook|wb|w=s', 'output file name'],
103 :     ['predictions|p=s', 'prediction file'],
104 :     ['prob|P=s', 'probability file'],
105 :     ['ic50only', 'only create IC50 sheet']
106 :     );
107 :     # Validate the parameters.
108 :     my ($pDir, $drugFile, $lineFile) = @ARGV;
109 :     if (! $pDir) {
110 :     die "No input directory specified.";
111 :     } elsif (! -d $pDir) {
112 :     die "$pDir is missing or invalid.";
113 :     } elsif (! $drugFile) {
114 :     die "No drug name file specified.";
115 :     } elsif (! -s $drugFile) {
116 :     die "$drugFile is missing or empty.";
117 :     } elsif (! $lineFile) {
118 :     die "No cell line name file specified.";
119 :     } elsif (! -s $lineFile) {
120 :     die "$lineFile is missing or empty.";
121 :     }
122 :     my $stats = Stats->new();
123 :     # Save the options.
124 :     my $allSheets = ! $opt->ic50only;
125 :     # Compute the output file name and create the spreadsheet writer.
126 :     my $output = $opt->workbook // "$pDir/growth.xls$mode";
127 :     if (-f $output) {
128 :     print "Deleting old $output.\n";
129 :     unlink $output;
130 :     }
131 :     my $workbook;
132 :     print "Creating $output.\n";
133 :     if ($mode) {
134 :     $workbook = Excel::Writer::XLSX->new($output);
135 :     } else {
136 :     $workbook = Spreadsheet::WriteExcel->new($output);
137 :     }
138 :     if (! $workbook) {
139 :     die "Could not create workbook.";
140 :     }
141 :     print "Spreadsheet created as $output.\n";
142 :     # Read in the cell line and drug IDs of interest.
143 :     my $clHash = ReadNames("$pDir/cell_lines", cl => $lineFile);
144 :     my $drugHash = ReadNames("$pDir/drugs", drugs => $drugFile);
145 :     # We are now going to set up the prediction/probability numbers. This tracks the next available
146 :     # column on the IC50 page.
147 :     my $sCol = IC50L + 1;
148 :     # This is a three-level hash. It contains the predicted probability of success for each drug/cl pair
149 :     # for each version of the drug. The third key is the source (taken from the column header drug ID).
150 :     my %probMap;
151 :     # This hash contains the sources found, mapping each one to a column number.
152 :     my %probType;
153 :     if ($opt->prob) {
154 :     open(my $ph, '<', $opt->prob) || die "Could not open probability file: $!";
155 :     # Read the headers. This hash will map each drug ID to a column number.
156 :     my %drugCols;
157 :     my @cols = ScriptUtils::get_line($ph);
158 :     my @data = ScriptUtils::get_line($ph);
159 :     for (my $i = 0; $i < @cols; $i++) {
160 :     if ($drugHash->{$cols[$i]} && $data[$i] eq 'Prob') {
161 :     $drugCols{$cols[$i]} = $i;
162 :     $stats->Add(probCol => 1);
163 :     }
164 :     }
165 :     print scalar(keys %drugCols) . " drug columns found in probability file.\n";
166 :     # Now loop through the cell lines, saving the scores.
167 :     my $count = 0;
168 :     while (! eof $ph) {
169 :     @data = ScriptUtils::get_line($ph);
170 :     my $line = $clHash->{$data[0]};
171 :     if ($line) {
172 :     for my $drug (keys %drugCols) {
173 :     my $dname = $drugHash->{$drug};
174 :     my ($source) = split /\./, $drug;
175 :     $probMap{$dname}{$line}{$source} = $data[$drugCols{$drug}];
176 :     $count++;
177 :     $probType{$source} = 1;
178 :     }
179 :     $stats->Add(probLine => 1);
180 :     }
181 :     }
182 :     print "$count probability values stored.\n";
183 :     # Set up the sources.
184 :     for my $source (sort keys %probType) {
185 :     $probType{$source} = $sCol;
186 :     $sCol++;
187 :     $stats->Add(probType => 1);
188 :     }
189 :     }
190 :     # This is another three-level hash. It contains the predicted growth reduction for each drug/cl pair
191 :     # for each version of the drug. The third key is the source (taken from the column header drug ID).
192 :     my %predMap;
193 :     # This hash contains the sources found, mapping each one to a column number.
194 :     my %predType;
195 :     if ($opt->predictions) {
196 :     open(my $ph, '<', $opt->predictions) || die "Could not open predictions file: $!";
197 :     # Read the headers. This hash will map each drug ID to a column number.
198 :     my %drugCols;
199 :     my @cols = ScriptUtils::get_line($ph);
200 :     for (my $i = 0; $i < @cols; $i++) {
201 :     if ($drugHash->{$cols[$i]}) {
202 :     $drugCols{$cols[$i]} = $i;
203 :     $stats->Add(predCol => 1);
204 :     }
205 :     }
206 :     print scalar(keys %drugCols) . " drug columns found in predictions file.\n";
207 :     # Now loop through the cell lines, saving the scores.
208 :     my $count = 0;
209 :     while (! eof $ph) {
210 :     my @data = ScriptUtils::get_line($ph);
211 :     my $line = $clHash->{$data[0]};
212 :     if ($line) {
213 :     for my $drug (keys %drugCols) {
214 :     my $dname = $drugHash->{$drug};
215 :     my ($source) = split /\./, $drug;
216 :     $predMap{$dname}{$line}{$source} = $data[$drugCols{$drug}];
217 :     $count++;
218 :     $predType{$source} = 1;
219 :     }
220 :     $stats->Add(predLine => 1);
221 :     }
222 :     }
223 :     print "$count prediction values stored.\n";
224 :     # Set up the sources.
225 :     for my $source (sort keys %predType) {
226 :     $predType{$source} = $sCol;
227 :     $sCol++;
228 :     $stats->Add(predType => 1);
229 :     }
230 :     }
231 :     # This is a two-level hash. It contains a list of [dosage,growth] tuples for each drug/cl pair.
232 :     # We use it to sort and process the data.
233 :     my %growthMap;
234 :     # Connect to the input file.
235 :     print "Processing input file.\n";
236 :     open(my $ih, "<$pDir/dose_response/combined_single_drug_growth") || die "Could not open input file: $!";
237 :     my ($count, $found) = (0, 0);
238 :     # Discard the header.
239 :     my $line = <$ih>;
240 :     # Loop through the data.
241 :     while (! eof $ih) {
242 :     my ($type, $drug, $cl, undef, $dosage, undef, $growth) = ScriptUtils::get_line($ih);
243 :     $stats->Add(lineIn => 1);
244 :     # Check to see if we care about this data line.
245 :     my $dName = $drugHash->{$drug};
246 :     my $clName = $clHash->{$cl};
247 :     if ($dName && $clName) {
248 :     # Yes we do.
249 :     $stats->Add(lineUsed => 1);
250 :     push @{$growthMap{$dName}{$clName}{$type}}, [$dosage, $growth];
251 :     $found++;
252 :     } elsif (! $dName) {
253 :     $stats->Add(lineNotDrug => 1);
254 :     } else {
255 :     $stats->Add(lineDrugNotCl => 1);
256 :     }
257 :     $count++;
258 :     print "$count lines processed. $found used.\n" if $count % 100000 == 0;
259 :     }
260 :     close $ih; undef $ih;
261 :     print "Processing ALMANAC values.\n";
262 :     ($count, $found) = (0, 0);
263 :     open($ih, "<$pDir/dose_response/growth.tbl") || die "Could not open ALMANAC file: $!";
264 :     # This is the output column for almanac.
265 :     my $col = TYPEH->{ALMANAC};
266 :     # Skip the header line.
267 :     $line = <$ih>;
268 :     while (! eof $ih) {
269 :     my @cols = ScriptUtils::get_line($ih);
270 :     $stats->Add(almanacIn => 1);
271 :     if ($cols[14]) {
272 :     $stats->Add(almanacDualLine => 1);
273 :     } else {
274 :     # Here we have a single-drug data line. Drugs are NSC numbers.
275 :     my $drug = "NSC.$cols[8]";
276 :     my $dName = $drugHash->{$drug};
277 :     # Cell lines are NCI60 identifiers.
278 :     my $cl = "NCI60.$cols[28]";
279 :     my $clName = $clHash->{$cl};
280 :     if ($dName && $clName) {
281 :     $stats->Add(almanacFound => 1);
282 :     my $rawDosage = $cols[11];
283 :     my $dosage = log($rawDosage) / log(10);
284 :     my $growth = $cols[19];
285 :     push @{$growthMap{$dName}{$clName}{ALMANAC}}, [$dosage, $growth];
286 :     $stats->Add(almanacUsed => 1);
287 :     $found++;
288 :     } else {
289 :     $stats->Add(almanacSkipped => 1);
290 :     }
291 :     }
292 :     $count++;
293 :     print "$count lines processed. $found used.\n" if $count % 100000 == 0;
294 :     }
295 :     close $ih; undef $ih;
296 :     # Compute the mean values for the two high-volume sources.
297 :     for my $source (qw(NCI60 ALMANAC)) {
298 :     print "Computing mean growth rates for $source.\n";
299 :     my $meanType = $source . 'M';
300 :     $col = TYPEH->{$meanType};
301 :     for my $drug (keys %growthMap) {
302 :     my $clMap = $growthMap{$drug};
303 :     for my $cl (keys %$clMap) {
304 :     my $srcMap = $clMap->{$cl}{$source};
305 :     if ($srcMap) {
306 :     # Here we need to compute the mean value for each dosage. Get total and count.
307 :     my %growths;
308 :     my %counts;
309 :     for my $pair (@$srcMap) {
310 :     my ($dosage, $growth) = @$pair;
311 :     $growths{$dosage} += $growth;
312 :     $counts{$dosage}++;
313 :     }
314 :     # Now fill in the means.
315 :     for my $dosage (keys %growths) {
316 :     my $growth = $growths{$dosage}/$counts{$dosage};
317 :     push @{$growthMap{$drug}{$cl}{$meanType}}, [$dosage, $growth];
318 :     }
319 :     }
320 :     }
321 :     }
322 :     }
323 :     # Now we need to read in the IC50 values from the two downloaded files.
324 :     my %ic50;
325 :     for my $type (keys %{IC50H()}) {
326 :     ($count, $found) = (0, 0);
327 :     print "Reading IC50 results for $type.\n";
328 :     open($ih, "<$pDir/dose_response/${type}_IC50") || die "Could not open $type IC50 file: $!";
329 :     my $line = <$ih>;
330 :     while (! eof $ih) {
331 :     my ($drug, $cl, $dose) = ScriptUtils::get_line($ih);
332 :     $stats->Add("$type-ic50LineIn" => 1);
333 :     # Check to see if we care about this data line.
334 :     my $dName = $drugHash->{$drug};
335 :     my $clName = $clHash->{$cl};
336 :     if ($dName && $clName) {
337 :     # Yes we do.
338 :     $ic50{$dName}{$clName}{$type} = $dose;
339 :     $stats->Add("$type-ic50LineUsed" => 1);
340 :     $found++;
341 :     }
342 :     $count++;
343 :     print "$count lines processed. $found used.\n" if $count % 100000 == 0;
344 :     }
345 :     close $ih; undef $ih;
346 :     }
347 :     # We need an IC50 computer.
348 :     my $ic50Thing = IC50->new();
349 :     # Now we have all of the data. We must write it into the spreadsheet.
350 :     print "Filling in spreadsheet.\n";
351 :     # First, create the IC50 master sheet.
352 :     my $ic50Sheet = $workbook->add_worksheet("IC50");
353 :     $ic50Sheet->write_string(0, 0, "Drug");
354 :     $ic50Sheet->write_string(0, 1, "Cell line");
355 :     for my $type (keys %{TYPEH()}) {
356 :     $ic50Sheet->write_string(0, TYPEH->{$type} + 1, $type);
357 :     }
358 :     for my $type (keys %probType) {
359 :     $ic50Sheet->write_string(0, $probType{$type}, "$type-R");
360 :     }
361 :     for my $type (keys %predType) {
362 :     $ic50Sheet->write_string(0, $predType{$type}, "$type-P");
363 :     }
364 :     my $ic50Row = 0;
365 :     # Now process the drug / cell-line pairs.
366 :     for my $drug (sort keys %growthMap) {
367 :     print "Processing drug $drug.\n";
368 :     my $clMap = $growthMap{$drug};
369 :     my $ic50Map = $ic50{$drug} // {};
370 :     for my $cl (sort keys %$clMap) {
371 :     my $ic50H = $ic50Map->{$cl} // {};
372 :     print "Processing pair $drug $cl.\n";
373 :     $stats->Add(pairFound => 1);
374 :     # Here we need to open a sheet for this drug/CL pair.
375 :     my $sheet;
376 :     if ($allSheets) {
377 :     $sheet = $workbook->add_worksheet(substr("$drug $cl", 0, 31));
378 :     $sheet->write_string(2, 0, "Dosage");
379 :     for my $type (keys %{TYPEH()}) {
380 :     $sheet->write_string(2, TYPEH->{$type}, $type);
381 :     }
382 :     # The first row contains the offsets.
383 :     $sheet->write_string(0, 0, "Offset");
384 :     # The second row contains the computed IC50s.
385 :     $sheet->write_string(1, 0, "IC50");
386 :     # Past the end, the first two rows contain IC50 numbers from the web.
387 :     $sheet->write_string(1, IC50L, "IC50");
388 :     for my $type (keys %{IC50H()}) {
389 :     $sheet->write_string(0, IC50H->{$type}, $type);
390 :     my $value = $ic50H->{$type};
391 :     if (defined $value) {
392 :     $sheet->write_number(1, IC50H->{$type}, $value);
393 :     }
394 :     }
395 :     }
396 :     # Do the IC50 sheet stuff.
397 :     $ic50Row++;
398 :     $ic50Sheet->write_string($ic50Row, 0, $drug);
399 :     $ic50Sheet->write_string($ic50Row, 1, $cl);
400 :     # Get the map of source types to tuple lists. We must sort the lists and compute the offsets
401 :     # To get each one starting at 100.
402 :     print "Computing offsets and sorting dosages.\n";
403 :     my $pairMap = $clMap->{$cl};
404 :     my @types = keys %$pairMap;
405 :     for my $type (@types) {
406 :     my $tuples = $pairMap->{$type};
407 :     my @sorted = sort { $a->[0] <=> $b->[0] } @$tuples;
408 :     my $offset = 100 - $sorted[0][1];
409 :     if ($allSheets) {
410 :     $sheet->write_number(0, TYPEH->{$type}, $offset);
411 :     }
412 :     for my $tuple (@sorted) {
413 :     $tuple->[1] += $offset;
414 :     }
415 :     $pairMap->{$type} = \@sorted;
416 :     }
417 :     # Now compute and store the IC50s.
418 :     for my $type (@types) {
419 :     my $pairs = $pairMap->{$type};
420 :     my $ic50 = $ic50Thing->computeFromPairs($pairs);
421 :     if (defined $ic50) {
422 :     if ($allSheets) {
423 :     $sheet->write_number(1, TYPEH->{$type}, $ic50);
424 :     }
425 :     $ic50Sheet->write_number($ic50Row, TYPEH->{$type} + 1, $ic50);
426 :     $stats->Add(ic50Computed => 1);
427 :     } else {
428 :     $ic50Sheet->write_formula($ic50Row, TYPEH->{$type} + 1, "=NA()");
429 :     }
430 :     }
431 :     # Process the probabilities and predictions (if any).
432 :     for my $type (keys %probType) {
433 :     my $prob = $probMap{$drug}{$cl}{$type};
434 :     if (defined $prob) {
435 :     $ic50Sheet->write_number($ic50Row, $probType{$type}, $prob);
436 :     }
437 :     }
438 :     for my $type (keys %predType) {
439 :     my $pred = $predMap{$drug}{$cl}{$type};
440 :     if (defined $pred) {
441 :     $ic50Sheet->write_number($ic50Row, $predType{$type}, $pred);
442 :     }
443 :     }
444 :     if ($allSheets) {
445 :     # Now all the tuple lists are sorted and scaled. We do a sort of merge-y thing to put them
446 :     # into the output in dosage order. We start on row 2.
447 :     print "Writing data.\n";
448 :     my $row = 3;
449 :     while (@types) {
450 :     # Get the minimum of the top dosage in each type.
451 :     my $minDosage = 1000000;
452 :     for my $type (@types) {
453 :     my $dosage = $pairMap->{$type}[0][0];
454 :     if ($minDosage > $dosage) { $minDosage = $dosage; }
455 :     }
456 :     # Output everything at this dosage level. Note we keep a list of types with data still in them.
457 :     $sheet->write_number($row, 0, $minDosage);
458 :     my @keepers;
459 :     for my $type (@types) {
460 :     my $tuples = $pairMap->{$type};
461 :     if ($minDosage == $tuples->[0][0]) {
462 :     my $tuple = shift @$tuples;
463 :     if (@$tuples) {
464 :     push @keepers, $type;
465 :     }
466 :     $sheet->write_number($row, TYPEH->{$type}, $tuple->[1]);
467 :     } else {
468 :     push @keepers, $type;
469 :     }
470 :     }
471 :     @types = @keepers;
472 :     $row++;
473 :     }
474 :     }
475 :     }
476 :     }
477 :     print "Updating spreadsheet.\n";
478 :     $workbook->close();
479 :     print "All done.\n" . $stats->Show();
480 :    
481 :     ## Clean a drug or cell line name.
482 :     sub clean {
483 :     my ($name) = @_;
484 :     my $retVal = uc $name;
485 :     $retVal =~ s/[^A-Z0-9]//g;
486 :     return $retVal;
487 :     }
488 :    
489 :    
490 :     ## Create the id-to-name mappings for drugs or cell lines.
491 :     sub ReadNames {
492 :     my ($dir, $type, $inFile) = @_;
493 :     # This will be the return hash.
494 :     my %retVal;
495 :     # Read in the names of interest.
496 :     print "Reading $inFile for $type names.\n";
497 :     open(my $ih, "<$inFile") || die "Could not open $inFile: $!";
498 :     my %names;
499 :     while (! eof $ih) {
500 :     my $line = <$ih>;
501 :     chomp $line;
502 :     $stats->Add("$type-in" => 1);
503 :     $names{$line} = 1;
504 :     }
505 :     close $ih;
506 :     print scalar(keys %names) . " $type names specified.\n";
507 :     my %found;
508 :     # We need to process each data source.
509 :     for my $source (@{TYPES()}) {
510 :     # Open the data source's mapping file.
511 :     my $fname = $source . "_$type";
512 :     if (! -s "$dir/$fname") {
513 :     print "No file found for $source $type.\n";
514 :     } else {
515 :     open(my $dh, "<$dir/$fname") || die "Could not open $fname: $!";
516 :     # Skip the header.
517 :     my $line = <$dh>;
518 :     # Read the IDs.
519 :     while (! eof $dh) {
520 :     my ($id, undef, $name) = ScriptUtils::get_line($dh);
521 :     if (! $name) {
522 :     print "Missing value for for $id in $fname.\n";
523 :     } elsif ($names{$name}) {
524 :     $retVal{$id} = $name;
525 :     $stats->Add("$type-mapped" => 1);
526 :     $found{$name} = 1;
527 :     } else {
528 :     $stats->Add("$type-skipped" => 1);
529 :     }
530 :     }
531 :     }
532 :     }
533 :     for my $name (keys %names) {
534 :     if (! $found{$name}) {
535 :     print "No identifier found for $type $name.\n";
536 :     }
537 :     }
538 :     print scalar(keys %retVal) . " identifiers found for " . scalar(keys %found) . " $type names.\n";
539 :     # Return the hash table.
540 :     return \%retVal;
541 :     }

MCS Webmaster
ViewVC Help
Powered by ViewVC 1.0.3