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