Parent Directory
|
Revision Log
syncing pg HEAD with pg2.4.7 on 6/25/2009
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader$ 5 # 6 # This program is free software; you can redistribute it and/or modify it under 7 # the terms of either: (a) the GNU General Public License as published by the 8 # Free Software Foundation; either version 2, or (at your option) any later 9 # version, or (b) the "Artistic License" which comes with this package. 10 # 11 # This program is distributed in the hope that it will be useful, but WITHOUT 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the 14 # Artistic License for more details. 15 ################################################################################ 16 17 =head1 NAME 18 19 extraAnswerEvaluators.pl - Answer evaluators for intervals, lists of numbers, 20 and lists of points. 21 22 =head1 SYNPOSIS 23 24 interval_cmp() -- checks answers which are unions of intervals. It can also 25 be used for checking an ordered pair or list of ordered 26 pairs. 27 28 number_list_cmp() -- checks a comma separated list of numbers. By use of 29 optional arguments, you can request that order be 30 important, that complex numbers be allowed, and specify 31 extra arguments to be sent to num_cmp (or cplx_cmp) for 32 checking individual entries. 33 34 equation_cmp() -- provides a limited facility for checking equations. It 35 makes no pretense of checking to see if the real locus of 36 the student's equation matches the real locus of the 37 instructor's equation. The student's equation must be of 38 the same general type as the instructors to get credit. 39 40 =head1 DESCRIPTION 41 42 This file adds subroutines which create "answer evaluators" for checking student 43 answers of various "exotic" types. 44 45 =cut 46 47 # ^uses loadMacros 48 loadMacros('MathObjects.pl'); 49 50 { 51 # ^package Equation_eval 52 package Equation_eval; 53 54 # ^function split_eqn 55 sub split_eqn { 56 my $instring = shift; 57 58 split /=/, $instring; 59 } 60 61 #FIXME -- this could be improved so that 62 # 1. it uses an answer evaluator object instead of a sub routine 63 # 2. it provides error messages when previous answers are equivalent 64 # ^function equation_cmp 65 # ^uses AnswerHash::new 66 # ^uses split_eqn 67 # ^uses main::check_syntax 68 # ^uses main::fun_cmp 69 sub equation_cmp { 70 my $right_ans = shift; 71 my %opts = @_; 72 my $vars = ['x','y']; 73 74 75 $vars = $opts{'vars'} if defined($opts{'vars'}); 76 77 my $ans_eval = sub { 78 my $student = shift; 79 my %response_options = @_; 80 my $ans_hash = new AnswerHash( 81 'score'=>0, 82 'correct_ans'=>$right_ans, 83 'student_ans'=>$student, 84 'original_student_ans' => $student, 85 'type' => 'equation_cmp', 86 'ans_message'=>'', 87 'preview_text_string'=>'', 88 'preview_latex_string'=>'', 89 ); 90 91 if(! ($student =~ /\S/)) { return $ans_hash; } 92 93 my @right= split_eqn($right_ans); 94 if(scalar(@right) != 2) { 95 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 96 return $ans_hash; 97 } 98 my @studsplit = split_eqn($student); 99 if(scalar(@studsplit) != 2) { 100 $ans_hash->{'ans_message'} = "You did not enter an equation (with an equals sign and two sides)."; 101 return $ans_hash; 102 } 103 104 # Next we should do syntax checks on everyone 105 106 my $ah = new AnswerHash; 107 $ah->input($right[0]); 108 $ah=main::check_syntax($ah); 109 if($ah->{error_flag}) { 110 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 111 return $ans_hash; 112 } 113 114 $ah->input($right[1]); 115 $ah=main::check_syntax($ah); 116 if($ah->{error_flag}) { 117 $ans_hash->{'ans_message'} = "Tell your professor that there is an error in this problem."; 118 return $ans_hash; 119 } 120 121 # Correct answer checks out, now check student's syntax 122 123 my @prevs = ("",""); 124 my @prevtxt = ("",""); 125 $ah->input($studsplit[0]); 126 $ah=main::check_syntax($ah); 127 if($ah->{error_flag}) { 128 $ans_hash->{'ans_message'} = "Syntax error on the left side of your equation."; 129 return $ans_hash; 130 } 131 $prevs[0] = $ah->{'preview_latex_string'}; 132 $prevstxt[0] = $ah->{'preview_text_string'}; 133 134 135 $ah->input($studsplit[1]); 136 $ah=main::check_syntax($ah); 137 if($ah->{error_flag}) { 138 $ans_hash->{'ans_message'} = "Syntax error on the right side of your equation."; 139 return $ans_hash; 140 } 141 $prevs[1] = $ah->{'preview_latex_string'}; 142 $prevstxt[1] = $ah->{'preview_text_string'}; 143 144 $ans_hash->{'preview_latex_string'} = "$prevs[0] = $prevs[1]"; 145 $ans_hash->{'preview_text_string'} = "$prevstxt[0] = $prevstxt[1]"; 146 147 148 # Check for answer equivalent to 0=0 149 # Could be false positive below because of parameter 150 my $ae = main::fun_cmp("0", %opts); 151 my $res = $ae->evaluate("$studsplit[0]-($studsplit[1])"); 152 if($res->{'score'}==1) { 153 # Student is 0=0, is correct answer also like this? 154 $res = $ae->evaluate("$right[0]-($right[1])"); 155 if($res->{'score'}==1) { 156 $ans_hash-> setKeys('score' => $res->{'score'}); 157 } 158 return $ans_hash; 159 } 160 161 # Maybe answer really is 0=0, and student got it wrong, so check that 162 $res = $ae->evaluate("$right[0]-($right[1])"); 163 if($res->{'score'}==1) { 164 return $ans_hash; 165 } 166 167 # Finally, use fun_cmp to check the answers 168 169 $ae = main::fun_cmp("o*($right[0]-($right[1]))", vars=>$vars, params=>['o'], %opts); 170 $res= $ae->evaluate("$studsplit[0]-($studsplit[1])",%response_options); 171 $ans_hash-> setKeys('score' => $res->{'score'}); 172 173 return $ans_hash; 174 }; 175 176 return $ans_eval; 177 } 178 } 179 # ^package main 180 181 # ^function mode2context 182 # ^uses Parser::Context::getCopy 183 # ^uses %context 184 # ^uses $numZeroLevelTolDefault 185 # ^uses $numAbsTolDefault 186 # ^uses $numRelPercentTolDefault 187 # ^uses $numFormatDefault 188 sub mode2context { 189 my $mode = shift; 190 my %options = @_; 191 my $context; 192 for ($mode) { 193 /^strict$/i and do { 194 $context = Parser::Context->getCopy(\%main::context,"LimitedNumeric"); 195 $context->operators->redefine(','); 196 last; 197 }; 198 /^arith$/i and do { 199 $context = Parser::Context->getCopy(\%main::context,"LegacyNumeric"); 200 $context->functions->disable('All'); 201 last; 202 }; 203 /^frac$/i and do { 204 $context = Parser::Context->getCopy(\%main::context,"LimitedNumeric-Fraction"); 205 $context->operators->redefine(','); 206 last; 207 }; 208 209 # default 210 $context = Parser::Context->getCopy(\%main::context,"LegacyNumeric"); 211 } 212 # If we are using complex numbers, then we ignore the other mode parts 213 if(defined($options{'complex'}) && 214 ($options{'complex'} =~ /(yes|ok)/i)) { 215 #$context->constants->redefine('i', from=>'Complex'); 216 #$context->functions->redefine(['arg','mod','Re','Im','conj', 'sqrt', 'log'], from=>'Complex'); 217 #$context->operators->redefine(['^', '**'], from=>'Complex'); 218 $context = Parser::Context->getCopy(\%main::context,"Complex"); 219 } 220 $options{tolType} = $options{tolType} || 'relative'; 221 $options{tolType} = 'absolute' if defined($options{tol}); 222 $options{zeroLevel} = $options{zeroLevel} || $options{zeroLevelTol} || 223 $main::numZeroLevelTolDefault; 224 if ($options{tolType} eq 'absolute' or defined($options{abstol})) { 225 $options{tolerance} = $options{tolerance} || $options{tol} || 226 $options{reltol} || $options{relTol} || $options{abstol} || 227 $main::numAbsTolDefault; 228 $context->flags->set( 229 tolerance => $options{tolerance}, 230 tolType => 'absolute', 231 ); 232 } else { 233 $options{tolerance} = $options{tolerance} || $options{tol} || 234 $options{reltol} || $options{relTol} || $options{abstol} || 235 $main::numRelPercentTolDefault; 236 $context->flags->set( 237 tolerance => .01*$options{tolerance}, 238 tolType => 'relative', 239 ); 240 } 241 $context->flags->set( 242 zeroLevel => $options{zeroLevel}, 243 zeroLevelTol => $options{zeroLevelTol} || $main::numZeroLevelTolDefault, 244 ); 245 $context->{format}{number} = $options{'format'} || $main::numFormatDefault; 246 return($context); 247 } 248 249 =head1 MACROS 250 251 =head2 interval_cmp 252 253 Compares an interval or union of intervals. Typical invocations are 254 255 interval_cmp("(2, 3] U(7, 11)") 256 257 The U is used for union symbol. In fact, any garbage (or nothing at all) can go 258 between intervals. It makes sure open/closed parts of intervals are correct, 259 unless you don't like that. To have it ignore the difference between open and 260 closed endpoints, use 261 262 interval_cmp("(2, 3] U(7, 11)", sloppy=>'yes') 263 264 interval_cmp uses num_cmp on the endpoints. You can pass optional arguments for 265 num_cmp, so to change the tolerance, you can use 266 267 interval_cmp("(2, 3] U(3+4, 11)", relTol=>3) 268 269 The intervals can be listed in any order, unless you want to force a 270 particular order, which is signaled as 271 272 interval_cmp("(2, 3] U(3+4, 11)", ordered=>'strict') 273 274 You can specify infinity as an endpoint. It will do a case-insensitive 275 string match looking for I, Infinity, Infty, or Inf. You can prepend a + 276 or -, as in 277 278 interval_cmp("(-inf, 3] U [e^10, infinity)") 279 280 or 281 282 interval_cmp("(-INF, 3] U [e^10, +I)") 283 284 If the question might have an empty set as the answer, you can use 285 the strings option to allow for it. So 286 287 interval_cmp("$ans", strings=>['empty']) 288 289 will not generate an error message if the student enters the string 290 empty. Better still, it will mark a student answer of "empty" as correct 291 iff this matches $ans. 292 293 You can use interval_cmp for ordered pairs, or lists of ordered pairs. 294 Internally, this is just a distinction of whether to put nice union symbols 295 between intervals, or commas. To get commas, use 296 297 interval_cmp("(1,2), (2,3), (4,-1)", unions=>'no') 298 299 Note that interval_cmp makes no attempt at simplifying overlapping intervals. 300 This becomes an important feature when you are really checking lists of 301 ordered pairs. 302 303 Now we use the Parser package for checking intervals (or lists of 304 points if unions=>'no'). So, one can specify the Parser options 305 showCoordinateHints, showHints, partialCredit, and/or showLengthHints 306 as optional arguments: 307 308 interval_cmp("(1,2), (2,3), (4,-1)", unions=>'no', partialCredit=>1) 309 310 Also, set differences and 'R' for all real numbers now work too since they work 311 for Parser Intervals and Unions. 312 313 =cut 314 315 # ^function interval_cmp 316 # ^uses Context 317 # ^uses mode2context 318 # ^uses List 319 # ^uses Union 320 sub interval_cmp { 321 my $correct_ans = shift; 322 323 my %opts = @_; 324 325 my $mode = $opts{mode} || 'std'; 326 my %options = (debug => $opts{debug}); 327 my $ans_type = ''; # set to List, Union, or String below 328 329 # 330 # Get an apppropriate context based on the mode 331 # 332 my $oldContext = Context(); 333 my $context = mode2context($mode, %opts); 334 335 if(defined($opts{unions}) and $opts{unions} eq 'no' ) { 336 # This is really a list of points, not intervals at all 337 $ans_type = 'List'; 338 $context->parens->redefine('('); 339 $context->parens->redefine('['); 340 $context->parens->redefine('{'); 341 $context->operators->redefine('u',using=>','); 342 $context->operators->set(u=>{string=>", ", TeX=>',\,'}); 343 } else { 344 $context->parens->redefine('(', from=>'Interval'); 345 $context->parens->redefine('[', from=>'Interval'); 346 $context->parens->redefine('{', from=>'Interval'); 347 348 $context->constants->redefine('R',from=>'Interval'); 349 $context->operators->redefine('U',from=>"Interval"); 350 $context->operators->redefine('u',from=>"Interval",using=>"U"); 351 $ans_type = 'Union'; 352 } 353 # Take optional arguments intended for List, or Union 354 for my $o qw( showCoordinateHints showHints partialCredit showLengthHints ) { 355 $options{$o} = $opts{$o} || 0; 356 } 357 $options{showUnionReduceWarnings} = $opts{showUnionReduceWarnings}; 358 $options{studentsMustReduceUnions} = $opts{studentsMustReduceUnions}; 359 if(defined($opts{ordered}) and $opts{ordered}) { 360 $options{ordered} = 1; 361 # Force this option if the the union must be ordered 362 $options{studentsMustReduceUnions} = 1; 363 } 364 if (defined($opts{'sloppy'}) && $opts{'sloppy'} eq 'yes') { 365 $options{requireParenMatch} = 0; 366 } 367 # historically we allow more infinities 368 $context->strings->add( 369 'i' => {alias=>'infinity'}, 370 'infty' => {alias=>'infinity'}, 371 'minfinity' => {infinite=>1, negative=>1}, 372 'minfty' => {alias=>'minfinity'}, 373 'minf' => {alias=>'minfinity'}, 374 'mi' => {alias=>'minfinity'}, 375 ); 376 # Add any strings 377 if ($opts{strings}) { 378 foreach my $string (@{$opts{strings}}) { 379 $string = uc($string); 380 $context->strings->add($string) unless 381 defined($context->strings->get($string)); 382 $ans_type = 'String' if $string eq uc($correct_ans); 383 } 384 } 385 # Add any variables 386 $opts{vars} = $opts{var} if ($opts{var}); 387 if ($opts{vars}) { 388 $context->variables->are(); # clear old vars 389 $opts{vars} = [$opts{vars}] unless ref($opts{vars}) eq 'ARRAY'; 390 foreach my $v (@{$opts{vars}}) { 391 $context->variables->add($v=>'Real') 392 unless $context->variables->get($v); 393 } 394 } 395 396 my $ans_eval; 397 Context($context); 398 if($ans_type eq 'List') { 399 $ans_eval = List($correct_ans)->cmp(%options); 400 } elsif($ans_type eq 'Union') { 401 $ans_eval = Union($correct_ans)->cmp(%options); 402 } elsif($ans_type eq 'String') { 403 $ans_eval = List($correct_ans)->cmp(%options); 404 } else { 405 warn "Bug -- should not be here in interval_cmp"; 406 } 407 408 Context($oldContext); 409 return($ans_eval); 410 } 411 412 =head2 number_list_cmp 413 414 Checks an answer which is a comma-separated list of numbers. The actual 415 numbers are fed to num_cmp, so all of the flexibilty of num_cmp carries 416 over (values can be expressions to be evaluated). For example, 417 418 number_list_cmp("1, -2") 419 420 will accept "1, -2", "-2, 1", or "-1-1,sqrt(1)". 421 422 number_list_cmp("1^2 + 1, 2^2 + 1, 3^2 + 1", ordered=>'strict') 423 424 will accept "2, 5, 10", but not "5, 2, 10". 425 426 If you want to allow complex number entries, complex=>'ok' will cause it 427 to use cplx_cmp instead: 428 429 number_list_cmp("2, -2, 2i, -2i", complex=>'ok') 430 431 In cases where you set complex=>'ok', be sure the problem file loads 432 PGcomplexmacros.pl. 433 434 Optional arguements for num_cmp (resp. cplx_cmp) can be used as well, 435 such as 436 437 number_list_cmp("cos(3), sqrt(111)", relTol => 3) 438 439 The strings=>['hello'] argument is treated specially. It can be used to 440 replace the entire answer. So 441 442 number_list_cmp("cos(3), sqrt(111)", strings=>['none']) 443 444 will mark "none" wrong, but not generate an error. On the other hand, 445 446 number_list_cmp("none", strings=>['none']) 447 448 will mark "none" as correct. 449 450 One can also specify optionnal arguments for Parser's List checker: showHints, 451 partialCredit, and showLengthHints, as in: 452 453 number_list_cmp("cos(3), sqrt(111)", partialCredit=>1) 454 455 =cut 456 457 # ^function number_list_cmp 458 # ^uses Context 459 # ^uses mode2context 460 # ^uses List 461 sub number_list_cmp { 462 my $list = shift; 463 464 my %num_params = @_; 465 466 my $mode = $num_params{mode} || 'std'; 467 my %options = (debug => $num_params{debug}); 468 469 # 470 # Get an apppropriate context based on the mode 471 # 472 my $oldContext = Context(); 473 my $context = mode2context($mode, %num_params); 474 475 #$context->strings->clear; 476 if ($num_params{strings}) { 477 foreach my $string (@{$num_params{strings}}) { 478 my %tex = ($string =~ m/(-?)inf(inity)?/i)? (TeX => "$1\\infty"): (); 479 $string = uc($string); 480 $context->strings->add($string => {%tex}) unless 481 defined($context->strings->get($string)); 482 } 483 } 484 485 $options{ordered} = 1 if defined($num_params{ordered}); 486 # These didn't exist before in number_list_cmp so they behaved like 487 # in List()->cmp. Now they can be optionally set 488 for my $o qw( showHints partialCredit showLengthHints ) { 489 $options{$o} = $num_params{$o} || 0; 490 } 491 492 Context($context); 493 my $ans_eval = List($list)->cmp(%options); 494 Context($oldContext); 495 return($ans_eval); 496 } 497 498 499 =heads equation_cmp 500 501 Compares an equation. This really piggy-backs off of fun_cmp. It looks 502 at LHS-RHS of the equations to see if they agree up to constant multiple. 503 It also guards against an answer of 0=0 (which technically gives a constant 504 multiple of any equation). It is best suited to situations such as checking 505 the equation of a line which might be vertical and you don't want to give 506 that away, or checking equations of ellipses where the students answer should 507 be quadratic. 508 509 Typical invocation would be: 510 511 equation_com("x^2+(y-1)^2 = 11", vars=>['x','y']) 512 513 =cut 514 515 # ^function equation_cmp 516 # ^uses Equation_eval::equation_cmp 517 sub equation_cmp { 518 Equation_eval::equation_cmp(@_); 519 }
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |