Parent Directory
|
Revision Log
Ok, there is the ans_array_filter added which allows for the answer to be coming from the ans_array.
1 #!/usr/local/bin/webwork-perl 2 3 BEGIN{ 4 be_strict(); 5 } 6 7 sub _PGmorematrixmacros_init{} 8 9 sub random_inv_matrix { ## Builds and returns a random invertible \$row by \$col matrix. 10 11 warn "Usage: \$new_matrix = random_inv_matrix(\$rows,\$cols)" 12 if (@_ != 2); 13 my $A = new Matrix($_[0],$_[1]); 14 my $A_lr = new Matrix($_[0],$_[1]); 15 my $det = 0; 16 my $safety=0; 17 while ($det == 0 and $safety < 6) { 18 foreach my $i (1..$_[0]) { 19 foreach my $j (1..$_[1]) { 20 $A->assign($i,$j,random(-9,9,1) ); 21 } 22 } 23 $A_lr = $A->decompose_LR(); 24 $det = $A_lr->det_LR(); 25 } 26 return $A; 27 } 28 29 sub swap_rows{ 30 31 warn "Usage: \$new_matrix = swap_rows(\$matrix,\$row1,\$row2);" 32 if (@_ != 3); 33 my $matrix = $_[0]; 34 my ($i,$j) = ($_[1],$_[2]); 35 warn "Error: Rows to be swapped must exist!" 36 if ($i>@$matrix or $j >@$matrix); 37 warn "Warning: Swapping the same row is pointless" 38 if ($i==$j); 39 my $cols = @{$matrix->[0]}; 40 my $B = new Matrix(@$matrix,$cols); 41 foreach my $k (1..$cols){ 42 $B->assign($i,$k,element $matrix($j,$k)); 43 $B->assign($j,$k,element $matrix($i,$k)); 44 } 45 return $B; 46 } 47 48 sub row_mult{ 49 50 warn "Usage: \$new_matrix = row_mult(\$matrix,\$scalar,\$row);" 51 if (@_ != 3); 52 my $matrix = $_[0]; 53 my ($scalar,$row) = ($_[1],$_[2]); 54 warn "Undefined row multiplication" 55 if ($row > @$matrix); 56 my $B = new Matrix(@$matrix,@{$matrix->[0]}); 57 foreach my $j (1..@{$matrix->[0]}) { 58 $B->assign($row,$j,$scalar*element $matrix($row,$j)); 59 } 60 return $B; 61 } 62 63 sub linear_combo{ 64 65 warn "Usage: \$new_matrix = linear_combo(\$matrix,\$scalar,\$row1,\$row2);" 66 if (@_ != 4); 67 my $matrix = $_[0]; 68 my ($scalar,$row1,$row2) = ($_[1],$_[2],$_[3]); 69 warn "Undefined row in multiplication" 70 if ($row1>@$matrix or $row2>@$matrix); 71 warn "Warning: Using the same row" 72 if ($row1==$row2); 73 my $B = new Matrix(@$matrix,@{$matrix->[0]}); 74 foreach my $j (1..@$matrix) { 75 my ($t1,$t2) = (element $matrix($row1,$j),element $matrix($row2,$j)); 76 $B->assign($row2,$j,$scalar*$t1+$t2); 77 } 78 return $B; 79 } 80 81 =head3 basis_cmp() 82 83 Compares a list of vectors by finding the change of coordinate matrix 84 from the Prof's vectors to the students, and then taking the determinant of 85 that to determine the existence of the change of coordinate matrix going the 86 other way. 87 88 ANS( basis_cmp( vectors_as_array_ref_in_array_ref, options_hash ) ); 89 90 1. a reference to an array of correct vectors 91 2. a hash with the following keys (all optional): 92 mode -- 'basis' (default) (only a basis allowed) 93 'orthogonal' (only an orthogonal basis is allowed) 94 'unit' (only unit vectors in the basis allowed) 95 'orthonormal' (only orthogonal unit vectors in basis allowed) 96 zeroLevelTol -- absolute tolerance to allow when answer is close 97 to zero 98 99 debug -- if set to 1, provides verbose listing of 100 hash entries throughout fliters. 101 102 help -- 'none' (default) (is quiet on all errors) 103 'dim' (Tells student if wrong number of vectors are entered) 104 'length' (Tells student if there is a vector of the wrong length) 105 'orthogonal' (Tells student if their vectors are not orthogonal) 106 (This is only in orthogonal mode) 107 'unit' (Tells student if there is a vector not of unit length) 108 (This is only in unit mode) 109 'orthonormal' (Gives errors from orthogonal and orthonormal) 110 (This is only in orthonormal mode) 111 'verbose' (Gives all the above answer messages) 112 113 Returns an answer evaluator. 114 115 EXAMPLES: 116 117 basis_cmp([[1,0,0],[0,1,0],[0,0,1]]) 118 -- correct answer is any basis for R^3. 119 basis_cmp([1,0,2,0],[0,1,0,0], 'mode'=>orthonormal ) 120 -- correct answer is any orthonormal basis 121 for this space such as: 122 [1/sqrt(3),0,2/sqrt(3),0],[0,1,0,0] 123 124 =cut 125 126 127 sub basis_cmp { 128 my $correctAnswer = shift; 129 my %opt = @_; 130 131 set_default_options( \%opt, 132 'zeroLevelTol' => $main::functZeroLevelTolDefault, 133 'debug' => 0, 134 'mode' => 'basis', 135 'help' => 'none', 136 ); 137 138 # produce answer evaluator 139 BASIS_CMP( 140 'correct_ans' => $correctAnswer, 141 'zeroLevelTol' => $opt{'zeroLevelTol'}, 142 'debug' => $opt{'debug'}, 143 'mode' => $opt{'mode'}, 144 'help' => $opt{'help'}, 145 ); 146 } 147 148 =head BASIS_CMP 149 150 Made to keep the same format as num_cmp and fun_cmp. 151 152 =cut 153 154 sub BASIS_CMP { 155 my %mat_params = @_; 156 my $zeroLevelTol = $mat_params{'zeroLevelTol'}; 157 158 # Check that everything is defined: 159 $mat_params{debug} = 0 unless defined($mat_params{debug}); 160 $zeroLevelTol = $main::functZeroLevelTolDefault unless defined $zeroLevelTol; 161 $mat_params{'zeroLevelTol'} = $zeroLevelTol; 162 163 ## This is where the correct answer should be checked someday. 164 my $matrix = Matrix->new_from_col_vecs($mat_params{'correct_ans'}); 165 166 #construct the answer evaluator 167 my $answer_evaluator = new AnswerEvaluator; 168 169 $answer_evaluator->{debug} = $mat_params{debug}; 170 171 $answer_evaluator->ans_hash( correct_ans => pretty_print($mat_params{correct_ans}), 172 rm_correct_ans => $matrix, 173 zeroLevelTol => $mat_params{zeroLevelTol}, 174 debug => $mat_params{debug}, 175 mode => $mat_params{mode}, 176 help => $mat_params{help}, 177 ); 178 179 $answer_evaluator->install_pre_filter(sub {my $rh_ans = shift; 180 $rh_ans->{student_ans} =~ s/\s+//g; # remove all whitespace 181 $rh_ans; 182 }); 183 184 $answer_evaluator->install_pre_filter(\&math_constants); 185 $answer_evaluator->install_pre_filter(sub{my $rh_ans = shift; my @options = @_; 186 if( $rh_ans->{ans_label} =~ /ArRaY/ ){ 187 $rh_ans = ans_array_filter($rh_ans,@options); 188 my @student_array = @{$rh_ans->{ra_student_ans}}; 189 my @array = (); 190 for( my $i = 0; $i < scalar(@student_array) ; $i ++ ) 191 { 192 push( @array, Matrix->new_from_array_ref($student_array[$i])); 193 } 194 $rh_ans->{ra_student_ans} = \@array; 195 $rh_ans; 196 }else{ 197 vec_list_string($rh_ans,@options); 198 } 199 200 });#ra_student_ans is now the students answer as an array of vectors 201 # anonymous subroutine to check dimension and length of the student vectors 202 # if either is wrong, the answer is wrong. 203 $answer_evaluator->install_pre_filter(sub{ 204 my $rh_ans = shift; 205 my $length = $rh_ans->{rm_correct_ans}->[1]; 206 my $dim = $rh_ans->{rm_correct_ans}->[2]; 207 if( $dim != scalar(@{$rh_ans->{ra_student_ans}})) 208 { 209 210 $rh_ans->{score} = 0; 211 if( $rh_ans->{help} =~ /dim|verbose/ ) 212 { 213 $rh_ans->throw_error('EVAL','You have entered the wrong number of vectors.'); 214 }else{ 215 $rh_ans->throw_error('EVAL'); 216 } 217 } 218 for( my $i = 0; $i < scalar( @{$rh_ans->{ra_student_ans} }) ; $i++ ) 219 { 220 if( $length != $rh_ans->{ra_student_ans}->[$i]->[1]) 221 { 222 $rh_ans->{score} = 0; 223 if( $rh_ans->{help} =~ /length|verbose/ ) 224 { 225 $rh_ans->throw_error('EVAL','You have entered vector(s) of the wrong length.'); 226 }else{ 227 $rh_ans->throw_error('EVAL'); 228 } 229 } 230 } 231 $rh_ans; 232 }); 233 # Install prefilter for various modes 234 if( $mat_params{mode} ne 'basis' ) 235 { 236 if( $mat_params{mode} =~ /orthogonal|orthonormal/ ) 237 { 238 $answer_evaluator->install_pre_filter(sub{ 239 my $rh_ans = shift; 240 my @vecs = @{$rh_ans->{ra_student_ans}}; 241 my ($i,$j) = (0,0); 242 my $num = scalar(@vecs); 243 my $length = $vecs[0]->[1]; 244 245 for( ; $i < $num ; $i ++ ) 246 { 247 for( $j = $i+1; $j < $num ; $j++ ) 248 { 249 my $sum = 0; 250 my $k = 0; 251 252 for( ; $k < $length; $k++ ) { 253 $sum += $vecs[$i]->[0][$k][0]*$vecs[$j]->[0][$k][0]; 254 } 255 256 if( $sum > $mat_params{zeroLevelTol} ) 257 { 258 $rh_ans->{score} = 0; 259 if( $rh_ans->{help} =~ /orthogonal|orthonormal|verbose/ ) 260 { 261 $rh_ans->throw_error('EVAL','You have entered vectors which are not orthogonal. '); 262 }else{ 263 $rh_ans->throw_error('EVAL'); 264 } 265 } 266 } 267 } 268 269 270 $rh_ans; 271 }); 272 } 273 274 if( $mat_params{mode} =~ /unit|orthonormal/ ) 275 { 276 $answer_evaluator->install_pre_filter(sub{ 277 my $rh_ans = shift; 278 my @vecs = @{$rh_ans->{ra_student_ans}}; 279 my $i = 0; 280 my $num = scalar(@vecs); 281 my $length = $vecs[0]->[1]; 282 283 for( ; $i < $num ; $i ++ ) 284 { 285 my $sum = 0; 286 my $k = 0; 287 288 for( ; $k < $length; $k++ ) { 289 $sum += $vecs[$i]->[0][$k][0]*$vecs[$i]->[0][$k][0]; 290 } 291 if( abs(sqrt($sum) - 1) > $mat_params{zeroLevelTol} ) 292 { 293 $rh_ans->{score} = 0; 294 295 if( $rh_ans->{help} =~ /unit|orthonormal|verbose/ ) 296 { 297 $rh_ans->throw_error('EVAL','You have entered vector(s) which are not of unit length.'); 298 }else{ 299 $rh_ans->throw_error('EVAL'); 300 } 301 } 302 } 303 304 305 $rh_ans; 306 }); 307 308 } 309 } 310 $answer_evaluator->install_evaluator(\&compare_basis, %mat_params); 311 $answer_evaluator->install_post_filter( 312 sub {my $rh_ans = shift; 313 if ($rh_ans->catch_error('SYNTAX') ) { 314 $rh_ans->{ans_message} = $rh_ans->{error_message}; 315 $rh_ans->clear_error('SYNTAX'); 316 } 317 if ($rh_ans->catch_error('EVAL') ) { 318 $rh_ans->{ans_message} = $rh_ans->{error_message}; 319 $rh_ans->clear_error('EVAL'); 320 } 321 $rh_ans; 322 } 323 ); 324 $answer_evaluator; 325 } 326 327 =head4 compare_basis 328 329 compare_basis( $ans_hash, %options); 330 331 {ra_student_ans}, # a reference to the array of students answer vectors 332 {rm_correct_ans}, # a reference to the correct answer matrix 333 %options 334 ) 335 336 =cut 337 338 sub compare_basis { 339 my ($rh_ans, %options) = @_; 340 my @ch_coord; 341 my @vecs = @{$rh_ans->{ra_student_ans}}; 342 343 # A lot of the follosing code was taken from Matrix::proj_coeff 344 # calling this method recursively would be a waste of time since 345 # the prof's matrix never changes and solve_LR is an expensive 346 # operation. This way it is only done once. 347 my $matrix = $rh_ans->{rm_correct_ans}; 348 my ($dim,$x_vector, $base_matrix); 349 my $errors = undef; 350 my $lin_space_tr= ~ $matrix; 351 $matrix = $lin_space_tr * $matrix; 352 my $matrix_lr = $matrix->decompose_LR(); 353 354 #finds the coefficient vectors for each of the students vectors 355 for( my $i = 0; $i < scalar(@{$rh_ans->{ra_student_ans}}) ; $i++ ) 356 { 357 358 $vecs[$i] = $lin_space_tr*$vecs[$i]; 359 ($dim,$x_vector, $base_matrix) = $matrix_lr->solve_LR($vecs[$i]); 360 push( @ch_coord, $x_vector ); 361 $errors = "A unique adapted answer could not be determined. Possibly the parameters have coefficient zero.<br> dim = $dim base_matrix is $base_matrix\n" if $dim; # only print if the dim is not zero. 362 } 363 364 if( defined($errors)) 365 { 366 $rh_ans->throw_error('EVAL', $errors) ; 367 }else{ 368 my $ch_coord_mat = Matrix->new_from_col_vecs(\@ch_coord);#creates change of coordinate matrix 369 #existence of this matrix implies that 370 #the all of the students answers are a 371 #linear combo of the prof's 372 $ch_coord_mat = $ch_coord_mat->decompose_LR(); 373 374 if( $ch_coord_mat->det_LR() > $options{zeroLevelTol} )# if the det of the change of coordinate matrix is 375 # non-zero, this implies the existence of an inverse 376 # which implies all of the prof's vectors are a linear 377 # combo of the students vectors, showing containment 378 # both ways. 379 { 380 # I think sometimes if the students space has the same dimension as the profs space it 381 # will get projected into the profs space even if it isn't a basis for that space. 382 # this just checks that the prof's matrix times the change of coordinate matrix is actually 383 #the students matrix 384 if( abs(Matrix->new_from_col_vecs(\@{$rh_ans->{ra_student_ans}}) - ($rh_ans->{rm_correct_ans})*(Matrix->new_from_col_vecs(\@ch_coord))) < $options{zeroLevelTol} ) 385 { 386 $rh_ans->{score} = 1; 387 }else{ 388 $rh_ans->{score} = 0; 389 } 390 } 391 else{ 392 $rh_ans->{score}=0; 393 } 394 } 395 $rh_ans; 396 397 } 398 399 400 =head 2 vec_list_string 401 402 This is a check_syntax type method (in fact I borrowed some of that method's code) for vector input. 403 The student needs to enter vectors like: [1,0,0],[1,2,3],[0,9/sqrt(10),1/sqrt(10)] 404 Each entry can contain functions and operations and the usual math constants (pi and e). 405 The vectors, however can not be added or multiplied or scalar multiplied by the student. 406 Most errors are handled well. Any error in an entry is caught by the PG_answer_eval like it is in num_cmp or fun_cmp. 407 Right now the method basically ignores every thing outside the vectors. Also, an unmatched open parenthesis is caught, 408 but a unmatched close parenthesis ends the vector, and since everything outside is ignored, no error is sent (other than the 409 later when the length of the vectors is checked. 410 In the end, the method returns an array of Matrix objects. 411 412 413 =cut 414 415 sub vec_list_string{ 416 my $rh_ans = shift; 417 my %options = @_; 418 my $i; 419 my $entry = ""; 420 my $char; 421 my @paren_stack; 422 my $length = length($rh_ans->{student_ans}); 423 my @temp; 424 my $j = 0; 425 my @answers; 426 my $paren; 427 my $display_ans; 428 429 for( $i = 0; $i < $length ; $i++ ) 430 { 431 $char = substr($rh_ans->{student_ans},$i,1); 432 433 if( $char =~ /\(|\[|\{/ ){ 434 push( @paren_stack, $char ) 435 } 436 437 if( !( $char =~ /\(|\[|\{/ && scalar(@paren_stack) == 1 ) ) 438 { 439 if( $char !~ /,|\)|\]|\}/ ){ 440 $entry .= $char; 441 }else{ 442 if( $char =~ /,/ || ( $char =~ /\)|\]|\}/ && scalar(@paren_stack) == 1 ) ) 443 { 444 if( length($entry) == 0 ){ 445 if( $char !~ /,/ ){ 446 $rh_ans->throw_error('EVAL','There is a syntax error in your answer'); 447 }else{ 448 $rh_ans->{preview_text_string} .= ","; 449 $rh_ans->{preview_latex_string} .= ","; 450 $display_ans .= ","; 451 } 452 }else{ 453 454 # This parser code was origianally taken from PGanswermacros::check_syntax 455 # but parts of it needed to be slighty modified for this context 456 my $parser = new AlgParserWithImplicitExpand; 457 my $ret = $parser -> parse($entry); #for use with loops 458 459 if ( ref($ret) ) { ## parsed successfully 460 $parser -> tostring(); 461 $parser -> normalize(); 462 $entry = $parser -> tostring(); 463 $rh_ans->{preview_text_string} .= $entry.","; 464 $rh_ans->{preview_latex_string} .= $parser -> tolatex().","; 465 466 } else { ## error in parsing 467 468 $rh_ans->{'student_ans'} = 'syntax error:'.$display_ans. $parser->{htmlerror}, 469 $rh_ans->{'ans_message'} = $display_ans.$parser -> {error_msg}, 470 $rh_ans->{'preview_text_string'} = '', 471 $rh_ans->{'preview_latex_string'} = '', 472 $rh_ans->throw_error('SYNTAX', 'syntax error in answer:'.$display_ans.$parser->{htmlerror} . "$main::BR" .$parser -> {error_msg}.".$main::BR"); 473 } 474 475 my ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($entry); 476 477 if ($PG_eval_errors) { 478 $rh_ans->throw_error('EVAL','There is a syntax error in your answer.') ; 479 $rh_ans->{ans_message} = clean_up_error_msg($PG_eval_errors); 480 last; 481 } else { 482 $entry = prfmt($inVal,$options{format}); 483 $display_ans .= $entry.","; 484 push(@temp , $entry); 485 } 486 487 if( $char =~ /\)|\]|\}/ && scalar(@paren_stack) == 1) 488 { 489 pop @paren_stack; 490 chop($rh_ans->{preview_text_string}); 491 chop($rh_ans->{preview_latex_string}); 492 chop($display_ans); 493 $rh_ans->{preview_text_string} .= "]"; 494 $rh_ans->{preview_latex_string} .= "]"; 495 $display_ans .= "]"; 496 if( scalar(@temp) > 0 ) 497 { 498 push( @answers,Matrix->new_from_col_vecs([\@temp])); 499 while(scalar(@temp) > 0 ){ 500 pop @temp; 501 } 502 }else{ 503 $rh_ans->throw_error('EVAL','There is a syntax error in your answer.'); 504 } 505 } 506 } 507 $entry = ""; 508 }else{ 509 $paren = pop @paren_stack; 510 if( scalar(@paren_stack) > 0 ){ 511 #this uses ASCII to check if the parens match up 512 # in ASCII ord ( = 40 , ord ) = 41 , ord [ = 91 , 513 # ord ] = 93 , ord { = 123 , ord } = 125 514 if( (ord($char) - ord($paren) <= 2) ){ 515 $entry = $entry . $char; 516 }else{ 517 $rh_ans->throw_error('EVAL','There is a syntax error in your answer'); 518 } 519 } 520 } 521 } 522 }else{ 523 $rh_ans->{preview_text_string} .= "["; 524 $rh_ans->{preview_latex_string} .= "["; 525 $display_ans .= "["; 526 } 527 } 528 $rh_ans->{ra_student_ans} = \@answers; 529 $rh_ans->{student_ans} = $display_ans unless $rh_ans->{error_flag}; 530 $rh_ans; 531 } 532 533 sub ans_array_filter{ 534 my $rh_ans = shift; 535 my %options = @_; 536 $rh_ans->{ans_label} =~ /ArRaY(\d+)\[\d+,\d+,\d+\]/; 537 my $ans_num = $1; 538 my @keys = grep /ArRaY$ans_num/, keys(%{$main::inputs_ref}); 539 my $key; 540 my @array = (); 541 my ($i,$j,$k) = (0,0,0); 542 543 #the keys aren't in order, so their info has to be put into the array before doing anything with it 544 foreach $key (@keys){ 545 $key =~ /ArRaY\d+\[(\d+),(\d+),(\d+)\]/; 546 ($i,$j,$k) = ($1,$2,$3); 547 $array[$i][$j][$k] = ${$main::inputs_ref}{'ArRaY'.$ans_num.'['.$i.','.$j.','.$k.']'}; 548 } 549 550 my $display_ans = ""; 551 552 for( $i=0; $i < scalar(@array) ; $i ++ ) 553 { 554 $display_ans .= " ["; 555 $rh_ans->{preview_text_string} .= ' ['; 556 $rh_ans->{preview_latex_string} .= ' ['; 557 for( $j = 0; $j < scalar( @{$array[$i]} ) ; $j++ ) 558 { 559 $display_ans .= " ["; 560 $rh_ans->{preview_text_string} .= ' ['; 561 $rh_ans->{preview_latex_string} .= ' ['; 562 for( $k = 0; $k < scalar( @{$array[$i][$j]} ) ; $k ++ ){ 563 my $entry = $array[$i][$j][$k]; 564 565 # This parser code was origianally taken from PGanswermacros::check_syntax 566 # but parts of it needed to be slighty modified for this context 567 my $parser = new AlgParserWithImplicitExpand; 568 my $ret = $parser -> parse($entry); #for use with loops 569 570 if ( ref($ret) ) { ## parsed successfully 571 $parser -> tostring(); 572 $parser -> normalize(); 573 $entry = $parser -> tostring(); 574 $rh_ans->{preview_text_string} .= $entry.","; 575 $rh_ans->{preview_latex_string} .= $parser -> tolatex().","; 576 } else { ## error in parsing 577 $rh_ans->{'student_ans'} = 'syntax error:'.$display_ans. $parser->{htmlerror}, 578 $rh_ans->{'ans_message'} = $display_ans.$parser -> {error_msg}, 579 $rh_ans->{'preview_text_string'} = '', 580 $rh_ans->{'preview_latex_string'} = '', 581 $rh_ans->throw_error('SYNTAX', 'syntax error in answer:'.$display_ans.$parser->{htmlerror} . "$main::BR" .$parser -> {error_msg}.".$main::BR"); 582 } 583 584 my ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($entry); 585 if ($PG_eval_errors) { 586 $rh_ans->throw_error('EVAL','There is a syntax error in your answer.') ; 587 $rh_ans->{ans_message} = clean_up_error_msg($PG_eval_errors); 588 last; 589 } else { 590 $entry = prfmt($inVal,$options{format}); 591 $display_ans .= $entry.","; 592 $array[$i][$j][$k] = $entry; 593 } 594 } 595 chop($rh_ans->{preview_text_string}); 596 chop($rh_ans->{preview_latex_string}); 597 chop($display_ans); 598 $rh_ans->{preview_text_string} .= '] ,'; 599 $rh_ans->{preview_latex_string} .= '] ,'; 600 $display_ans .= '] ,'; 601 602 } 603 chop($rh_ans->{preview_text_string}); 604 chop($rh_ans->{preview_latex_string}); 605 chop($display_ans); 606 $rh_ans->{preview_text_string} .= '] ,'; 607 $rh_ans->{preview_latex_string} .= '] ,'; 608 $display_ans .= '] ,'; 609 } 610 chop($rh_ans->{preview_text_string}); 611 chop($rh_ans->{preview_latex_string}); 612 chop($display_ans); 613 614 $rh_ans->{original_student_ans} = $display_ans; 615 $rh_ans->{ra_student_ans} = \@array; 616 $rh_ans->{student_ans} = $display_ans; 617 618 $rh_ans; 619 620 } 621 622 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |