Parent Directory
|
Revision Log
initial import
1 #!/usr/local/bin/perl 2 3 # This file is PGanswermacros.pl 4 # This includes the subroutines for the ANS macros, that 5 # is, macros allowing a more flexible answer checking 6 #################################################################### 7 # Copyright @ 1995-2000 University of Rochester 8 # All Rights Reserved 9 #################################################################### 10 11 12 =head1 NAME 13 14 PGanswermacros.pl -- located in the courseScripts directory 15 16 =head1 SYNPOSIS 17 18 Number Answer Evaluators: 19 num_cmp() -- uses an input hash to determine parameters 20 std_num_cmp(), std_num_cmp_list(), std_num_cmp_abs, std_num_cmp_abs_list() 21 frac_num_cmp(), frac_num_cmp_list(), frac_num_cmp_abs, frac_num_cmp_abs_list() 22 arith_num_cmp(), arith_num_cmp_list(), arith_num_cmp_abs, arith_num_cmp_abs_list() 23 strict_num_cmp(), strict_num_cmp_list(), strict_num_cmp_abs, strict_num_cmp_abs_list() 24 numerical_compare_with_units() -- requires units as part of the answer 25 std_num_str_cmp() -- also accepts a set of strings as possible answers 26 27 Function Answer Evaluators: 28 fun_cmp() -- uses an input hash to determine parameters 29 function_cmp(), function_cmp_abs() 30 function_cmp_up_to_constant(), function_cmp_up_to_constant_abs() 31 multivar_function_cmp() 32 33 String Answer Evaluators: 34 str_cmp() -- uses an input hash to determine parameters 35 std_str_cmp(), std_str_cmp_list(), std_cs_str_cmp(), std_cs_str_cmp_list() 36 strict_str_cmp(), strict_str_cmp_list() 37 ordered_str_cmp(), ordered_str_cmp_list(), ordered_cs_str_cmp(), ordered_cs_str_cmp_list() 38 unordered_str_cmp(), unordered_str_cmp_list(), unordered_cs_str_cmp(), unordered_cs_str_cmp_list() 39 40 Miscellaneous Answer Evaluators: 41 checkbox_cmp() 42 radio_cmp() 43 44 =cut 45 46 =head1 DESCRIPTION 47 48 This file adds subroutines which create "answer evaluators" for checking 49 answers. Each answer evaluator accepts a single input from a student answer, 50 checks it and creates an output hash %ans_hash with seven or eight entries 51 (the preview_latex_string is optional). The output hash is now being created 52 with the AnswerHash package "class", which is located at the end of this file. 53 This class is currently just a wrapper for the hash, but this might change in 54 the future as new capabilities are added. 55 56 score => $correctQ, 57 correct_ans => $originalCorrEqn, 58 student_ans => $modified_student_ans 59 original_student_ans => $original_student_answer, 60 ans_message => $PGanswerMessage, 61 type => 'typeString', 62 preview_text_string => $preview_text_string, 63 preview_latex_string => $preview_latex_string 64 65 66 $ans_hash{score} -- a number between 0 and 1 indicating 67 whether the answer is correct. Fractions 68 allow the implementation of partial 69 credit for incorrect answers. 70 $ans_hash{correct_ans} -- The correct answer, as supplied by the 71 instructor and then formatted. This can 72 be viewed by the student after the answer date. 73 $ans_hash{student_ans} -- This is the student answer, after reformatting; 74 for example the answer might be forced 75 to capital letters for comparison with 76 the instructors answer. For a numerical 77 answer, it gives the evaluated answer. 78 This is displayed in the section reporting 79 the results of checking the student answers. 80 $ans_hash{original_student_ans} -- This is the original student answer. This is displayed 81 on the preview page and may be used for sticky answers. 82 $ans_hash{ans_message} -- Any error message, or hint provided by the answer evaluator. 83 This is also displayed in the section reporting 84 the results of checking the student answers. 85 $ans_hash{type} -- A string indicating the type of answer evaluator. This 86 helps in preprocessing the student answer for errors. 87 Some examples: 88 'number_with_units' 89 'function' 90 'frac_number' 91 'arith_number' 92 $ans_hash{preview_text_string} -- This typically shows how the student answer was parsed. It is 93 displayed on the preview page. For a student answer of 2sin(3x) 94 this would be 2*sin(3*x). For string answers it is typically the 95 same as $ans_hash{student_ans}. 96 $ans_hash{preview_latex_string -- THIS IS OPTIONAL. This is latex version of the student answer 97 which is used to show a typeset view on the answer on the preview 98 page. For a student answer of 2/3, this would be \frac{2}{3}. 99 100 Technical note: the routines in this file are not actually answer evaluators. Instead, they create 101 answer evaluators. An answer evaluator is an anonymous subroutine, referenced by a named scalar. The 102 routines in this file build the subroutine and return a reference to it. Later, when the student 103 actually enters an answer, the problem processor feeds that answer to the referenced subroutine, which 104 evaluates it and returns a score (usually 0 or 1). For most users, this distinction is unimportant, but 105 if you plan on writing your own answer evaluators, you should understand this point. 106 107 =cut 108 109 BEGIN { 110 be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. 111 } 112 113 my $BR = $main::BR; # convenient localizations. 114 my $PAR = $main::PAR; 115 116 # import defaults 117 # these are now imported from the %envir variable 118 my $numRelPercentTolDefault = $main::numRelPercentTolDefault; 119 my $numZeroLevelDefault = $main::numZeroLevelDefault; 120 my $numZeroLevelTolDefault = $main::numZeroLevelTolDefault; 121 my $numAbsTolDefault = $main::numAbsTolDefault; 122 my $numFormatDefault = $main::numFormatDefault; 123 124 my $functRelPercentTolDefault = $main::functRelPercentTolDefault; 125 my $functZeroLevelDefault = $main::functZeroLevelDefault; 126 my $functZeroLevelTolDefault = $main::functZeroLevelTolDefault; 127 my $functAbsTolDefault = $main::functAbsTolDefault; 128 my $functNumOfPoints = $main::functNumOfPoints; 129 my $functVarDefault = $main::functVarDefault; 130 my $functLLimitDefault = $main::functLLimitDefault; 131 my $functULimitDefault = $main::functULimitDefault; 132 my $functMaxConstantOfIntegration = $main::functMaxConstantOfIntegration; 133 134 135 136 ########################################################################## 137 ########################################################################## 138 ## Number answer evaluators 139 140 =head2 Number Answer Evaluators 141 142 Number answer evaluators take in a numerical answer, compare it to the correct answer, 143 and return a score. In addition, they can choose to accept or reject an answer based on 144 its format, closeness to the correct answer, and other criteria. There are two types 145 of numerical answer evaluators: num_cmp(), which takes a hash of named options as parameters, 146 and the "mode"_num_cmp() variety, which use different functions to access different sets of 147 options. In addition, there is the special case of std_num_str_cmp(), which can evaluate 148 both numbers and strings. 149 150 Numerical Comparison Options 151 152 correctAnswer -- This is the correct answer that the student answer will 153 be compared to. However, this does not mean that the 154 student answer must match this exactly. How close the 155 student answer must be is determined by the other 156 options, especially tolerance and format. 157 158 tolerance -- These options determine how close the student answer 159 must be to the correct answer to qualify. There are two 160 types of tolerance: relative and absolute. Relative 161 tolerances are given in percentages. A relative 162 tolerance of 1 indicates that the student answer must 163 be within 1% of the correct answer to qualify as correct. 164 In other words, a student answer is correct when 165 abs(studentAnswer - correctAnswer) <= abs(.01*relpercentTol*correctAnswer) 166 Using absolute tolerance, the student answer must be a 167 fixed distance from the correct answer to qualify. 168 For example, an absolute tolerance of 5 means that any 169 number which is +-5 of the correct answer qualifies as correct. 170 Final (rarely used) tolerance options are zeroLevel 171 and zeroLevelTol, used in conjunction with relative 172 tolerance. if correctAnswer has absolute value less than 173 or equal to zeroLevel, then the student answer must be, 174 in absolute terms, within zeroLevelTol of correctAnswer, i.e., 175 abs(studentAnswer - correctAnswer) <= zeroLevelTol. 176 In other words, if the correct answer is very near zero, 177 an absolute tolerance will be used. One must do this to 178 handle floating point answers very near zero, because of 179 the inaccuracy of floating point arithmetic. However, the 180 default values are almost always adequate. 181 182 mode -- This determines the allowable methods for entering an 183 answer. Answers which do not meet this requirement will 184 be graded as incorrect, regardless of their numerical 185 value. The recognized modes are: 186 'std' (default) -- allows any expression which evaluates 187 to a number, including those using 188 elementary functions like sin() and 189 exp(), as well as the operations of 190 arithmetic (+, -, *, /, ^) 191 'strict' -- only decimal numbers are allowed 192 'frac' -- whole numbers and fractions are allowed 193 'arith' -- arithmetic expressions are allowed, but 194 no functions 195 Note that all modes allow the use of "pi" and "e" as 196 constants, and also the use of "E" to represent scientific 197 notation. 198 199 format -- The format to use when displaying the correct and 200 submitted answers. This has no effect on how answers are 201 evaluated; it is only for cosmetic purposes. The 202 formatting syntax is the same as Perl uses for the sprintf() 203 function. Format strings are of the form '%m.nx' or '%m.nx#', 204 where m and n are described below, and x is a formatter. 205 Esentially, m is the minimum length of the field 206 (make this negative to left-justify). Note that the decimal 207 point counts as a character when determining the field width. 208 If m begins with a zero, the number will be padded with zeros 209 instead of spaces to fit the field. 210 The precision specifier (n) works differently, depending 211 on which formatter you are using. For d, i, o, u, x and X 212 formatters (non-floating point formatters), n is the minimum 213 number of digits to display. For e and f, it is the number of 214 digits that appear after the decimal point (extra digits will 215 be rounded; insufficient digits will be padded with spaces--see 216 '#' below). For g, it is the number of significant digits to 217 display. 218 The full list of formatters can be found in the manpage 219 for printf(3), or by typing "perldoc -f sprintf" at a 220 terminal prompt. The following is a brief summary of the 221 most frequent formatters: 222 d -- decimal number 223 ld -- long decimal number 224 u -- unsigned decimal number 225 lu -- long unsigned decimal number 226 x -- hexadecimal number 227 o -- octal number 228 e -- floating point number in scientific notation 229 f -- floating point number 230 g -- either e or f, whichever takes less space 231 Technically, g will use e if the exponent is less than -4 or 232 greater than or equal to the precision. Trailing zeros are 233 removed in this mode. 234 If the format string ends in '#', trailing zeros will be 235 removed in the decimal part. Note that this is not a standard 236 syntax; it is handled internally by WeBWorK and not by Perl 237 (although this should not be a concern to end users). 238 The default format is '%0.5f#', which displays as a floating 239 point number with 5 digits of precision and no trailing zeros. 240 Other useful format strings might be '%0.2f' for displaying 241 dollar amounts, or '%010d' to display an integer with leading 242 zeros. Setting format to an empty string ( '' ) means no 243 formatting will be used; this will show 'arbitrary' precision 244 floating points. 245 246 Default Values (As of 7/24/2000) (Option -- Variable Name -- Value) 247 248 Format -- $numFormatDefault -- "%0.5f#" 249 Relative Tolerance -- $numRelPercentTolDefault -- .1 250 Absolute Tolerance -- $numAbsTolDefault -- .001 251 Zero Level -- $numZeroLevelDefault -- 1E-14 252 Zero Level Tolerance -- $numZeroLevelTolDefault -- 1E-12 253 254 =cut 255 256 =head3 "mode"_num_cmp() functions 257 258 There are 16 functions total, 4 for each mode (std, frac, strict, arith). Each mode has 259 one "normal" function, one which accepts a list of answers, one which uses absolute 260 rather than relative tolerance, and one which uses absolute tolerance and accepts a list. 261 The "std" family is documented below; all others work precisely the same. 262 263 std_num_cmp($correctAnswer) OR 264 std_num_cmp($correctAnswer, $relPercentTol) OR 265 std_num_cmp($correctAnswer, $relPercentTol, $format) OR 266 std_num_cmp($correctAnswer, $relPercentTol, $format, $zeroLevel) OR 267 std_num_cmp($correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol) 268 269 $correctAnswer -- the correct answer 270 $relPercentTol -- the tolerance, as a percentage (optional) 271 $format -- the format of the displayed answer (optional) 272 $zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies (optional) 273 $zeroLevelTol -- absolute tolerance to allow when correct answer is close to zero (optional) 274 275 std_num_cmp() uses standard mode (arithmetic operations and elementary 276 functions allowed) and relative tolerance. Options are specified by 277 one or more parameters. Note that if you wish to set an option which 278 is later in the parameter list, you must set all previous options. 279 280 std_num_cmp_abs($correctAnswer) OR 281 std_num_cmp_abs($correctAnswer, $absTol) OR 282 std_num_cmp_abs($correctAnswer, $absTol, $format) 283 284 $correctAnswer -- the correct answer 285 $absTol -- an absolute tolerance (optional) 286 $format -- the format of the displayed answer (optional) 287 288 std_num_cmp_abs() uses standard mode and absolute tolerance. Options 289 are set as with std_num_cmp(). Note that $zeroLevel and $zeroLevelTol 290 do not apply with absolute tolerance. 291 292 std_num_cmp_list($relPercentTol, $format, @answerList) 293 294 $relPercentTol -- the tolerance, as a percentage 295 $format -- the format of the displayed answer(s) 296 @answerList -- a list of one or more correct answers 297 298 std_num_cmp_list() uses standard mode and relative tolerance. There 299 is no way to set $zeroLevel or $zeroLevelTol. Note that no 300 parameters are optional. All answers in the list will be 301 evaluated with the same set of parameters. 302 303 std_num_cmp_abs_list($absTol, $format, @answerList) 304 305 $absTol -- an absolute tolerance 306 $format -- the format of the displayed answer(s) 307 @answerList -- a list of one or more correct answers 308 309 std_num_cmp_abs_list() uses standard mode and absolute tolerance. 310 Note that no parameters are optional. All answers in the list will be 311 evaluated with the same set of parameters. 312 313 arith_num_cmp(), arith_num_cmp_list(), arith_num_cmp_abs(), arith_num_cmp_abs_list() 314 strict_num_cmp(), strict_num_cmp_list(), strict_num_cmp_abs(), strict_num_cmp_abs_list() 315 frac_num_cmp(), frac_num_cmp_list(), frac_num_cmp_abs(), frac_num_cmp_abs_list() 316 317 Examples: 318 319 ANS( strict_num_cmp( 3.14159 ) ) -- The student answer must be a number 320 in decimal or scientific notation which is within .1 percent of 3.14159. 321 This assumes $numRelPercentTolDefault has been set to .1. 322 ANS( strict_num_cmp( $answer, .01 ) ) -- The student answer must be a 323 number within .01 percent of $answer (e.g. 3.14159 if $answer is 3.14159 324 or $answer is "pi" or $answer is 4*atan(1)). 325 ANS( frac_num_cmp( $answer) ) or ANS( frac_num_cmp( $answer,.01 )) -- 326 The student answer can be a number or fraction, e.g. 2/3. 327 ANS( arith_num_cmp( $answer) ) or ANS( arith_num_cmp( $answer,.01 )) -- 328 The student answer can be an arithmetic expression, e.g. (2+3)/7-2^.5 . 329 ANS( std_num_cmp( $answer) ) or ANS( std_num_cmp( $answer,.01 )) -- 330 The student answer can contain elementary functions, e.g. sin(.3+pi/2) 331 332 =cut 333 334 sub std_num_cmp { # compare numbers allowing use of elementary functions 335 my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; 336 337 NUM_CMP( 'correctAnswer' => $correctAnswer, 338 'tolerance' => $relPercentTol, 339 'tolType' => 'relative', 340 'format' => $format, 341 'mode' => 'std', 342 'zeroLevel' => $zeroLevel, 343 'zeroLevelTol' => $zeroLevelTol 344 ); 345 } 346 347 ## Similar to std_num_cmp but accepts a list of numbers in the form 348 ## std_num_cmp_list(relpercentTol,format,ans1,ans2,ans3,...) 349 ## format is of the form "%10.3g" or "", i.e., a format suitable for sprintf(). Use "" for default 350 ## You must enter a format and tolerance 351 sub std_num_cmp_list { 352 my ( $relPercentTol, $format, @answerList) = @_; 353 354 NUM_CMP_LIST( 'tolerance' => $relPercentTol, 355 'tolType' => 'relative', 356 'format' => $format, 357 'mode' => 'std', 358 'zeroLevel' => $numZeroLevelDefault, 359 'zeroLevelTol' => $numZeroLevelTolDefault, 360 'answerList' => \@answerList 361 ); 362 } 363 364 sub std_num_cmp_abs { # compare numbers allowing use of elementary functions with absolute tolerance 365 my ( $correctAnswer, $absTol, $format) = @_; 366 367 NUM_CMP( 'correctAnswer' => $correctAnswer, 368 'tolerance' => $absTol, 369 'tolType' => 'absolute', 370 'format' => $format, 371 'mode' => 'std', 372 'zeroLevel' => 0, 373 'zeroLevelTol' => 0 374 ); 375 } 376 377 ## See std_num_cmp_list for usage 378 sub std_num_cmp_abs_list { 379 my ( $absTol, $format, @answerList ) = @_; 380 381 NUM_CMP_LIST( 'tolerance' => $absTol, 382 'tolType' => 'absolute', 383 'format' => $format, 384 'mode' => 'std', 385 'zeroLevel' => 0, 386 'zeroLevelTol' => 0, 387 'answerList' => \@answerList 388 ); 389 } 390 391 392 sub frac_num_cmp { # only allow fractions and numbers as submitted answer 393 my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; 394 395 NUM_CMP( 'correctAnswer' => $correctAnswer, 396 'tolerance' => $relPercentTol, 397 'tolType' => 'relative', 398 'format' => $format, 399 'mode' => 'frac', 400 'zeroLevel' => $zeroLevel, 401 'zeroLevelTol' => $zeroLevelTol 402 ); 403 } 404 405 ## See std_num_cmp_list for usage 406 sub frac_num_cmp_list { 407 my ( $relPercentTol, $format, @answerList ) = @_; 408 409 NUM_CMP_LIST( 'tolerance' => $relPercentTol, 410 'tolType' => 'relative', 411 'format' => $format, 412 'mode' => 'frac', 413 'zeroLevel' => $numZeroLevelDefault, 414 'zeroLevelTol' => $numZeroLevelTolDefault, 415 'answerList' => \@answerList 416 ); 417 } 418 419 sub frac_num_cmp_abs { # only allow fraction expressions as submitted answer with absolute tolerance 420 my ( $correctAnswer, $absTol, $format ) = @_; 421 422 NUM_CMP( 'correctAnswer' => $correctAnswer, 423 'tolerance' => $absTol, 424 'tolType' => 'absolute', 425 'format' => $format, 426 'mode' => 'frac', 427 'zeroLevel' => 0, 428 'zeroLevelTol' => 0 429 ); 430 } 431 432 ## See std_num_cmp_list for usage 433 sub frac_num_cmp_abs_list { 434 my ( $absTol, $format, @answerList ) = @_; 435 436 NUM_CMP_LIST( 'tolerance' => $absTol, 437 'tolType' => 'absolute', 438 'format' => $format, 439 'mode' => 'frac', 440 'zeroLevel' => 0, 441 'zeroLevelTol' => 0, 442 'answerList' => \@answerList 443 ); 444 } 445 446 447 sub arith_num_cmp { # only allow arithmetic expressions as submitted answer 448 my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; 449 450 NUM_CMP( 'correctAnswer' => $correctAnswer, 451 'tolerance' => $relPercentTol, 452 'tolType' => 'relative', 453 'format' => $format, 454 'mode' => 'arith', 455 'zeroLevel' => $zeroLevel, 456 'zeroLevelTol' => $zeroLevelTol 457 ); 458 } 459 460 ## See std_num_cmp_list for usage 461 sub arith_num_cmp_list { 462 my ( $relPercentTol, $format, @answerList ) = @_; 463 464 NUM_CMP_LIST( 'tolerance' => $relPercentTol, 465 'tolType' => 'relative', 466 'format' => $format, 467 'mode' => 'arith', 468 'zeroLevel' => $numZeroLevelDefault, 469 'zeroLevelTol' => $numZeroLevelTolDefault, 470 'answerList' => \@answerList 471 ); 472 } 473 474 sub arith_num_cmp_abs { # only allow arithmetic expressions as submitted answer with absolute tolerance 475 my ( $correctAnswer, $absTol, $format ) = @_; 476 477 NUM_CMP( 'correctAnswer' => $correctAnswer, 478 'tolerance' => $absTol, 479 'tolType' => 'absolute', 480 'format' => $format, 481 'mode' => 'arith', 482 'zeroLevel' => 0, 483 'zeroLevelTol' => 0 484 ); 485 } 486 487 ## See std_num_cmp_list for usage 488 sub arith_num_cmp_abs_list { 489 my ( $absTol, $format, @answerList ) = @_; 490 491 NUM_CMP_LIST( 'tolerance' => $absTol, 492 'tolType' => 'absolute', 493 'format' => $format, 494 'mode' => 'arith', 495 'zeroLevel' => 0, 496 'zeroLevelTol' => 0, 497 'answerList' => \@answerList 498 ); 499 } 500 501 sub strict_num_cmp { # only allow numbers as submitted answer 502 my ( $correctAnswer, $relPercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; 503 504 NUM_CMP( 'correctAnswer' => $correctAnswer, 505 'tolerance' => $relPercentTol, 506 'tolType' => 'relative', 507 'format' => $format, 508 'mode' => 'strict', 509 'zeroLevel' => $zeroLevel, 510 'zeroLevelTol' => $zeroLevelTol 511 ); 512 } 513 514 ## See std_num_cmp_list for usage 515 sub strict_num_cmp_list { # compare numbers 516 my ( $relPercentTol, $format, @answerList ) = @_; 517 518 NUM_CMP_LIST( 'tolerance' => $relPercentTol, 519 'tolType' => 'relative', 520 'format' => $format, 521 'mode' => 'strict', 522 'zeroLevel' => $numZeroLevelDefault, 523 'zeroLevelTol' => $numZeroLevelTolDefault, 524 'answerList' => \@answerList 525 ); 526 } 527 528 sub strict_num_cmp_abs { # only allow numbers as submitted answer with absolute tolerance 529 my ( $correctAnswer, $absTol, $format ) = @_; 530 531 NUM_CMP( 'correctAnswer' => $correctAnswer, 532 'tolerance' => $absTol, 533 'tolType' => 'absolute', 534 'format' => $format, 535 'mode' => 'strict', 536 'zeroLevel' => 0, 537 'zeroLevelTol' => 0 538 ); 539 } 540 541 ## See std_num_cmp_list for usage 542 sub strict_num_cmp_abs_list { # compare numbers 543 my ( $absTol, $format, @answerList ) = @_; 544 545 NUM_CMP_LIST( 'tolerance' => $absTol, 546 'tolType' => 'absolute', 547 'format' => $format, 548 'mode' => 'strict', 549 'zeroLevel' => 0, 550 'zeroLevelTol' => 0, 551 'answerList' => \@answerList 552 ); 553 } 554 555 556 ## Compares a number with units 557 ## Deprecated; use num_cmp() 558 ## 559 ## IN: a string which includes the numerical answer and the units 560 ## a hash with the following keys (all optional): 561 ## mode -- 'std', 'frac', 'arith', or 'strict' 562 ## format -- the format to use when displaying the answer 563 ## tol -- an absolute tolerance, or 564 ## relTol -- a relative tolerance 565 ## zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies 566 ## zeroLevelTol -- absolute tolerance to allow when correct answer is close to zero 567 sub numerical_compare_with_units { 568 my $correct_answer = shift; # the answer is a string which includes both the numerical answer and the units. 569 my %options = @_; # all of the other inputs are (key value) pairs 570 571 # handle the defaults 572 $options{'mode'} = 'std' unless defined( $options{'mode'} ); 573 $options{'format'} = $numFormatDefault unless defined( $options{'format'} ); 574 $options{'zeroLevel'} = $numZeroLevelDefault unless defined( $options{'zeroLevel'} ); 575 $options{'zeroLevelTol'} = $numZeroLevelTolDefault unless defined( $options{'zeroLevelTol'} ); 576 577 # both spellings are maintained for backward compatibility 578 # relTol is preferred 579 if( defined $options{'reltol'} ) { 580 $options{'relTol'} = $options{'reltol'}; 581 delete $options{'reltol'}; 582 } 583 584 my ($tol, $tolerance_mode); 585 if ( defined $options{'tol'} ) { 586 $tol = $options{'tol'}; 587 $tolerance_mode = 'absolute'; 588 } 589 elsif( defined $options{'relTol'} ) { 590 $tol = $options{'relTol'}; 591 $tolerance_mode = 'relative'; 592 } 593 else { #the default is a relative tolerance 594 $tol = $numRelPercentTolDefault; 595 $tolerance_mode = 'relative'; 596 } 597 598 # Prepare the correct answer 599 $correct_answer = str_filters( $correct_answer, 'trim_whitespace' ); 600 601 # it surprises me that the match below works since the first .* is greedy. 602 my ($correct_num_answer, $correct_units) = $correct_answer =~ /^(.*)\s+([^\s]*)$/; 603 604 my %correct_units = Units::evaluate_units($correct_units); 605 if ( defined( $correct_units{'ERROR'} ) ) { 606 die "ERROR: The answer \"$correct_answer\" in the problem definition cannot be parsed:\n" . 607 "$correct_units{'ERROR'}\n"; 608 } 609 610 my $ans_evaluator = sub { 611 612 my $ans = shift; 613 $ans = '' unless defined($ans); 614 my $original_student_ans = $ans; 615 616 $ans = str_filters( $ans, 'trim_whitespace' ); 617 618 my $ans_hash = new AnswerHash( 619 'score' => 0, 620 'correct_ans' => spf($correct_num_answer,$options{'format'}) . " $correct_units", 621 'student_ans' => $ans, 622 'ans_message' => '', 623 'type' => 'num_cmp_with_units', 624 'preview_text_string' => '', 625 'original_student_ans' => $original_student_ans 626 ); 627 628 # it surprises me that the match below works since the first .* is greedy. 629 my ($num_answer, $units) = $ans =~ /^(.*)\s+([^\s]*)$/; 630 631 unless ( defined($num_answer) && $units ) { 632 # there is an error reading the input 633 if ( $ans =~ /\S/ ) { # the answer is not blank 634 $ans_hash -> setKeys( 'ans_message' => "The answer \"$ans\" could not be interpreted " . 635 "as a number or an arithmetic expression followed by a unit specification. " . 636 "Your answer must contain units." ); 637 } 638 639 return $ans_hash; 640 } 641 642 # we have been able to parse the answer into a numerical part and a unit part 643 644 $num_answer = $1; #$1 and $2 from the regular expression above 645 $units = $2; 646 647 my %units = Units::evaluate_units($units); 648 if ( defined( $units{'ERROR'} ) ) { 649 # handle error condition 650 $units{'ERROR'} = clean_up_error_msg($units{'ERROR'}); 651 652 $ans_hash -> setKeys( 'ans_message' => "$units{'ERROR'}" ); 653 654 return $ans_hash; 655 } 656 657 my $units_match = 1; 658 my $fund_unit; 659 foreach $fund_unit (keys %correct_units) { 660 next if $fund_unit eq 'factor'; 661 $units_match = 0 unless $correct_units{$fund_unit} == $units{$fund_unit}; 662 } 663 664 if ( $units_match ) { 665 666 # units are ok. Evaluate the numerical part of the answer 667 $tol = $tol * $correct_units{'factor'}/$units{'factor'} if 668 $tolerance_mode eq 'absolute'; # the tolerance is in the units specified by the instructor. 669 670 my $numerical_answer_evaluator = NUM_CMP( 'correctAnswer' => $correct_num_answer*$correct_units{'factor'}/$units{'factor'}, 671 'tolerance' => $tol, 672 'tolType' => $tolerance_mode, 673 'format' => $options{'format'}, 674 'mode' => $options{'mode'}, 675 'zeroLevel' => $options{'zeroLevel'}, 676 'zeroLevelTol' => $options{'zeroLevelTol'} ); 677 678 # because num_answer may contain an arithmetic expression rather than 679 # a number we can't multiply it by the $units{'factor'} 680 # instead we divide the correct answer by this amount; 681 # this is also why the numerical_answer_evaluator is not defined outside this subroutine. 682 683 $ans_hash = &$numerical_answer_evaluator($num_answer); 684 685 # now we need to doctor the correct answer in order to add units 686 # to it and correct for the division we did before 687 $ans_hash -> {correct_ans} = 688 prfmt( ( $ans_hash->{'correct_ans'} )*$units{'factor'}/$correct_units{'factor'}, 689 $options{'format'} ) . " $correct_units"; 690 # we also need to doctor the submitted answer to get it back in its original format. 691 692 # we don't add the units on if there is an error message from numerical_answer_evaluator 693 if ( ( $ans_hash -> {ans_message} ) =~ /^\s*$/ ) { 694 $ans_hash -> {student_ans} = $ans_hash -> {student_ans} . " $units"; 695 $ans_hash -> setKeys( original_student_ans => $ans ); 696 } 697 else { 698 # error message from numerical_answer_evaluator doesn't have units tacked on 699 $ans_hash -> setKeys( original_student_ans => $ans ); 700 } 701 } 702 else { 703 $ans_hash -> setKeys( ans_message => 'There is an error in the units for this answer.' ); 704 } 705 706 return $ans_hash; 707 }; 708 709 $ans_evaluator; 710 } 711 712 =head3 std_num_str_cmp() 713 714 NOTE: This function is maintained for compatibility. num_cmp() with the 715 'strings' parameter is slightly preferred. 716 717 std_num_str_cmp() is used when the correct answer could be either a number or a 718 string. For example, if you wanted the student to evaluate a function at number 719 of points, but write "Inf" or "Minf" if the function is unbounded. This routine 720 will provide error messages that do not give a hint as to whether the correct 721 answer is a string or a number. For numerical comparisons, std_num_cmp() is 722 used internally; for string comparisons, std_str_cmp() is used. 723 724 std_num_str_cmp( $correctAnswer ) OR 725 std_num_str_cmp( $correctAnswer, $ra_legalStrings ) OR 726 std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol ) OR 727 std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format ) OR 728 std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format, $zeroLevel ) OR 729 std_num_str_cmp( $correctAnswer, $ra_legalStrings, $relPercentTol, $format, 730 $zeroLevel, $zeroLevelTol ) 731 732 $correctAnswer -- the correct answer 733 $ra_legalStrings -- a reference to an array of legal strings, e.g. ["str1", "str2"] 734 $relPercentTol -- the error tolerance as a percentage 735 $format -- the display format 736 $zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies 737 $zeroLevelTol -- absolute tolerance to allow when correct answer is close to zero 738 739 Example: 740 ANS( std_num_str_cmp( $ans, ["Inf", "Minf", "NaN"] ) ); 741 742 =cut 743 744 sub std_num_str_cmp { 745 my ( $correctAnswer, $ra_legalStrings, $relpercentTol, $format, $zeroLevel, $zeroLevelTol ) = @_; 746 747 $ra_legalStrings = [''] unless defined $ra_legalStrings; 748 my @legalStrings = @{$ra_legalStrings}; 749 750 my $ans_evaluator = sub { 751 752 my $ans = shift; 753 my $ans_hash; 754 my $corrAnswerIsString = 0; 755 # my $studAnswerIsString = 0; ## uses new incorrect logic 756 my $studAnswerIsString = 1; 757 758 my $legalString = ''; 759 foreach $legalString (@legalStrings) { 760 if ( uc($correctAnswer) eq uc($legalString) ) { 761 $corrAnswerIsString = 1; 762 last; 763 } 764 } ## at this point $corrAnswerIsString = 0 iff correct answer is numeric 765 766 # Neither of these is perfect; the first is more general, but 767 # has problems with certain special strings like "ee", while the 768 # second doesn't support arithmetic expressions. 769 # 770 # if( $ans !~ m/^\s*([\+\-\*\/\^\(\)\[\]\{\}\s\d\.Ee]*|e|pi)\s*$/ ) { 771 # $studAnswerIsString = 1; 772 # } 773 #if( $ans !~ m/^\s*([\d+\-*\/^()]|e|pi)\s*$/ ) { 774 # $studAnswerIsString = 1; 775 #} 776 777 ## Both the above new versions are incorrect. We replace this by the original logic namely that 778 ## an answer that contain any of the symbols 779 ## a digit(0-9), +, -, *, /, ^, (, ), {, }, [, ] 780 ## or an answer that consists of "pi" or "e" alone 781 ## will be considered an arithmetic expression rather than a string answer. 782 783 if ($ans =~ m/[\d+\-*\/^(){}\[\]]|^\s*e\s*$|^\s*pi\s*$/) {$studAnswerIsString = 0;} 784 785 786 ## at this point $studAnswerIsString = 0 iff correct answer is numeric 787 788 if( $studAnswerIsString ) { 789 $ans = str_filters( $ans, 'compress_whitespace' ) 790 } 791 792 if ( ($corrAnswerIsString == 1) and ($studAnswerIsString == 1) ) { 793 my $string_answer_evaluator = std_str_cmp( $correctAnswer ); 794 $ans_hash = &$string_answer_evaluator( $ans ); 795 796 if( ($ans_hash -> {score}) != 1 ) { ## find out if string makes sense 797 my $sensibleAnswer = 0; 798 foreach $legalString (@legalStrings) { 799 if ( uc($ans) eq uc($legalString) ) { 800 $sensibleAnswer = 1; 801 last; 802 } 803 } 804 $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible 805 806 $ans_hash -> setKeys( 'ans_message' => 'Your answer is not a recognized answer' ) 807 unless ($sensibleAnswer); 808 $ans_hash -> setKeys( 'student_ans' => uc($ans) ); 809 } 810 } 811 elsif ( ($corrAnswerIsString == 0) and ($studAnswerIsString == 0) ) { 812 my $numeric_answer_evaluator = std_num_cmp($correctAnswer,$relpercentTol,$format,$zeroLevel,$zeroLevelTol); 813 $ans_hash = &$numeric_answer_evaluator($ans); 814 } 815 elsif ( ($corrAnswerIsString == 1) and ($studAnswerIsString == 0) ) { 816 my $numeric_answer_evaluator = std_num_cmp(1); 817 $ans_hash = &$numeric_answer_evaluator($ans); 818 $ans_hash -> setKeys( 'score' => 0, 819 'correct_ans' => $correctAnswer 820 ); 821 } 822 elsif ( ($corrAnswerIsString == 0) and ($studAnswerIsString == 1) ) { 823 my $string_answer_evaluator = std_str_cmp('bad'); 824 $ans_hash = &$string_answer_evaluator($ans); 825 826 $ans_hash -> setKeys( 'score' => 0, 827 'correct_ans' => $correctAnswer 828 ); 829 830 ## find out if string makes sense 831 my $sensibleAnswer = 0; 832 foreach $legalString (@legalStrings) { 833 if ( uc($ans) eq uc($legalString) ) { 834 $sensibleAnswer = 1; 835 last; 836 } 837 } 838 $sensibleAnswer = 1 unless $ans =~ /\S/; ## empty answers are sensible 839 840 $ans_hash -> setKeys( 'ans_message' => "Your answer is not a recognized answer" ) 841 unless $sensibleAnswer; 842 } 843 844 return $ans_hash; 845 }; 846 847 return $ans_evaluator; 848 } 849 850 =head3 num_cmp() 851 852 Compares a number or a list of numbers, using a named hash of options to set 853 parameters. This can make for more readable code than using the "mode"_num_cmp() 854 style, but some people find one or the other easier to remember. 855 856 ANS( num_cmp( answer or answer_array_ref, options_hash ) ); 857 858 1. the correct answer, or a reference to an array of correct answers 859 2. a hash with the following keys (all optional): 860 mode -- 'std' (default) (allows any expression evaluating to a number) 861 'strict' (only numbers are allowed) 862 'frac' (fractions are allowed) 863 'arith' (arithmetic expressions allowed) 864 format -- '%0.5f#' (default); defines formatting for the correct answer 865 tol -- an absolute tolerance, or 866 relTol -- a relative tolerance 867 units -- the units to use for the answer(s) 868 strings -- a reference to an array of strings which are valid 869 answers (works like std_num_str_cmp() ) 870 zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies 871 zeroLevelTol -- absolute tolerance to allow when answer is close to zero 872 873 Returns an answer evaluator, or (if given a reference to an array of 874 answers), a list of answer evaluators. Note that a reference to an array of 875 answers results is just a shortcut to writing a separate cum_cmp() for each 876 answer. It does not mean that any of those answers are considered correct 877 for one question. 878 879 EXAMPLES: 880 881 num_cmp( 5 ) -- correct answer is 5, using defaults for all options 882 num_cmp( [5,6,7] ) -- correct answers are 5, 6, and 7, using defaults for all options 883 num_cmp( 5, mode => 'strict' ) -- correct answer is 5, mode is strict 884 num_cmp( [5,6], relTol => 5 ) -- correct answers are 5 and 6, both with 5% relative tolerance 885 num_cmp( 6, strings => ["Inf", "Minf", "NaN"] ) -- correct answer is 6, "Inf", "Minf", and "NaN" 886 recognized as valid answers 887 888 =cut 889 890 sub num_cmp { 891 my $correctAnswer = shift @_; 892 my @opt = @_; 893 894 my %known_options = ( 'mode' => 'std', 895 'format' => $numFormatDefault, 896 'tol' => $numAbsTolDefault, 897 'relTol' => $numRelPercentTolDefault, 898 'units' => undef, 899 'strings' => undef, 900 'zeroLevel' => $numZeroLevelDefault, 901 'zeroLevelTol' => $numZeroLevelTolDefault, 902 903 'reltol' => undef, #alternate spelling 904 'unit' => undef #alternate spelling 905 ); 906 my %in_options; 907 my @output_list; 908 my %out_options; 909 910 unless( ref($correctAnswer) eq 'ARRAY' || scalar( @opt ) == 0 || 911 ( defined($opt[0]) and exists $known_options{$opt[0]} ) ) { 912 # unless the first parameter is a list of arrays 913 # or the second parameter is a known option or 914 # no options were used, 915 # use the old num_cmp which does not use options, but has inputs 916 # $relPercentTol,$format,$zeroLevel,$zeroLevelTol 917 warn "This method of using num_cmp() is deprecated. Please rewrite this" . 918 " problem using the options style of parameter passing (or" . 919 " check that your first option is spelled correctly)."; 920 my( $relPercentTol, $format, $zeroLevel, $zeroLevelTol) = @opt; 921 922 %out_options = ( 'relTol' => $relPercentTol, 923 'format' => $format, 924 'zeroLevel' => $zeroLevel, 925 'zeroLevelTol' => $zeroLevelTol, 926 'mode' => 'std' 927 ); 928 } 929 else { 930 # handle options 931 932 check_option_list( @opt ); 933 %in_options = @opt; 934 935 # both spellings maintained for compatibility 936 # relTol is preferred 937 if( defined( $in_options{'reltol'} ) ) { 938 $in_options{'relTol'} = $in_options{'reltol'}; 939 delete $in_options{'reltol'}; 940 } 941 942 # both spellings maintained for compatibility 943 # units is preferred 944 if( defined( $in_options{'unit'} ) ) { 945 $in_options{'units'} = $in_options{'unit'}; 946 delete $in_options{'unit'}; 947 } 948 949 # can't use both units and strings 950 if( defined( $in_options{'units'} ) && defined( $in_options{'strings'} ) ) { 951 warn "Can't use both 'units' and 'strings' in the same problem " . 952 "(check your parameters to num_cmp() )"; 953 } 954 955 #%out_options = %known_options; 956 foreach my $opt_name (keys %in_options) { 957 958 if( exists( $known_options{$opt_name} ) ) { 959 $out_options{$opt_name} = $in_options{$opt_name}; 960 } 961 else { 962 die "Option $opt_name is not defined for num_cmp. Answer is $correctAnswer; " . 963 "Default options are:<BR> ", display_options(%known_options); 964 } 965 } 966 } 967 968 # set tolerance flags -- note that the order of testing means that 969 # relative tolerance is the default 970 my ($tolType, $tol); 971 972 if ( defined( $out_options{'tol'} ) ) { 973 $tolType = 'absolute'; 974 $tol = $out_options{'tol'}; 975 } 976 else { 977 $tolType = 'relative'; 978 $tol = $out_options{'relTol'}; 979 } 980 981 # thread over lists 982 my @ans_list = (); 983 984 if ( ref($correctAnswer) eq 'ARRAY' ) { 985 @ans_list = @{$correctAnswer}; 986 } 987 else { 988 push( @ans_list, $correctAnswer ); 989 } 990 # produce answer evaluators 991 foreach my $ans (@ans_list) { 992 if( defined( $out_options{'units'} ) ) { 993 $ans = "$ans $out_options{'units'}"; 994 push( @output_list, numerical_compare_with_units($ans, %out_options) ); 995 } 996 elsif( defined( $out_options{'strings'} ) ) { 997 if( defined $out_options{'tol'} ) { 998 warn "You are using 'tol' (for absolute tolerance) with a num/str " . 999 "compare, which currently only uses relative tolerance. The default " . 1000 "tolerance will be used."; 1001 } 1002 1003 push( @output_list, std_num_str_cmp( $ans, $out_options{'strings'}, 1004 $out_options{'relTol'}, 1005 $out_options{'format'}, 1006 $out_options{'zeroLevel'}, 1007 $out_options{'zeroLevelTol'} 1008 ) 1009 ); 1010 } 1011 else { 1012 push(@output_list, 1013 NUM_CMP( 'correctAnswer' => $ans, 1014 'tolerance' => $tol, 1015 'tolType' => $tolType, 1016 'format' => $out_options{'format'}, 1017 'mode' => $out_options{'mode'}, 1018 'zeroLevel' => $out_options{'zeroLevel'}, 1019 'zeroLevelTol' => $out_options{'zeroLevelTol'}, 1020 ), 1021 ); 1022 } 1023 } 1024 1025 return @output_list; 1026 } 1027 1028 #legacy code for compatability purposes 1029 sub num_rel_cmp { # compare numbers 1030 std_num_cmp( @_ ); 1031 } 1032 1033 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION 1034 ## 1035 ## IN: a hash containing the following items (error-checking to be added later?): 1036 ## correctAnswer -- the correct answer 1037 ## tolerance -- the allowable margin of error 1038 ## tolType -- 'relative' or 'absolute' 1039 ## format -- the display format of the answer 1040 ## mode -- one of 'std', 'strict', 'arith', or 'frac'; 1041 ## determines allowable formats for the input 1042 ## zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies 1043 ## zeroLevelTol -- absolute tolerance to allow when answer is close to zero 1044 sub NUM_CMP { # low level numeric compare 1045 my %num_params = @_; 1046 1047 my $correctAnswer = $num_params{'correctAnswer'}; 1048 my $tol = $num_params{'tolerance'}; 1049 my $tolType = $num_params{'tolType'}; 1050 my $format = $num_params{'format'}; 1051 my $mode = $num_params{'mode'}; 1052 my $zeroLevel = $num_params{'zeroLevel'}; 1053 my $zeroLevelTol = $num_params{'zeroLevelTol'}; 1054 1055 if( $tolType eq 'relative' ) { 1056 $tol = $numRelPercentTolDefault unless defined $tol; 1057 $tol *= .01; 1058 } 1059 else { 1060 $tol = $numAbsTolDefault unless defined $tol; 1061 } 1062 $format = $numFormatDefault unless defined $format; 1063 $mode = 'std' unless defined $mode; 1064 $zeroLevel = $numZeroLevelDefault unless defined $zeroLevel; 1065 $zeroLevelTol = $numZeroLevelTolDefault unless defined $zeroLevelTol; 1066 1067 my $formattedCorrectAnswer = prfmt( $correctAnswer, $format ); 1068 1069 my $answer_evaluator = sub { 1070 my $in = shift @_; 1071 $in = '' unless defined $in; 1072 my $score = 0; 1073 my $original_student_answer = $in; 1074 my $parser = new AlgParserWithImplicitExpand; 1075 my $ret = $parser -> parse($in); 1076 my $preview_text_string = ''; 1077 my $preview_latex_string = ''; 1078 1079 if ( ref($ret) ) { ## parsed successfully 1080 $parser -> tostring(); 1081 $parser -> normalize(); 1082 $in = $parser -> tostring(); 1083 $preview_text_string = $in; 1084 $preview_latex_string = $parser -> tolatex(); 1085 1086 } 1087 else { ## error in parsing 1088 my $ans_hash = new AnswerHash( 1089 'score' => $score, 1090 'correct_ans' => $formattedCorrectAnswer, 1091 'student_ans' => "error: $parser->{htmlerror}", 1092 'ans_message' => $parser -> {error_msg}, 1093 'type' => "${mode}_number", 1094 'preview_text_string' => $preview_text_string, 1095 'preview_latex_string' => $preview_latex_string, 1096 'original_student_ans' => $original_student_answer 1097 ); 1098 1099 return $ans_hash; 1100 } 1101 1102 my $PGanswerMessage = ''; 1103 1104 my ($inVal,$correctVal,$PG_eval_errors,$PG_full_error_report); 1105 1106 $inVal = ''; 1107 $correctAnswer = math_constants($correctAnswer); 1108 my $formattedSubmittedAnswer = ''; 1109 1110 #special variable $@ holds the last error from a Perl eval statement 1111 $@=''; 1112 1113 if ($correctAnswer =~ /\S/) { 1114 ($correctVal, $PG_eval_errors,$PG_full_error_report) = PG_answer_eval($correctAnswer); 1115 } 1116 else { 1117 $PG_eval_errors = ' '; 1118 } 1119 1120 if ( $PG_eval_errors or not is_a_number($correctVal) ) { ##error message from eval or above 1121 $formattedSubmittedAnswer = $PG_eval_errors; 1122 $formattedSubmittedAnswer = clean_up_error_msg($formattedSubmittedAnswer); 1123 $PGanswerMessage = 'Tell your professor that there is an error in this problem'; 1124 my $ans_hash = new AnswerHash( 1125 'score' => $score, 1126 'correct_ans' => $formattedCorrectAnswer, 1127 'student_ans' => $formattedSubmittedAnswer, 1128 'ans_message' => $PGanswerMessage, 1129 'type' => 'number', 1130 'preview_text_string' => $preview_text_string, 1131 'preview_latex_string' => $preview_latex_string, 1132 'original_student_ans' => $original_student_answer 1133 ); 1134 1135 return $ans_hash; 1136 } 1137 1138 $in = &math_constants($in); 1139 1140 MODE_CASE: { ## bare block for "case" statement 1141 if ($mode eq 'std') { 1142 last MODE_CASE; 1143 } 1144 elsif ($mode eq 'strict') { 1145 unless (is_a_number($in)) { 1146 $PGanswerMessage = 'You must enter a number, e.g. -6, 5.3, or 6.12E-3'; 1147 $formattedSubmittedAnswer = 'Incorrect number format'; 1148 } 1149 else { 1150 last MODE_CASE; 1151 } 1152 } 1153 elsif ($mode eq 'arith') { 1154 unless (is_an_arithmetic_expression($in)) { 1155 $PGanswerMessage = 'You must enter an arithmetic expression, e.g. -6 or (2.3*4+5/3)^2'; 1156 $formattedSubmittedAnswer = 'Not an arithmetic expression'; 1157 } 1158 else { 1159 last MODE_CASE; 1160 } 1161 } 1162 elsif ($mode eq 'frac') { 1163 unless (is_a_fraction($in)) { 1164 $PGanswerMessage = 'You must enter a number or fraction , e.g. -6 or 7/13'; 1165 $formattedSubmittedAnswer = 'Not a number or fraction'; 1166 } 1167 else { 1168 last MODE_CASE; 1169 } 1170 } 1171 else { 1172 $PGanswerMessage = 'Tell your professor that there is an error in his or her answer mechanism. No mode was specified.'; 1173 $formattedSubmittedAnswer = $in; 1174 } 1175 1176 my $ans_hash = new AnswerHash( 1177 score => $score, 1178 correct_ans => $formattedCorrectAnswer, 1179 student_ans => $formattedSubmittedAnswer, 1180 ans_message => $PGanswerMessage, 1181 type => "${mode}_number", 1182 preview_text_string => $preview_text_string, 1183 preview_latex_string => $preview_latex_string, 1184 original_student_ans => $original_student_answer 1185 ); 1186 1187 return $ans_hash; 1188 } # end of MODE_CASES bare block 1189 1190 $@ = ''; 1191 if ($in =~ /\S/) { 1192 1193 ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($in); 1194 } 1195 else { 1196 $PG_eval_errors = ' '; 1197 } 1198 1199 if ($PG_eval_errors) { ##error message from eval or above 1200 $formattedSubmittedAnswer = $PG_eval_errors; 1201 $formattedSubmittedAnswer =clean_up_error_msg($formattedSubmittedAnswer); 1202 $PGanswerMessage = 'There is a syntax error in your answer'; 1203 $PGanswerMessage = '' if $PG_eval_errors eq ' '; 1204 my $ans_hash = new AnswerHash( 1205 'score' => $score, 1206 'correct_ans' => $formattedCorrectAnswer, 1207 'student_ans' => $formattedSubmittedAnswer, 1208 'ans_message' => $PGanswerMessage, 1209 'type' => "${mode}_number", 1210 'preview_text_string' => $preview_text_string, 1211 'preview_latex_string' => $preview_latex_string, 1212 'original_student_ans' => $original_student_answer 1213 ); 1214 1215 return $ans_hash; 1216 } 1217 else { 1218 $formattedSubmittedAnswer = prfmt($inVal,$format); 1219 } 1220 1221 my $permitted_error; 1222 if (defined($tolType) && $tolType eq 'absolute') { 1223 $permitted_error = $tol; 1224 } 1225 elsif ( abs($correctVal) <= $zeroLevel) { 1226 $permitted_error = $zeroLevelTol; ## want $tol to be non zero 1227 } 1228 else { 1229 $permitted_error = abs($tol*$correctVal); 1230 } 1231 1232 my $is_a_number = is_a_number($inVal); 1233 $score = 1 if ( ($is_a_number) and 1234 (abs( $inVal - $correctVal ) <= $permitted_error) ); 1235 if ($PG_eval_errors) { 1236 $PGanswerMessage = 'There is a syntax error in your answer'; 1237 } 1238 elsif (not $is_a_number) { 1239 $PGanswerMessage = 'Your answer does not evaluate to a number'; 1240 } 1241 1242 my $ans_hash = new AnswerHash( 1243 'score' => $score, 1244 'correct_ans' => $formattedCorrectAnswer, 1245 'student_ans' => $formattedSubmittedAnswer, 1246 'ans_message' => $PGanswerMessage, 1247 'type' => "${mode}_number", 1248 'preview_text_string' => $preview_text_string, 1249 'preview_latex_string' => $preview_latex_string, 1250 'original_student_ans' => $original_student_answer 1251 ); 1252 1253 return $ans_hash; 1254 }; 1255 1256 return $answer_evaluator; 1257 } 1258 1259 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION 1260 sub NUM_CMP_LIST { # low level numeric list compare 1261 my %num_params = @_; 1262 1263 my @outputList; 1264 my $ans; 1265 1266 while ( @{$num_params{'answerList'}} ) { 1267 $ans = shift @{$num_params{'answerList'}}; 1268 push( @outputList, NUM_CMP( 'correctAnswer' => $ans, 1269 'tolerance' => $num_params{'tolerance'}, 1270 'tolType' => $num_params{'tolType'}, 1271 'format' => $num_params{'format'}, 1272 'mode' => $num_params{'mode'}, 1273 'zeroLevel' => $num_params{'zeroLevel'}, 1274 'zeroLevelTol' => $num_params{'zeroLevelTol'} 1275 ) 1276 ); 1277 } 1278 1279 return @outputList; 1280 } 1281 1282 1283 1284 ########################################################################## 1285 ########################################################################## 1286 ## Function answer evaluators 1287 1288 =head2 Function Answer Evaluators 1289 1290 Function answer evaluators take in a function, compare it numerically to a 1291 correct function, and return a score. They can require an exactly equivalent 1292 function, or one that is equal up to a constant. They can accept or reject an 1293 answer based on specified tolerances for numerical deviation. 1294 1295 Function Comparison Options 1296 1297 correctEqn -- The correct equation, specified as a string. It may include 1298 all basic arithmetic operations, as well as elementary 1299 functions. Variable usage is described below. 1300 1301 Variables -- The independent variable(s). When comparing the correct 1302 equation to the student equation, each variable will be 1303 replaced by a certain number of numerical values. If 1304 the student equation agrees numerically with the correct 1305 equation, they are considered equal. Note that all 1306 comparison is numeric; it is possible (although highly 1307 unlikely and never a practical concern) for two unequal 1308 functions to yield the same numerical results. 1309 1310 Limits -- The limits of evaluation for the independent variables. 1311 Each variable is evaluated only in the half-open interval 1312 [lower_limit, upper_limit). This is useful if the function 1313 has a singularity or is not defined in a certain range. 1314 For example, the function "sqrt(-1-x)" could be evaluated 1315 in [-2,-1). 1316 1317 Tolerance -- Tolerance in function comparisons works exactly as in 1318 numerical comparisons; see the numerical comparison 1319 documentation for a complete description. Note that the 1320 tolerance does applies to the function as a whole, not 1321 each point individually. 1322 1323 Number of -- Specifies how many points to evaluate each variable at. This 1324 Points is typically 3, but can be set higher if it is felt that 1325 there is a strong possibility of "false positives." 1326 1327 Maximum -- Sets the maximum size of the constant of integration. For 1328 Constant of technical reasons concerning floating point arithmetic, if 1329 Integration the additive constant, i.e., the constant of integration, is 1330 greater (in absolute value) than maxConstantOfIntegration 1331 AND is greater than maxConstantOfIntegration times the 1332 correct value, WeBWorK will give an error message saying 1333 that it can not handle such a large constant of integration. 1334 This is to prevent e.g. cos(x) + 1E20 or even 1E20 as being 1335 accepted as a correct antiderivatives of sin(x) since 1336 floating point arithmetic cannot tell the difference 1337 between cos(x) + 1E20, 1E20, and -cos(x) + 1E20. 1338 1339 Technical note: if you examine the code for the function routines, you will see 1340 that most subroutines are simply doing some basic error-checking and then 1341 passing the parameters on to the low-level FUNCTION_CMP(). Because this routine 1342 is set up to handle multivariable functions, with single-variable functions as 1343 a special case, it is possible to pass multivariable parameters to single- 1344 variable functions. This usage is strongly discouraged as unnecessarily 1345 confusing. Avoid it. 1346 1347 Default Values (As of 7/24/2000) (Option -- Variable Name -- Value) 1348 1349 Variable -- $functVarDefault -- 'x' 1350 Relative Tolerance -- $functRelPercentTolDefault -- .1 1351 Absolute Tolerance -- $functAbsTolDefault -- .001 1352 Lower Limit -- $functLLimitDefault -- .0000001 1353 Upper Limit -- $functULimitDefault -- 1 1354 Number of Points -- $functNumOfPoints -- 3 1355 Zero Level -- $functZeroLevelDefault -- 1E-14 1356 Zero Level Tolerance -- $functZeroLevelTolDefault -- 1E-12 1357 Maximum Constant -- $functMaxConstantOfIntegration -- 1E8 1358 of Integration 1359 1360 =cut 1361 1362 =head3 Single-variable Function Comparisons 1363 1364 There are four single-variable function answer evaluators: "normal," absolute 1365 tolerance, antiderivative, and antiderivative with absolute tolerance. All 1366 parameters (other than the correct equation) are optional. 1367 1368 function_cmp( $correctEqn ) OR 1369 function_cmp( $correctEqn, $var ) OR 1370 function_cmp( $correctEqn, $var, $llimit, $ulimit ) OR 1371 function_cmp( $correctEqn, $var, $llimit, $ulimit, $relPercentTol ) OR 1372 function_cmp( $correctEqn, $var, $llimit, $ulimit, 1373 $relPercentTol, $numPoints ) OR 1374 function_cmp( $correctEqn, $var, $llimit, $ulimit, 1375 $relPercentTol, $numPoints, $zeroLevel ) OR 1376 function_cmp( $correctEqn, $var, $llimit, $ulimit, $relPercentTol, $numPoints, 1377 $zeroLevel,$zeroLevelTol ) 1378 1379 $correctEqn -- the correct equation, as a string 1380 $var -- the string representing the variable (optional) 1381 $llimit -- the lower limit of the interval to evaluate the 1382 variable in (optional) 1383 $ulimit -- the upper limit of the interval to evaluate the 1384 variable in (optional) 1385 $relPercentTol -- the error tolerance as a percentage (optional) 1386 $numPoints -- the number of points at which to evaluate the 1387 variable (optional) 1388 $zeroLevel -- if the correct answer is this close to zero, then 1389 zeroLevelTol applies (optional) 1390 $zeroLevelTol -- absolute tolerance to allow when answer is close to zero 1391 1392 function_cmp() uses standard comparison and relative tolerance. It takes a 1393 string representing a single-variable function and compares the student 1394 answer to that function numerically. 1395 1396 function_cmp_up_to_constant( $correctEqn ) OR 1397 function_cmp_up_to_constant( $correctEqn, $var ) OR 1398 function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit ) OR 1399 function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit, 1400 $relpercentTol ) OR 1401 function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit, 1402 $relpercentTol, $numOfPoints ) OR 1403 function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit, 1404 $relpercentTol, $numOfPoints, 1405 $maxConstantOfIntegration ) OR 1406 function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit, 1407 $relpercentTol, $numOfPoints, 1408 $maxConstantOfIntegration, $zeroLevel) OR 1409 function_cmp_up_to_constant( $correctEqn, $var, $llimit, $ulimit, 1410 $relpercentTol, $numOfPoints, 1411 $maxConstantOfIntegration, 1412 $zeroLevel, $zeroLevelTol ) 1413 1414 $maxConstantOfIntegration -- the maximum size of the constant of 1415 integration 1416 1417 function_cmp_up_to_constant() uses antiderivative compare and relative 1418 tolerance. All options work exactly like function_cmp(), except of course 1419 $maxConstantOfIntegration. It will accept as correct any function which 1420 differs from $correctEqn by at most a constant; that is, if 1421 $studentEqn = $correctEqn + C 1422 the answer is correct. 1423 1424 function_cmp_abs( $correctFunction ) OR 1425 function_cmp_abs( $correctFunction, $var ) OR 1426 function_cmp_abs( $correctFunction, $var, $llimit, $ulimit ) OR 1427 function_cmp_abs( $correctFunction, $var, $llimit, $ulimit, $absTol ) OR 1428 function_cmp_abs( $correctFunction, $var, $llimit, $ulimit, $absTol, 1429 $numOfPoints ) 1430 1431 $absTol -- the tolerance as an absolute value 1432 1433 function_cmp_abs() uses standard compare and absolute tolerance. All 1434 other options work exactly as for function_cmp(). 1435 1436 function_cmp_up_to_constant_abs( $correctFunction ) OR 1437 function_cmp_up_to_constant_abs( $correctFunction, $var ) OR 1438 function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit ) OR 1439 function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit, 1440 $absTol ) OR 1441 function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit, 1442 $absTol, $numOfPoints ) OR 1443 function_cmp_up_to_constant_abs( $correctFunction, $var, $llimit, $ulimit, 1444 $absTol, $numOfPoints, 1445 $maxConstantOfIntegration ) 1446 1447 function_cmp_up_to_constant_abs() uses antiderivative compare 1448 and absolute tolerance. All other options work exactly as with 1449 function_cmp_up_to_constant(). 1450 1451 Examples: 1452 1453 ANS( function_cmp( "cos(x)" ) ) -- Accepts cos(x), sin(x+pi/2), 1454 sin(x)^2 + cos(x) + cos(x)^2 -1, etc. This assumes 1455 $functVarDefault has been set to "x". 1456 ANS( function_cmp( $answer, "t" ) ) -- Assuming $answer is "cos(t)", 1457 accepts cos(t), etc. 1458 ANS( function_cmp_up_to_constant( "cos(x)" ) ) -- Accepts any 1459 antiderivative of sin(x), e.g. cos(x) + 5. 1460 ANS( function_cmp_up_to_constant( "cos(z)", "z" ) ) -- Accepts any 1461 antiderivative of sin(z), e.g. sin(z+pi/2) + 5. 1462 1463 =cut 1464 sub adaptive_function_cmp { 1465 my $correctEqn = shift; 1466 my %options = @_; 1467 check_options( \%options, 1468 'vars' => [qw( x y )], 1469 'params' => [], 1470 'limits' => [ [0,1], [0,1]], 1471 'reltol' => $main::functRelPercentTolDefault, 1472 'numPoints' => $main::functNumOfPoints, 1473 'zeroLevel' => $main::functZeroLevelDefault, 1474 'zeroLevelTol' => $main::functZeroLevelTolDefault, 1475 'debug' => 0, 1476 ); 1477 1478 my $var_ref = $options{'vars'}; 1479 my $ra_params = $options{ 'params'}; 1480 my $limit_ref = $options{'limits'}; 1481 my $relPercentTol= $options{'reltol'}; 1482 my $numPoints = $options{'numPoints'}; 1483 my $zeroLevel = $options{'zeroLevel'}; 1484 my $zeroLevelTol = $options{'zeroLevelTol'}; 1485 1486 FUNCTION_CMP( 'correctEqn' => $correctEqn, 1487 'var' => $var_ref, 1488 'limits' => $limit_ref, 1489 'tolerance' => $relPercentTol, 1490 'tolType' => 'relative', 1491 'numPoints' => $numPoints, 1492 'mode' => 'std', 1493 'maxConstantOfIntegration' => 10**100, 1494 'zeroLevel' => $zeroLevel, 1495 'zeroLevelTol' => $zeroLevelTol, 1496 'scale_norm' => 1, 1497 'params' => $ra_params, 1498 'debug' => $options{debug} , 1499 ); 1500 1501 } 1502 1503 sub function_cmp { 1504 my ($correctEqn,$var,$llimit,$ulimit,$relPercentTol,$numPoints,$zeroLevel,$zeroLevelTol) = @_; 1505 1506 if ( (scalar(@_) == 3) or (scalar(@_) > 8) or (scalar(@_) == 0) ) { 1507 function_invalid_params( $correctEqn ); 1508 } 1509 else { 1510 FUNCTION_CMP( 'correctEqn' => $correctEqn, 1511 'var' => $var, 1512 'limits' => [$llimit, $ulimit], 1513 'tolerance' => $relPercentTol, 1514 'tolType' => 'relative', 1515 'numPoints' => $numPoints, 1516 'mode' => 'std', 1517 'maxConstantOfIntegration' => 0, 1518 'zeroLevel' => $zeroLevel, 1519 'zeroLevelTol' => $zeroLevelTol 1520 ); 1521 } 1522 } 1523 1524 sub function_cmp_up_to_constant { ## for antiderivative problems 1525 my ($correctEqn,$var,$llimit,$ulimit,$relPercentTol,$numPoints,$maxConstantOfIntegration,$zeroLevel,$zeroLevelTol) = @_; 1526 1527 if ( (scalar(@_) == 3) or (scalar(@_) > 9) or (scalar(@_) == 0) ) { 1528 function_invalid_params( $correctEqn ); 1529 } 1530 else { 1531 FUNCTION_CMP( 'correctEqn' => $correctEqn, 1532 'var' => $var, 1533 'limits' => [$llimit, $ulimit], 1534 'tolerance' => $relPercentTol, 1535 'tolType' => 'relative', 1536 'numPoints' => $numPoints, 1537 'mode' => 'antider', 1538 'maxConstantOfIntegration' => $maxConstantOfIntegration, 1539 'zeroLevel' => $zeroLevel, 1540 'zeroLevelTol' => $zeroLevelTol 1541 ); 1542 } 1543 } 1544 1545 sub function_cmp_abs { ## similar to function_cmp but uses absolute tolerance 1546 my ($correctEqn,$var,$llimit,$ulimit,$absTol,$numPoints) = @_; 1547 1548 if ( (scalar(@_) == 3) or (scalar(@_) > 6) or (scalar(@_) == 0) ) { 1549 function_invalid_params( $correctEqn ); 1550 } 1551 else { 1552 FUNCTION_CMP( 'correctEqn' => $correctEqn, 1553 'var' => $var, 1554 'limits' => [$llimit, $ulimit], 1555 'tolerance' => $absTol, 1556 'tolType' => 'absolute', 1557 'numPoints' => $numPoints, 1558 'mode' => 'std', 1559 'maxConstantOfIntegration' => 0, 1560 'zeroLevel' => 0, 1561 'zeroLevelTol' => 0 1562 ); 1563 } 1564 } 1565 1566 1567 sub function_cmp_up_to_constant_abs { ## for antiderivative problems 1568 ## similar to function_cmp_up_to_constant 1569 ## but uses absolute tolerance 1570 my ($correctEqn,$var,$llimit,$ulimit,$absTol,$numPoints,$maxConstantOfIntegration) = @_; 1571 1572 if ( (scalar(@_) == 3) or (scalar(@_) > 7) or (scalar(@_) == 0) ) { 1573 function_invalid_params( $correctEqn ); 1574 } 1575 1576 else { 1577 FUNCTION_CMP( 'correctEqn' => $correctEqn, 1578 'var' => $var, 1579 'limits' => [$llimit, $ulimit], 1580 'tolerance' => $absTol, 1581 'tolType' => 'absolute', 1582 'numPoints' => $numPoints, 1583 'mode' => 'antider', 1584 'maxConstantOfIntegration' => $maxConstantOfIntegration, 1585 'zeroLevel' => 0, 1586 'zeroLevelTol' => 0 1587 ); 1588 } 1589 } 1590 1591 ## The following answer evaluator for comparing multivarable functions was 1592 ## contributed by Professor William K. Ziemer 1593 ## (Note: most of the multivariable functionality provided by Professor Ziemer 1594 ## has now been integrated into fun_cmp and FUNCTION_CMP) 1595 ############################ 1596 # W.K. Ziemer, Sep. 1999 1597 # Math Dept. CSULB 1598 # email: wziemer@csulb.edu 1599 ############################ 1600 1601 =head3 multivar_function_cmp 1602 1603 NOTE: this function is maintained for compatibility. fun_cmp() is 1604 slightly preferred. 1605 1606 usage: 1607 1608 multivar_function_cmp( $answer, $var_reference, options) 1609 $answer -- string, represents function of several variables 1610 $var_reference -- number (of variables), or list reference (e.g. ["var1","var2"] ) 1611 options: 1612 $limit_reference -- reference to list of lists (e.g. [[1,2],[3,4]]) 1613 $relPercentTol -- relative percent tolerance in answer 1614 $numPoints -- number of points to sample in for each variable 1615 $zeroLevel -- if the correct answer is this close to zero, then zeroLevelTol applies 1616 $zeroLevelTol -- absolute tolerance to allow when answer is close to zero 1617 1618 =cut 1619 1620 sub multivar_function_cmp { 1621 my ($correctEqn,$var_ref,$limit_ref,$relPercentTol,$numPoints,$zeroLevel,$zeroLevelTol) = @_; 1622 1623 if ( (scalar(@_) > 7) or (scalar(@_) < 2) ) { 1624 function_invalid_params( $correctEqn ); 1625 } 1626 1627 FUNCTION_CMP( 'correctEqn' => $correctEqn, 1628 'var' => $var_ref, 1629 'limits' => $limit_ref, 1630 'tolerance' => $relPercentTol, 1631 'tolType' => 'relative', 1632 'numPoints' => $numPoints, 1633 'mode' => 'std', 1634 'maxConstantOfIntegration' => 0, 1635 'zeroLevel' => $zeroLevel, 1636 'zeroLevelTol' => $zeroLevelTol 1637 ); 1638 } 1639 1640 =head3 fun_cmp() 1641 1642 Compares a function or a list of functions, using a named hash of options to set 1643 parameters. This can make for more readable code than using the function_cmp() 1644 style, but some people find one or the other easier to remember. 1645 1646 ANS( fun_cmp( answer or answer_array_ref, options_hash ) ); 1647 1648 1. a string containing the correct function, or a reference to an 1649 array of correct functions 1650 2. a hash containing the following items (all optional): 1651 var -- either the number of variables or a reference to an 1652 array of variable names (see below) 1653 limits -- reference to an array of arrays of limits (see below), or: 1654 mode -- 'std' (default) (function must match exactly), or: 1655 'antider' (function must match up to a constant) 1656 relTol -- (default) a relative tolerance (as a percentage), or: 1657 tol -- an absolute tolerance for error 1658 numPoints -- the number of points to evaluate the function at 1659 maxConstantOfIntegration -- maximum size of the constant of integration 1660 zeroLevel -- if the correct answer is this close to zero, then 1661 zeroLevelTol applies 1662 zeroLevelTol -- absolute tolerance to allow when answer is close to zero 1663 params -- an array of "free" parameters which can be used to adapt 1664 -- the correct answer to the submitted answer. (e.g. ['c'] for 1665 -- a constant of integration in the answer x^3/3 + c. 1666 debug -- when set to 1 this provides extra information while checking the 1667 -- the answer. 1668 1669 Returns an answer evaluator, or (if given a reference to an array 1670 of answers), a list of answer evaluators 1671 1672 ANSWER: 1673 1674 The answer must be in the form of a string. The answer can contain 1675 functions, pi, e, and arithmetic operations. However, the correct answer 1676 string follows a slightly stricter syntax than student answers; specifically, 1677 there is no implicit multiplication. So the correct answer must be "3*x" rather 1678 than "3 x". Students can still enter "3 x". 1679 1680 VARIABLES: 1681 1682 The var parameter can contain either a number or a reference to an array of 1683 variable names. If it contains a number, the variables are named automatically 1684 as follows: 1 variable -- x 1685 2 variables -- x, y 1686 3 variables -- x, y, z 1687 4 or more -- x_1, x_2, x_3, etc. 1688 If the var parameter contains a reference to an array of variable names, then 1689 the number of variables is determined by the number of items in the array. A 1690 reference to an array is created with brackets, e.g. "var => ['r', 's', 't']". 1691 If only one variable is being used, you can write either "var => ['t']" for 1692 consistency or "var => 't'" as a shortcut. The default is one variable, x. 1693 1694 LIMITS: 1695 1696 Limits are specified with the limits parameter. You may NOT use llimit/ulimit. 1697 If you specify limits for one variable, you must specify them for all variables. 1698 The limit parameter must be a reference to an array of arrays of the form 1699 [lower_limit. upper_limit], each array corresponding to the lower and upper 1700 endpoints of the (half-open) domain of one variable. For example, 1701 "vars => 2, limits => [[0,2], [-3,8]]" would cause x to be evaluated in [0,2) and 1702 y to be evaluated in [-3,8). If only one variable is being used, you can write 1703 either "limits => [[0,3]]" for consistency or "limits => [0,3]" as a shortcut. 1704 1705 EXAMPLES: 1706 1707 fun_cmp( "3*x" ) -- standard compare, variable is x 1708 fun_cmp( ["3*x", "4*x+3", "3*x**2"] ) -- standard compare, defaults used for all three functions 1709 fun_cmp( "3*t", var => 't' ) -- standard compare, variable is t 1710 fun_cmp( "5*x*y*z", var => 3 ) -- x, y and z are the variables 1711 fun_cmp( "5*x", mode => 'antider' ) -- student answer must match up to constant (i.e., 5x+C) 1712 fun_cmp( ["3*x*y", "4*x*y"], limits => [[0,2], [5,7]] ) -- x evaluated in [0,2) 1713 y evaluated in [5,7) 1714 1715 =cut 1716 1717 sub fun_cmp { 1718 my $correctAnswer = shift @_; 1719 my @opt = @_; 1720 1721 my %known_options = ( 'var' => $functVarDefault, 1722 'limits' => [[$functLLimitDefault, $functULimitDefault]], 1723 'mode' => 'std', 1724 'tol' => $functAbsTolDefault, 1725 'relTol' => $functRelPercentTolDefault, 1726 'numPoints' => $functNumOfPoints, 1727 'maxConstantOfIntegration' => $functMaxConstantOfIntegration, 1728 'zeroLevel' => $functZeroLevelDefault, 1729 'zeroLevelTol' => $functZeroLevelTolDefault, 1730 'params' => [], 1731 'debug' => 0, 1732 ); 1733 my @output_list = (); 1734 my %out_options = (); 1735 1736 check_option_list( @opt ); 1737 my %in_options = @opt; 1738 1739 # both spellings maintained for compatibility 1740 # relTol is preferred 1741 if( defined( $in_options{'reltol'} ) ) { 1742 $in_options{'relTol'} = $in_options{'reltol'}; 1743 delete $in_options{'reltol'}; 1744 } 1745 # var is preferred 1746 if( defined( $in_options{'vars'} ) ) { 1747 $in_options{'var'} = $in_options{'vars'}; 1748 delete $in_options{'vars'}; 1749 } 1750 1751 # %out_options = %known_options; # set defaults, then override them if 1752 # the function is called with an option defined. 1753 foreach my $opt_name (keys %in_options) { 1754 1755 if( exists( $known_options{$opt_name} ) ) { 1756 $out_options{$opt_name} = $in_options{$opt_name}; # if defined($in_options{$opt_name}); 1757 } 1758 else { 1759 die "Option $opt_name is not defined for fun_cmp. Answer is $correctAnswer; " . 1760 "Default options are:<BR> ", display_options(%known_options); 1761 } 1762 } 1763 1764 1765 # thread over lists 1766 my @ans_list = (); 1767 1768 if ( ref($correctAnswer) eq 'ARRAY' ) { 1769 @ans_list = @{$correctAnswer}; 1770 } 1771 else { 1772 push( @ans_list, $correctAnswer ); 1773 } 1774 1775 my ($tolType, $tol); 1776 if ( defined( $out_options{'tol'} ) ) { 1777 $tolType = 'absolute'; 1778 $tol = $out_options{'tol'}; 1779 } 1780 else { 1781 $tolType = 'relative'; 1782 $tol = $out_options{'relTol'}; 1783 } 1784 1785 # produce answer evaluators 1786 foreach my $ans (@ans_list) { 1787 push(@output_list, 1788 FUNCTION_CMP( 'correctEqn' => $ans, 1789 'var' => $out_options{'var'}, 1790 'limits' => $out_options{'limits'}, 1791 'tolerance' => $tol, 1792 'tolType' => $tolType, 1793 'numPoints' => $out_options{'numPoints'}, 1794 'mode' => $out_options{'mode'}, 1795 'maxConstantOfIntegration' => $out_options{'maxConstantOfIntegration'}, 1796 'zeroLevel' => $out_options{'zeroLevel'}, 1797 'zeroLevelTol' => $out_options{'zeroLevelTol'}, 1798 'params' => $out_options{'params'}, 1799 'debug' => $out_options{'debug'}, 1800 ), 1801 ); 1802 } 1803 1804 return @output_list; 1805 } 1806 1807 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION 1808 ## NOTE: PG_answer_eval is used instead of PG_restricted_eval in order to insure that the answer 1809 ## evaluated within the context of the package the problem was originally defined in. 1810 ## Includes multivariable modifications contributed by Professor William K. Ziemer 1811 ## 1812 ## IN: a hash consisting of the following keys (error checking to be added later?) 1813 ## correctEqn -- the correct equation as a string 1814 ## var -- the variable name as a string, 1815 ## or a reference to an array of variables 1816 ## limits -- reference to an array of arrays of type [lower,upper] 1817 ## tolerance -- the allowable margin of error 1818 ## tolType -- 'relative' or 'absolute' 1819 ## numPoints -- the number of points to evaluate the function at 1820 ## mode -- 'std' or 'antider' 1821 ## maxConstantOfIntegration -- maximum size of the constant of integration 1822 ## zeroLevel -- if the correct answer is this close to zero, 1823 ## then zeroLevelTol applies 1824 ## zeroLevelTol -- absolute tolerance to allow when answer is close to zero 1825 1826 1827 sub FUNCTION_CMP { 1828 my %func_params = @_; 1829 1830 my $correctEqn = $func_params{'correctEqn'}; 1831 my $var = $func_params{'var'}; 1832 my $ra_limits = $func_params{'limits'}; 1833 my $tol = $func_params{'tolerance'}; 1834 my $tolType = $func_params{'tolType'}; 1835 my $numPoints = $func_params{'numPoints'}; 1836 my $mode = $func_params{'mode'}; 1837 my $maxConstantOfIntegration = $func_params{'maxConstantOfIntegration'}; 1838 my $zeroLevel = $func_params{'zeroLevel'}; 1839 my $zeroLevelTol = $func_params{'zeroLevelTol'}; 1840 1841 1842 # Check that everything is defined: 1843 $mode = 'std' unless defined($mode); 1844 my @VARS = get_var_array( $var ); 1845 my @limits = get_limits_array( $ra_limits ); 1846 my @PARAMS = (); 1847 @PARAMS = @{$func_params{'params'}} if defined($func_params{'params'}); 1848 1849 if ($mode eq 'antider' ) { 1850 # doctor the equation to allow addition of a constant 1851 my $CONSTANT_PARAM = 'Q'; # unfortunately parameters must be single letters. 1852 # There is the possibility of conflict here. 1853 # 'Q' seemed less dangerous than 'C'. 1854 $correctEqn = "( $correctEqn ) + $CONSTANT_PARAM"; 1855 push(@PARAMS, $CONSTANT_PARAM); 1856 } 1857 my $dim_of_param_space = @PARAMS; # dimension of equivalence space 1858 1859 if( $tolType eq 'relative' ) { 1860 $tol = $functRelPercentTolDefault unless defined $tol; 1861 $tol *= .01; 1862 } 1863 else { 1864 $tol = $functAbsTolDefault unless defined $tol; 1865 } 1866 1867 #loop ensures that number of limits matches number of variables 1868 for( my $i = 0; $i < scalar(@VARS); $i++ ) { 1869 $limits[$i][0] = $functLLimitDefault unless defined $limits[$i][0]; 1870 $limits[$i][1] = $functULimitDefault unless defined $limits[$i][1]; 1871 } 1872 $numPoints = $functNumOfPoints unless defined $numPoints; 1873 $maxConstantOfIntegration = $functMaxConstantOfIntegration unless defined $maxConstantOfIntegration; 1874 $zeroLevel = $functZeroLevelDefault unless defined $zeroLevel; 1875 $zeroLevelTol = $functZeroLevelTolDefault unless defined $zeroLevelTol; 1876 1877 $func_params{'var'} = $var; 1878 $func_params{'limits'} = \@limits; 1879 $func_params{'tolerance'}= $tol; 1880 $func_params{'tolType'} = $tolType; 1881 $func_params{'numPoints'}= $numPoints; 1882 $func_params{'mode'} = $mode; 1883 $func_params{'maxConstantOfIntegration'} = $maxConstantOfIntegration; 1884 $func_params{'zeroLevel'} = $zeroLevel; 1885 $func_params{'zeroLevelTol'} = $zeroLevelTol; 1886 1887 my $i; #for use with loops 1888 my $PGanswerMessage = ""; 1889 my $originalCorrEqn = $correctEqn; 1890 1891 #prepare the correct answer and check it's syntax 1892 my $rh_correct_ans = new AnswerHash; 1893 $rh_correct_ans->input($correctEqn); 1894 $rh_correct_ans = check_syntax($rh_correct_ans); 1895 warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag}; 1896 $rh_correct_ans->clear_error(); 1897 $rh_correct_ans = function_from_string2($rh_correct_ans, vars => [ @VARS, @PARAMS ], store_in =>'rf_correct_ans'); 1898 my $correct_eqn_sub = $rh_correct_ans->{rf_correct_ans}; 1899 warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag}; 1900 1901 #create the evaluation points 1902 my $random_for_answers = new PGrandom($main::PG_original_problemSeed); 1903 1904 my (@evaluation_points); 1905 for( my $count = 0; $count < @PARAMS+1+$numPoints; $count++ ) { 1906 my @vars; 1907 for( my $i = 0; $i < @VARS; $i++ ) { 1908 $vars[$i] = $random_for_answers->random($limits[$i][0], $limits[$i][1], abs($limits[$i][1] - $limits[$i][0])/100 ); 1909 }; 1910 push(@evaluation_points,\@vars); 1911 } 1912 my $evaluation_points = Matrix->new_from_array_ref(\@evaluation_points); 1913 1914 #my $COEFFS = determine_param_coeffs($correct_eqn_sub,$evaluation_points[0],$numOfParameters); 1915 #warn "coeff", join(" | ", @{$COEFFS}); 1916 1917 #construct the answer evaluator 1918 my $answer_evaluator = new AnswerEvaluator; 1919 $answer_evaluator->{debug} = $func_params{debug}; 1920 $answer_evaluator->ans_hash( correct_ans => $originalCorrEqn, 1921 rf_correct_ans => $rh_correct_ans->{rf_correct_ans}, 1922 evaluation_points => \@evaluation_points, 1923 ra_param_vars => \@PARAMS, 1924 ra_vars => \@VARS, 1925 type => 'function', 1926 ); 1927 $answer_evaluator->install_pre_filter(\&check_syntax); 1928 $answer_evaluator->install_pre_filter(\&function_from_string2,vars => \@VARS); # @VARS has been guaranteed to be an array, $var might be a single string. 1929 $answer_evaluator->install_pre_filter(\&best_approx_parameters, %func_params, param_vars => \@PARAMS); 1930 $answer_evaluator->install_evaluator(\&calculate_difference_vector, %func_params); 1931 $answer_evaluator->install_evaluator(\&is_zero_array, tol => $tol ); 1932 $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; $rh_ans->clear_error('SYNTAX'); $rh_ans;} ); 1933 $answer_evaluator->install_post_filter(sub {my $rh_ans = shift; 1934 if ($rh_ans->catch_error('EVAL') ) { 1935 $rh_ans->{ans_message} = $rh_ans->{error_message}; 1936 $rh_ans->clear_error('EVAL'); 1937 } 1938 $rh_ans; 1939 }); 1940 $answer_evaluator; 1941 } 1942 1943 =head4 Filters 1944 1945 =pod 1946 1947 is_array($rh_ans) 1948 returns: $rh_ans. Throws error "NOTARRAY" if this is not an array 1949 1950 =cut 1951 1952 sub is_array{ 1953 my $rh_ans = shift; 1954 # return if the result is an array 1955 return($rh_ans) if ref($rh_ans->{student_ans}) eq 'ARRAY' ; 1956 $rh_ans->throw_error("NOTARRAY","The answer is not an array"); 1957 $rh_ans; 1958 } 1959 1960 =pod 1961 1962 check_syntax( $rh_ans, %options) 1963 returns an answer hash. 1964 1965 latex2html preview code are installed in the answer hash. 1966 The input has been transformed, changing 7pi to 7*pi or 7x to 7*x. 1967 Syntax error messages may be generated and stored in student_ans 1968 Additional syntax error messages are stored in {ans_message} and duplicated in {error_message} 1969 1970 1971 =cut 1972 1973 sub check_syntax { 1974 my $rh_ans = shift; 1975 my %options = @_; 1976 unless ( defined( $rh_ans->{student_ans} ) ) { 1977 warn "Check_syntax requires an equation in the field {student_ans} or input"; 1978 $rh_ans->throw_error("1","{student_ans} field not defined"); 1979 return $rh_ans; 1980 } 1981 my $in = $rh_ans->{student_ans}; 1982 my $parser = new AlgParserWithImplicitExpand; 1983 my $ret = $parser -> parse($in); #for use with loops 1984 1985 if ( ref($ret) ) { ## parsed successfully 1986 $parser -> tostring(); 1987 $parser -> normalize(); 1988 $rh_ans->input( $parser -> tostring() ); 1989 $rh_ans->{preview_text_string} = $in; 1990 $rh_ans->{preview_latex_string} = $parser -> tolatex(); 1991 1992 } else { ## error in parsing 1993 1994 $rh_ans->{'student_ans'} = 'syntax error:'. $parser->{htmlerror}, 1995 $rh_ans->{'ans_message'} = $parser -> {error_msg}, 1996 $rh_ans->{'preview_text_string'} = '', 1997 $rh_ans->{'preview_latex_string'} = '', 1998 $rh_ans->throw_error('SYNTAX', 'syntax error in answer:'. $parser->{htmlerror} . "$BR" .$parser -> {error_msg}); 1999 } 2000 2001 2002 2003 $rh_ans; 2004 2005 2006 } 2007 2008 =pod 2009 2010 std_num_filter($rh_ans, %options) 2011 returns $rh_ans 2012 2013 Replaces some constants using math_constants, then evaluates a perl expression. 2014 2015 2016 =cut 2017 2018 sub std_num_filter { 2019 my $rh_ans = shift; 2020 my %options = @_; 2021 my $in = $rh_ans->input(); 2022 $in = math_constants($in); 2023 $rh_ans->{type} = 'std_number'; 2024 my ($inVal,$PG_eval_errors,$PG_full_error_report); 2025 if ($in =~ /\S/) { 2026 ($inVal,$PG_eval_errors,$PG_full_error_report) = PG_answer_eval($in); 2027 } else { 2028 $PG_eval_errors = ''; 2029 } 2030 2031 if ($PG_eval_errors) { ##error message from eval or above 2032 $rh_ans->{ans_message} = 'There is a syntax error in your answer'; 2033 $rh_ans->{student_ans} = clean_up_error_msg($PG_eval_errors); 2034 } else { 2035 $rh_ans->{student_ans} = $inVal; 2036 } 2037 $rh_ans; 2038 } 2039 2040 =pod 2041 2042 std_num_array_filter($rh_ans, %options) 2043 returns $rh_ans 2044 2045 Assumes the {student_ans} field is a numerical array, and applies BOTH check_syntax and std_num_filter 2046 to each element of the array. Does it's best to generate sensible error messages for syntax errors. 2047 A typical error message displayed in {studnet_ans} might be ( 56, error message, -4). 2048 2049 =cut 2050 2051 sub std_num_array_filter{ 2052 my $rh_ans= shift; 2053 my %options = @_; 2054 my @in = @{$rh_ans->{student_ans}}; 2055 my $temp_hash = new AnswerHash; 2056 my @out=(); 2057 my $PGanswerMessage = ''; 2058 foreach my $item (@in) { # evaluate each number in the vector 2059 $temp_hash->input($item); 2060 $temp_hash = check_syntax($temp_hash); 2061 if (defined($temp_hash->{error_flag}) and $temp_hash->{error_flag} eq 'SYNTAX') { 2062 $PGanswerMessage .= $temp_hash->{ans_message}; 2063 $temp_hash->{ans_message} = undef; 2064 } else { 2065 #continue processing 2066 $temp_hash = std_num_filter($temp_hash); 2067 if (defined($temp_hash->{ans_message}) and $temp_hash->{ans_message} ) { 2068 $PGanswerMessage .= $temp_hash->{ans_message}; 2069 $temp_hash->{ans_message} = undef; 2070 } 2071 } 2072 push(@out, $temp_hash->input()); 2073 2074 } 2075 if ($PGanswerMessage) { 2076 $rh_ans->input( "( " . join(", ", @out ) . " )" ); 2077 $rh_ans->throw_error('SYTNAX', 'There is a syntax error in your answer.'); 2078 } else { 2079 $rh_ans->input( [@out] ); 2080 } 2081 $rh_ans; 2082 } 2083 2084 2085 2086 sub function_from_string2 { 2087 my $rh_ans = shift; 2088 my %options = @_; 2089 my $eqn = $rh_ans->{student_ans}; 2090 2091 my $set_default = sub { # need to use anonymous sub to 2092 # avoid "variable won't stay shared problem" 2093 my ($hash_key,$default) = @_; 2094 ( defined($options{$hash_key}) ) ? $options{$hash_key} : $default; 2095 }; 2096 my @VARS = @{ &$set_default( 'vars',[qw( x y )] ) }; 2097 my $originalEqn = $eqn; 2098 $eqn = &math_constants($eqn); 2099 for( my $i = 0; $i < @VARS; $i++ ) { 2100 $eqn =~ s/\b$VARS[$i]\b/\$VARS[$i]/g; 2101 } 2102 2103 my ($function_sub,$PG_eval_errors, $PG_full_errors) = PG_answer_eval( q! 2104 sub { 2105 my @VARS = @_; 2106 my $input_str = ''; 2107 for( my $i=0; $i<@VARS; $i++ ) { 2108 $input_str .= "\$VARS[$i] = $VARS[$i]; "; 2109 } 2110 my $PGanswerMessage; 2111 $input_str .= '! . $eqn . q!'; # need the single quotes to keep the contents of $eqn from being 2112 # evaluated when it is assigned to $input_str; 2113 my ($out, $PG_eval_errors, $PG_full_errors) = PG_answer_eval($input_str); #Finally evaluated 2114 2115 if ( defined($PG_eval_errors) and $PG_eval_errors =~ /\S/ ) { 2116 $PG_eval_errors = clean_up_error_msg($PG_eval_errors); 2117 $PGanswerMessage = $PG_eval_errors; 2118 # This message seemed to verbose, but it does give extra information, we'll see if it is needed. 2119 # "<br> There was an error in evaluating your function <br> 2120 # !. $originalEqn . q! <br> 2121 # at ( " . join(', ', @VARS) . " ) <br> 2122 # $PG_eval_errors 2123 # "; # this message appears in the answer section which is not process by Latex2HTML so it must 2124 # # be in HTML. That is why $BR is NOT used. 2125 2126 } 2127 (wantarray) ? ($out, $PGanswerMessage): $out; # PGanswerMessage may be undefined. 2128 }; 2129 !); 2130 2131 if (defined($PG_eval_errors) and $PG_eval_errors =~/\S/ ) { 2132 $PG_eval_errors = clean_up_error_msg($PG_eval_errors); 2133 2134 my $PGanswerMessage = "There was an error in converting the expression 2135 $main::BR $originalEqn $main::BR into a function. 2136 $main::BR $PG_eval_errors."; 2137 $rh_ans->{rf_student_ans} = $function_sub; 2138 $rh_ans->{ans_message} = $PGanswerMessage; 2139 $rh_ans->{error_message} = $PGanswerMessage; 2140 $rh_ans->{error_flag} = 1; 2141 return ($rh_ans); # we couldn't compile the equation return error message. 2142 } else { 2143 if (defined($options{store_in} )) { 2144 $rh_ans ->{$options{store_in}} = $function_sub; 2145 } else { 2146 $rh_ans->{rf_student_ans} = $function_sub; 2147 } 2148 return ($rh_ans); 2149 } 2150 2151 2152 } 2153 2154 2155 sub is_zero_array{ 2156 my $rh_ans = shift; 2157 my %options = @_; 2158 my $array = $rh_ans -> {ra_differences}; 2159 my $num = @$array; 2160 my $i; 2161 my $max = 0; my $mm; 2162 for ($i=0; $i< $num; $i++) { 2163 $mm = $array->[$i] ; 2164 $max = abs($mm) if abs($mm) > $max; 2165 } 2166 my $tol = $options{tol} if defined($options{tol}); 2167 $tol = 0.01*$options{reltol} if defined($options{reltol}); 2168 $tol = .000001 unless defined($tol); 2169 $rh_ans->{score} = ($max <$tol) ? 1: 0; # 1 if the array is close to 0; 2170 $rh_ans; 2171 } 2172 =pod 2173 2174 best_approx_parameters($rh_ans,%options); 2175 {rf_student_ans} # reference to the test answer 2176 {rf_correct_ans} # reference to the comparison answer 2177 {evaluation_points}, # an array of row vectors indicating the points 2178 # to evaluate when comparing the functions 2179 %options # debug => 1 gives more error answers 2180 # param_vars => [''] additional parameters used to adapt to function 2181 ) 2182 returns $rh_ans; 2183 The parameters for the comparison function which best approximates the test_function are stored 2184 in the field {ra_parameters}. 2185 2186 The last $dim_of_parms_space variables are assumed to be parameters, and it is also 2187 assumed that the function \&comparison_fun 2188 depends linearly on these variables. This function finds the values for these parameters which minimizes the 2189 Euclidean distance (L2 distance) between the test function and the comparison function and the test points specified 2190 by the array reference \@rows_of_test_points. This is assumed to be an array of arrays, with the inner arrays 2191 determining a test point. 2192 2193 The comparison function should have $dim_of_params_space more input variables than the test function. 2194 2195 =cut 2196 2197 2198 2199 2200 2201 # =pod 2202 # 2203 # Used internally: 2204 # 2205 # &$determine_param_coeff( $rf_comparison_function # a reference to the correct answer function 2206 # $ra_variables # an array of the active input variables to the functions 2207 # $dim_of_params_space # indicates the number of parameters upon which the 2208 # # the comparison function depends linearly. These are assumed to 2209 # # be the last group of inputs to the comparison function. 2210 # 2211 # %options # $options{debug} gives more error messages 2212 # 2213 # # A typical function might look like 2214 # # f(x,y,z,a,b) = x^2+a*cos(xz) + b*sin(x) with a parameter 2215 # # space of dimension 2 and a variable space of dimension 3. 2216 # ) 2217 # # returns a list of coefficients 2218 # 2219 # =cut 2220 2221 2222 sub best_approx_parameters{ 2223 my $rh_ans = shift; 2224 my %options = @_; 2225 my $errors = undef; 2226 # This subroutine for the determining the coefficents of the parameters at a given point 2227 # is pretty specialized, so it is included here as a sub-subroutine. 2228 my $determine_param_coeffs = sub { 2229 my ($rf_fun, $ra_variables, $dim_of_params_space, %options) =@_; 2230 my @zero_params=(); 2231 for(my $i=1;$i<=$dim_of_params_space;$i++){push(@zero_params,0); } 2232 my @vars = @$ra_variables; 2233 my @coeff = (); 2234 my @inputs = (@vars,@zero_params); 2235 my ($f0, $f1, $err); 2236 ($f0, $err) = &{$rf_fun}(@inputs); 2237 if (defined($err) ) { 2238 $errors .= "$err "; 2239 } else { 2240 for (my $i=@vars;$i<@inputs;$i++) { 2241 $inputs[$i]=1; # set one parameter to 1; 2242 my($f1,$err) = &$rf_fun(@inputs); 2243 if (defined($err) ) { 2244 $errors .= " $err "; 2245 } else { 2246 push(@coeff, $f1-$f0); 2247 } 2248 $inputs[$i]=0; # set it back 2249 } 2250 } 2251 (\@coeff, $errors); 2252 }; 2253 my $rf_fun = $rh_ans->{rf_student_ans}; 2254 my $rf_correct_fun = $rh_ans->{rf_correct_ans}; 2255 my $ra_vars_matrix = $rh_ans->{evaluation_points}; 2256 my $dim_of_param_space = @{$options{param_vars}}; 2257 # Short cut. Bail if there are no param_vars 2258 unless ($dim_of_param_space >0) { 2259 $rh_ans ->{ra_paramters} = []; 2260 return $rh_ans; 2261 } 2262 # inputs are row arrays in this case. 2263 my @zero_params=(); 2264 2265 for(my $i=1;$i<=$dim_of_param_space;$i++){push(@zero_params,0); } 2266 my @rows_of_vars = @$ra_vars_matrix; 2267 warn "input rows ", join("<>",@rows_of_vars) if defined($options{debug}) and $options{debug}; 2268 my $rows = @rows_of_vars; 2269 my $matrix =new Matrix($rows,$dim_of_param_space); 2270 my $rhs_vec = new Matrix($rows, 1); 2271 my $row_num = 1; 2272 my ($ra_coeff,$val2, $val1, $err1,$err2,@inputs,@vars); 2273 my $number_of_data_points = $dim_of_param_space +2; 2274 while (@rows_of_vars and $row_num <= $number_of_data_points) { 2275 2276 # get one set of data points from the test function; 2277 @vars = @{ shift(@rows_of_vars) }; 2278 ($val2, $err1) = &{$rf_fun}(@vars); 2279 $errors .= " $err1 " if defined($err1); 2280 @inputs = (@vars,@zero_params); 2281 ($val1, $err2) = &{$rf_correct_fun}(@inputs); 2282 $errors .= " $err2 " if defined($err2); 2283 2284 unless (defined($err1) or defined($err2) ) { 2285 $rhs_vec->assign($row_num,1, $val2-$val1 ); 2286 2287 # warn "rhs data val1=$val1, val2=$val2, val2 - val1 = ", $val2 - $val1 if $options{debug}; 2288 # warn "vars ", join(" | ", @vars) if $options{debug}; 2289 2290 ($ra_coeff, $err1) = &{$determine_param_coeffs}($rf_correct_fun,\@vars,$dim_of_param_space,%options); 2291 if (defined($err1) ) { 2292 $errors .= " $err1 "; 2293 } else { 2294 my @coeff = @$ra_coeff; 2295 my $col_num=1; 2296 while(@coeff) { 2297 $matrix->assign($row_num,$col_num, shift(@coeff) ); 2298 $col_num++; 2299 } 2300 } 2301 # which might be useful for functions that are not defined at some points. 2302 } 2303 $row_num++; 2304 last if $errors; # break if there are any errors. 2305 # This cuts down on the size of error messages. 2306 # However it impossible to check for equivalence at 95% of points 2307 2308 } 2309 warn "<br> best_approx_parameters: matrix1 <br> ", " $matrix " if $options{debug}; 2310 warn "<br> best_approx_parameters: vector <br> ", " $rhs_vec " if $options{debug}; 2311 2312 # we have Matrix * parameter = data_vec + perpendicular vector 2313 # where the matrix has column vectors defining the span of the parameter space 2314 # multiply both sides by Matrix_transpose and solve for the parameters 2315 # This is exactly what the method proj_coeff method does. 2316 my @array; 2317 if (defined($errors) ) { 2318 @array = (); # new Matrix($dim_of_param_space,1); 2319 } else { 2320 @array = $matrix->proj_coeff($rhs_vec)->list(); 2321 } 2322 # check size (hack) 2323 my $max = 0; 2324 foreach my $val (@array ) { 2325 $max = abs($val) if $max < abs($val); 2326 } 2327 $errors .= "At least one of the adapting parameters (perhaps the constant of integration) is too large: $max, ( the maximum is $options{maxConstantOfIntegration} )" if $max > $options{maxConstantOfIntegration}; 2328 $rh_ans->{ra_parameters} = \@array; 2329 $rh_ans->throw_error('EVAL', $errors) if defined($errors); 2330 $rh_ans; 2331 } 2332 2333 =pod 2334 2335 calculate_difference_vector( $ans_hash, %options); 2336 2337 {rf_student_ans}, # a reference to the test function 2338 {rf_correct_ans}, # a reference to the correct answer function 2339 {evaluation_points}, # an array of row vectors indicating the points 2340 # to evaluate when comparing the functions 2341 {ra_parameters} # these are the (optional) additional inputs to 2342 # the comparison function which adapt it properly 2343 # to the problem at hand. 2344 2345 %options # mode => 'rel' specifies that each element in the 2346 # difference matrix is divided by the correct answer. 2347 # unless the correct answer is nearly 0. 2348 ) 2349 2350 =cut 2351 2352 2353 sub calculate_difference_vector { 2354 my $rh_ans = shift; 2355 my %options = @_; 2356 # initialize 2357 my $rf_fun = $rh_ans -> {rf_student_ans}; 2358 my $rf_correct_fun = $rh_ans -> {rf_correct_ans}; 2359 my $ra_parameters = $rh_ans ->{ra_parameters}; 2360 my @evaluation_points = @{$rh_ans->{evaluation_points} }; 2361 my @parameters = (); 2362 @parameters = @$ra_parameters if defined($ra_parameters) and ref($ra_parameters) eq 'ARRAY'; 2363 my $errors = undef; 2364 my @differences = (); 2365 my $diff; 2366 # calculate the vector of differences between the test function and the comparison function. 2367 while (@evaluation_points) { 2368 my ($err1, $err2); 2369 my @vars = @{ shift(@evaluation_points) }; 2370 my @inputs = (@vars, @parameters); 2371 my ($inVal, $correctVal); 2372 ($inVal, $err1) = &{$rf_fun}(@vars); 2373 $errors .= " $err1 " if defined($err1); 2374 ($correctVal, $err2) =&{$rf_correct_fun}(@inputs); 2375 $errors .= " There is an error in WeBWorK's answer to this problem, please alert your instructor.<br> $err2 " if defined($err2); 2376 unless (defined($err1) or defined($err2) ) { 2377 $diff = $inVal - $correctVal; 2378 if (defined($options{tolType}) and $options{tolType} eq 'relative' ) { #relative tolerance 2379 $diff = $diff/abs( &$rf_correct_fun(@inputs) ) if $correctVal > $options{zeroLevel}; 2380 } 2381 } 2382 last if $errors; # break if there are any errors. 2383 # This cuts down on the size of error messages. 2384 # However it impossible to check for equivalence at 95% of points 2385 # which might be useful for functions that are not defined at some points. 2386 push(@differences, $diff); 2387 } 2388 $rh_ans ->{ra_differences} = \@differences; 2389 $rh_ans->throw_error('EVAL', $errors) if defined($errors); 2390 $rh_ans; 2391 } 2392 2393 2394 ########################################################################## 2395 ########################################################################## 2396 ## String answer evaluators 2397 2398 =head2 String Answer Evaluators 2399 2400 String answer evaluators compare a student string to the correct string. 2401 Different filters can be applied to allow various degrees of variation. 2402 Both the student and correct answers are subject to the same filters, to 2403 ensure that there are no unexpected matches or rejections. 2404 2405 String Filters 2406 2407 remove_whitespace -- Removes all whitespace from the string. 2408 It applies the following substitution 2409 to the string: 2410 $filteredAnswer =~ s/\s+//g; 2411 2412 compress_whitespace -- Removes leading and trailing whitespace, and 2413 replaces all other blocks of whitespace by a 2414 single space. Applies the following substitutions: 2415 $filteredAnswer =~ s/^\s*//; 2416 $filteredAnswer =~ s/\s*$//; 2417 $filteredAnswer =~ s/\s+/ /g; 2418 2419 trim_whitespace -- Removes leading and trailing whitespace. 2420 Applies the following substitutions: 2421 $filteredAnswer =~ s/^\s*//; 2422 $filteredAnswer =~ s/\s*$//; 2423 2424 ignore_case -- Ignores the case of the string. More accurately, 2425 it converts the string to uppercase (by convention). 2426 Applies the following function: 2427 $filteredAnswer = uc $filteredAnswer; 2428 2429 ignore_order -- Ignores the order of the letters in the string. 2430 This is used for problems of the form "Choose all 2431 that apply." Specifically, it removes all 2432 whitespace and lexically sorts the letters in 2433 ascending alphabetical order. Applies the following 2434 functions: 2435 $filteredAnswer = join( "", lex_sort( 2436 split( /\s*/, $filteredAnswer ) ) ); 2437 2438 =cut 2439 2440 ################################ 2441 ## STRING ANSWER FILTERS 2442 2443 ## IN: --the string to be filtered 2444 ## --a list of the filters to use 2445 ## 2446 ## OUT: --the modified string 2447 ## 2448 ## Use this subroutine instead of the 2449 ## individual filters below it 2450 sub str_filters { 2451 my $stringToFilter = shift @_; 2452 my @filters_to_use = @_; 2453 my %known_filters = ( 'remove_whitespace' => undef, 2454 'compress_whitespace' => undef, 2455 'trim_whitespace' => undef, 2456 'ignore_case' => undef, 2457 'ignore_order' => undef 2458 ); 2459 2460 #test for unknown filters 2461 my $filter; 2462 foreach $filter (@filters_to_use) { 2463 die "Unknown string filter $filter (try checking the parameters to str_cmp() )" 2464 unless exists $known_filters{$filter}; 2465 } 2466 2467 if( grep( /remove_whitespace/i, @filters_to_use ) ) { 2468 $stringToFilter = remove_whitespace( $stringToFilter ); 2469 } 2470 if( grep( /compress_whitespace/i, @filters_to_use ) ) { 2471 $stringToFilter = compress_whitespace( $stringToFilter ); 2472 } 2473 if( grep( /trim_whitespace/i, @filters_to_use ) ) { 2474 $stringToFilter = trim_whitespace( $stringToFilter ); 2475 } 2476 if( grep( /ignore_case/i, @filters_to_use ) ) { 2477 $stringToFilter = ignore_case( $stringToFilter ); 2478 } 2479 if( grep( /ignore_order/i, @filters_to_use ) ) { 2480 $stringToFilter = ignore_order( $stringToFilter ); 2481 } 2482 2483 return $stringToFilter; 2484 } 2485 2486 sub remove_whitespace { 2487 my $filteredAnswer = shift; 2488 2489 $filteredAnswer =~ s/\s+//g; # remove all whitespace 2490 2491 return $filteredAnswer; 2492 } 2493 2494 sub compress_whitespace { 2495 my $filteredAnswer = shift; 2496 2497 $filteredAnswer =~ s/^\s*//; # remove initial whitespace 2498 $filteredAnswer =~ s/\s*$//; # remove trailing whitespace 2499 $filteredAnswer =~ s/\s+/ /g; # replace spaces by single space 2500 2501 return $filteredAnswer; 2502 } 2503 2504 sub trim_whitespace { 2505 my $filteredAnswer = shift; 2506 2507 $filteredAnswer =~ s/^\s*//; # remove initial whitespace 2508 $filteredAnswer =~ s/\s*$//; # remove trailing whitespace 2509 2510 return $filteredAnswer; 2511 } 2512 2513 sub ignore_case { 2514 my $filteredAnswer = shift; 2515 2516 $filteredAnswer = uc $filteredAnswer; 2517 2518 return $filteredAnswer; 2519 } 2520 2521 sub ignore_order { 2522 my $filteredAnswer = shift; 2523 2524 $filteredAnswer = join( "", lex_sort( split( /\s*/, $filteredAnswer ) ) ); 2525 2526 return $filteredAnswer; 2527 } 2528 ################################ 2529 ## END STRING ANSWER FILTERS 2530 2531 =head3 "mode"_str_cmp functions 2532 2533 The functions of the the form "mode"_str_cmp() use different functions to 2534 specify which filters to apply. They take no options except the correct 2535 string. There are also versions which accept a list of strings. 2536 2537 std_str_cmp( $correctString ) 2538 std_str_cmp_list( @correctStringList ) 2539 Filters: compress_whitespace, ignore_case 2540 2541 std_cs_str_cmp( $correctString ) 2542 std_cs_str_cmp_list( @correctStringList ) 2543 Filters: compress_whitespace 2544 2545 strict_str_cmp( $correctString ) 2546 strict_str_cmp_list( @correctStringList ) 2547 Filters: trim_whitespace 2548 2549 unordered_str_cmp( $correctString ) 2550 unordered_str_cmp_list( @correctStringList ) 2551 Filters: ignore_order, ignore_case 2552 2553 unordered_cs_str_cmp( $correctString ) 2554 unordered_cs_str_cmp_list( @correctStringList ) 2555 Filters: ignore_order 2556 2557 ordered_str_cmp( $correctString ) 2558 ordered_str_cmp_list( @correctStringList ) 2559 Filters: remove_whitespace, ignore_case 2560 2561 ordered_cs_str_cmp( $correctString ) 2562 ordered_cs_str_cmp_list( @correctStringList ) 2563 Filters: remove_whitespace 2564 2565 Examples 2566 2567 ANS( std_str_cmp( "W. Mozart" ) ) -- Accepts "W. Mozart", "W. MOZarT", 2568 and so forth. Case insensitive. All internal spaces treated 2569 as single spaces. 2570 ANS( std_cs_str_cmp( "Mozart" ) ) -- Rejects "mozart". Same as 2571 std_str_cmp() but case sensitive. 2572 ANS( strict_str_cmp( "W. Mozart" ) ) -- Accepts only the exact string. 2573 ANS( unordered_str_cmp( "ABC" ) ) -- Accepts "a c B", "CBA" and so forth. 2574 Unordered, case insensitive, spaces ignored. 2575 ANS( unordered_cs_str_cmp( "ABC" ) ) -- Rejects "abc". Same as 2576 unordered_str_cmp() but case sensitive. 2577 ANS( ordered_str_cmp( "ABC" ) ) -- Accepts "a b C", "A B C" and so forth. 2578 Ordered, case insensitive, spaces ignored. 2579 ANS( ordered_cs_str_cmp( "ABC" ) ) -- Rejects "abc", accepts "A BC" and 2580 so forth. Same as ordered_str_cmp() but case sensitive. 2581 2582 =cut 2583 2584 sub std_str_cmp { # compare strings 2585 my $correctAnswer = shift @_; 2586 my @filters = ( 'compress_whitespace', 'ignore_case' ); 2587 my $type = 'std_str_cmp'; 2588 STR_CMP( 'correctAnswer' => $correctAnswer, 2589 'filters' => \@filters, 2590 'type' => $type 2591 ); 2592 } 2593 2594 sub std_str_cmp_list { # alias for std_str_cmp 2595 my @answerList = @_; 2596 my @output; 2597 while (@answerList) { 2598 push( @output, std_str_cmp(shift @answerList) ); 2599 } 2600 @output; 2601 } 2602 2603 sub std_cs_str_cmp { # compare strings case sensitive 2604 my $correctAnswer = shift @_; 2605 my @filters = ( 'compress_whitespace' ); 2606 my $type = 'std_cs_str_cmp'; 2607 STR_CMP( 'correctAnswer' => $correctAnswer, 2608 'filters' => \@filters, 2609 'type' => $type 2610 ); 2611 } 2612 2613 sub std_cs_str_cmp_list { # alias for std_cs_str_cmp 2614 my @answerList = @_; 2615 my @output; 2616 while (@answerList) { 2617 push( @output, std_cs_str_cmp(shift @answerList) ); 2618 } 2619 @output; 2620 } 2621 2622 sub strict_str_cmp { # strict string compare 2623 my $correctAnswer = shift @_; 2624 my @filters = ( 'trim_whitespace' ); 2625 my $type = 'strict_str_cmp'; 2626 STR_CMP( 'correctAnswer' => $correctAnswer, 2627 'filters' => \@filters, 2628 'type' => $type 2629 ); 2630 } 2631 2632 sub strict_str_cmp_list { # alias for strict_str_cmp 2633 my @answerList = @_; 2634 my @output; 2635 while (@answerList) { 2636 push( @output, strict_str_cmp(shift @answerList) ); 2637 } 2638 @output; 2639 } 2640 2641 sub unordered_str_cmp { # unordered, case insensitive, spaces ignored 2642 my $correctAnswer = shift @_; 2643 my @filters = ( 'ignore_order', 'ignore_case' ); 2644 my $type = 'unordered_str_cmp'; 2645 STR_CMP( 'correctAnswer' => $correctAnswer, 2646 'filters' => \@filters, 2647 'type' => $type 2648 ); 2649 } 2650 2651 sub unordered_str_cmp_list { # alias for unordered_str_cmp 2652 my @answerList = @_; 2653 my @output; 2654 while (@answerList) { 2655 push( @output, unordered_str_cmp(shift @answerList) ); 2656 } 2657 @output; 2658 } 2659 2660 sub unordered_cs_str_cmp { # unordered, case sensitive, spaces ignored 2661 my $correctAnswer = shift @_; 2662 my @filters = ( 'ignore_order' ); 2663 my $type = 'unordered_cs_str_cmp'; 2664 STR_CMP( 'correctAnswer' => $correctAnswer, 2665 'filters' => \@filters, 2666 'type' => $type 2667 ); 2668 } 2669 2670 sub unordered_cs_str_cmp_list { # alias for unordered_cs_str_cmp 2671 my @answerList = @_; 2672 my @output; 2673 while (@answerList) { 2674 push( @output, unordered_cs_str_cmp(shift @answerList) ); 2675 } 2676 @output; 2677 } 2678 2679 sub ordered_str_cmp { # ordered, case insensitive, spaces ignored 2680 my $correctAnswer = shift @_; 2681 my @filters = ( 'remove_whitespace', 'ignore_case' ); 2682 my $type = 'ordered_str_cmp'; 2683 STR_CMP( 'correctAnswer' => $correctAnswer, 2684 'filters' => \@filters, 2685 'type' => $type 2686 ); 2687 } 2688 2689 sub ordered_str_cmp_list { # alias for ordered_str_cmp 2690 my @answerList = @_; 2691 my @output; 2692 while (@answerList) { 2693 push( @output, ordered_str_cmp(shift @answerList) ); 2694 } 2695 @output; 2696 2697 } 2698 2699 sub ordered_cs_str_cmp { # ordered, case sensitive, spaces ignored 2700 my $correctAnswer = shift @_; 2701 my @filters = ( 'remove_whitespace' ); 2702 my $type = 'ordered_cs_str_cmp'; 2703 STR_CMP( 'correctAnswer' => $correctAnswer, 2704 'filters' => \@filters, 2705 'type' => $type 2706 ); 2707 } 2708 2709 sub ordered_cs_str_cmp_list { # alias for ordered_cs_str_cmp 2710 my @answerList = @_; 2711 my @output; 2712 while (@answerList) { 2713 push( @output, ordered_cs_str_cmp(shift @answerList) ); 2714 } 2715 @output; 2716 } 2717 2718 =head3 str_cmp() 2719 2720 Compares a string or a list of strings, using a named hash of options to set 2721 parameters. This can make for more readable code than using the "mode"_str_cmp() 2722 style, but some people find one or the other easier to remember. 2723 2724 ANS( str_cmp( answer or answer_array_ref, options_hash ) ); 2725 2726 1. the correct answer or a reference to an array of answers 2727 2. either a list of filters, or: 2728 a hash consisting of 2729 filters - a reference to an array of filters 2730 2731 Returns an answer evaluator, or (if given a reference to an array of answers), 2732 a list of answer evaluators 2733 2734 FILTERS: 2735 2736 remove_whitespace -- removes all whitespace 2737 compress_whitespace -- removes whitespace from the beginning and end of the string, 2738 and treats one or more whitespace characters in a row as a 2739 single space (true by default) 2740 trim_whitespace -- removes whitespace from the beginning and end of the string 2741 ignore_case -- ignores the case of the letters (true by default) 2742 ignore_order -- ignores the order in which letters are entered 2743 2744 EXAMPLES: 2745 2746 str_cmp( "Hello" ) -- matches "Hello", " hello" (same as std_str_cmp() ) 2747 str_cmp( ["Hello", "Goodbye"] ) -- same as std_str_cmp_list() 2748 str_cmp( " hello ", trim_whitespace ) -- matches "hello", " hello " 2749 str_cmp( "ABC", filters => 'ignore_order' ) -- matches "ACB", "A B C", but not "abc" 2750 str_cmp( "D E F", remove_whitespace, ignore_case ) -- matches "def" and "d e f" but not "fed" 2751 2752 =cut 2753 2754 sub str_cmp { 2755 my $correctAnswer = shift @_; 2756 $correctAnswer = '' unless defined($correctAnswer); 2757 my @options = @_; 2758 my $ra_filters; 2759 2760 # error-checking for filters occurs in the filters() subroutine 2761 if( not defined( $options[0] ) ) { # used with no filters as alias for std_str_cmp() 2762 @options = ( 'compress_whitespace', 'ignore_case' ); 2763 } 2764 2765 if( $options[0] eq 'filters' ) { # using filters => [f1, f2, ...] notation 2766 $ra_filters = $options[1]; 2767 } 2768 else { # using a list of filters 2769 $ra_filters = \@options; 2770 } 2771 2772 # thread over lists 2773 my @ans_list = (); 2774 2775 if ( ref($correctAnswer) eq 'ARRAY' ) { 2776 @ans_list = @{$correctAnswer}; 2777 } 2778 else { 2779 push( @ans_list, $correctAnswer ); 2780 } 2781 2782 # final_answer; 2783 my @output_list = (); 2784 2785 foreach my $ans (@ans_list) { 2786 push(@output_list, STR_CMP( 'correctAnswer' => $ans, 2787 'filters' => $ra_filters, 2788 'type' => 'str_cmp' 2789 ) 2790 ); 2791 } 2792 2793 return @output_list; 2794 } 2795 2796 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION 2797 ## 2798 ## IN: a hashtable with the following entries (error-checking to be added later?): 2799 ## correctAnswer -- the correct answer, before filtering 2800 ## filters -- reference to an array containing the filters to be applied 2801 ## type -- a string containing the type of answer evaluator in use 2802 ## OUT: a reference to an answer evaluator subroutine 2803 sub STR_CMP { 2804 my %str_params = @_; 2805 2806 $str_params{'correctAnswer'} = str_filters( $str_params{'correctAnswer'}, @{$str_params{'filters'}} ); 2807 2808 my $answer_evaluator = sub { 2809 my $in = shift @_; 2810 $in = '' unless defined $in; 2811 my $original_student_ans = $in; 2812 2813 $in = str_filters( $in, @{$str_params{'filters'}} ); 2814 2815 my $correctQ = ( $in eq $str_params{'correctAnswer'} ) ? 1: 0; 2816 my $ans_hash = new AnswerHash( 2817 'score' => $correctQ, 2818 'correct_ans' => $str_params{'correctAnswer'}, 2819 'student_ans' => $in, 2820 'ans_message' => '', 2821 'type' => $str_params{'type'}, 2822 'preview_text_string' => $in, 2823 'preview_latex_string' => $in, 2824 'original_student_ans' => $original_student_ans 2825 ); 2826 2827 return $ans_hash; 2828 }; 2829 2830 return $answer_evaluator; 2831 } 2832 2833 2834 2835 ########################################################################## 2836 ########################################################################## 2837 ## Miscellaneous answer evaluators 2838 2839 =head2 Miscellaneous Answer Evaluators (Checkboxes and Radio Buttons) 2840 2841 These evaluators do not fit any of the other categories. 2842 2843 checkbox_cmp( $correctAnswer ) 2844 2845 $correctAnswer -- a string containing the names of the correct boxes, 2846 e.g. "ACD". Note that this means that individual 2847 checkbox names can only be one character. Internally, 2848 this is largely the same as unordered_cs_str_cmp(). 2849 2850 radio_cmp( $correctAnswer ) 2851 2852 $correctAnswer -- a string containing the name of the correct radio 2853 button, e.g. "Choice1". This is case sensitive and 2854 whitespace sensitive, so the correct answer must match 2855 the name of the radio button exactly. 2856 2857 =cut 2858 2859 # added 6/14/2000 by David Etlinger 2860 # because of the conversion of the answer 2861 # string to an array, I thought it better not 2862 # to force STR_CMP() to work with this 2863 sub checkbox_cmp { 2864 my $correctAnswer = shift @_; 2865 $correctAnswer = str_filters( $correctAnswer, 'ignore_order' ); 2866 2867 my $answer_evaluator = sub { 2868 my $in = shift @_; 2869 $in = '' unless defined $in; #in case no boxes checked 2870 2871 my @temp = split( "\0", $in ); #convert "\0"-delimited string to array... 2872 $in = join( "", @temp ); #and then to a single no-delimiter string 2873 2874 my $original_student_ans = $in; #well, almost original 2875 $in = str_filters( $in, 'ignore_order' ); 2876 2877 my $correctQ = ($in eq $correctAnswer) ? 1: 0; 2878 2879 my $ans_hash = new AnswerHash( 2880 'score' => $correctQ, 2881 'correct_ans' => $correctAnswer, 2882 'student_ans' => $in, 2883 'ans_message' => "", 2884 'type' => "checkbox_cmp", 2885 'preview_text_string' => $in, 2886 'original_student_ans' => $original_student_ans 2887 ); 2888 2889 return $ans_hash; 2890 2891 }; 2892 2893 return $answer_evaluator; 2894 } 2895 2896 #added 6/28/2000 by David Etlinger 2897 #exactly the same as strict_str_cmp, 2898 #but more intuitive to the user 2899 sub radio_cmp { 2900 strict_str_cmp( @_ ); 2901 } 2902 2903 2904 2905 ########################################################################## 2906 ########################################################################## 2907 ## Text and e-mail routines 2908 2909 2910 sub store_ans_at { 2911 my $answerStringRef = shift; 2912 my %options = @_; 2913 my $ans_eval= ''; 2914 if ( ref($answerStringRef) eq 'SCALAR' ) { 2915 $ans_eval= sub { 2916 my $text = shift; 2917 $text = '' unless defined($text); 2918 $$answerStringRef = $$answerStringRef . $text; 2919 my $ans_hash = new AnswerHash( 2920 'score' => 1, 2921 'correct_ans' => '', 2922 'student_ans' => $text, 2923 'ans_message' => '', 2924 'type' => 'store_ans_at', 2925 'original_student_ans' => $text, 2926 'preview_text_string' => '' 2927 2928 ); 2929 2930 return $ans_hash; 2931 }; 2932 } 2933 else { 2934 die "Syntax error: \n The argument to store_ans_at() must be a pointer to a scalar.\n(e.g. store_ans_at(~~\$MSG) )\n\n"; 2935 } 2936 2937 return $ans_eval; 2938 } 2939 2940 2941 #### subroutines used in producing a questionnaire 2942 #### these are at least good models for other answers of this type 2943 2944 my $QUESTIONNAIRE_ANSWERS=''; # stores the answers until it is time to send them 2945 # this must be initialized before the answer evaluators are run 2946 # but that happens long after all of the text in the problem is 2947 # evaluated. 2948 # this is a utility script for cleaning up the answer output for display in 2949 #the answers. 2950 2951 2952 sub DUMMY_ANSWER { 2953 my $num = shift; 2954 qq{<INPUT TYPE="HIDDEN" NAME="answer$num" VALUE="">} 2955 } 2956 2957 sub escapeHTML { 2958 my $string = shift; 2959 $string =~ s/\n/$BR/ge; 2960 $string; 2961 } 2962 2963 # these next two subroutines show how to modify the "store_and_at()" answer 2964 # evaluator to add extra information before storing the info 2965 # They provide a good model for how to tweak answer evaluators in special cases. 2966 sub anstext { 2967 my $num = shift; 2968 my $ans_eval_template = store_ans_at(\$QUESTIONNAIRE_ANSWERS); 2969 my $ans_eval = sub { 2970 my $text = shift; 2971 $text = '' unless defined($text); 2972 my $new_text = "\n$main::psvnNumber-Problem-$main::probNum-Question-$num:\n $text "; # modify entered text 2973 my $out = &$ans_eval_template($new_text); # standard evaluator 2974 #warn "$QUESTIONNAIRE_ANSWERS"; 2975 $out->{student_ans} = escapeHTML($text); # restore original entered text 2976 $out->{correct_ans} = "Question $num answered"; 2977 $out->{original_student_ans} = escapeHTML($text); 2978 $out; 2979 }; 2980 $ans_eval; 2981 } 2982 2983 sub ansradio { 2984 my $num = shift; 2985 my $ans_eval_template = store_ans_at(\$QUESTIONNAIRE_ANSWERS); 2986 my $ans_eval = sub { 2987 my $text = shift; 2988 $text = '' unless defined($text); 2989 my $new_text = "\n$main::psvnNumber-Problem-$main::probNum-RADIO-$num:\n $text "; # modify entered text 2990 my $out = $ans_eval_template->($new_text); # standard evaluator 2991 $out->{student_ans} =escapeHTML($text); # restore original entered text 2992 $out->{original_student_ans} = escapeHTML($text); 2993 $out; 2994 }; 2995 2996 2997 $ans_eval; 2998 } 2999 3000 # This is another example of how to modify an answer evaluator to obtain 3001 # the desired behavior in a special case. Here the object is to have 3002 # have the last answer trigger the send_mail_to subroutine which mails 3003 # all of the answers to the designated address. 3004 # (This address must be listed in PG_environment{'ALLOW_MAIL_TO'} or an error occurs.) 3005 3006 sub mail_answers_to { #accepts the last answer and mails off the result 3007 my $user_address = shift; 3008 my $ans_eval = sub { 3009 3010 # then mail out all of the answers, including this last one. 3011 3012 send_mail_to( $user_address, 3013 'subject' => "$main::courseName WeBWorK questionnaire", 3014 'body' => $QUESTIONNAIRE_ANSWERS, 3015 'ALLOW_MAIL_TO' => $main::ALLOW_MAIL_TO 3016 ); 3017 3018 my $ans_hash = new AnswerHash( 'score' => 1, 3019 'correct_ans' => '', 3020 'student_ans' => 'Answer recorded', 3021 'ans_message' => '', 3022 'type' => 'send_mail_to', 3023 ); 3024 3025 return $ans_hash; 3026 }; 3027 3028 return $ans_eval; 3029 } 3030 sub mail_answers_to2 { #accepts the last answer and mails off the result 3031 my $user_address = shift; 3032 my $subject = shift; 3033 $subject = "$main::courseName WeBWorK questionnaire" unless defined $subject; 3034 3035 3036 send_mail_to($user_address, 3037 'subject' => $subject, 3038 'body' => $QUESTIONNAIRE_ANSWERS, 3039 'ALLOW_MAIL_TO' => $main::ALLOW_MAIL_TO 3040 ); 3041 3042 3043 } 3044 3045 3046 3047 ########################################################################## 3048 ########################################################################## 3049 ## Problem Grader Subroutines 3050 3051 3052 ##################################### 3053 # This is a model for plug-in problem graders 3054 ##################################### 3055 sub install_problem_grader { 3056 my $rf_problem_grader = shift; 3057 $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} = $rf_problem_grader; 3058 } 3059 3060 #this is called std only for compatability purposes; 3061 #almost everyone uses avg_problem_grader 3062 sub std_problem_grader{ 3063 my $rh_evaluated_answers = shift; 3064 my $rh_problem_state = shift; 3065 my %form_options = @_; 3066 my %evaluated_answers = %{$rh_evaluated_answers}; 3067 # The hash $rh_evaluated_answers typically contains: 3068 # 'answer1' => 34, 'answer2'=> 'Mozart', etc. 3069 3070 # By default the old problem state is simply passed back out again. 3071 my %problem_state = %$rh_problem_state; 3072 3073 3074 # %form_options might include 3075 # The user login name 3076 # The permission level of the user 3077 # The studentLogin name for this psvn. 3078 # Whether the form is asking for a refresh or is submitting a new answer. 3079 3080 # initial setup of the answer 3081 my %problem_result = ( score => 0, 3082 errors => '', 3083 type => 'std_problem_grader', 3084 msg => '', 3085 ); 3086 # Checks 3087 3088 my $ansCount = keys %evaluated_answers; # get the number of answers 3089 unless ($ansCount > 0 ) { 3090 $problem_result{msg} = "This problem did not ask any questions."; 3091 return(\%problem_result,\%problem_state); 3092 } 3093 3094 if ($ansCount > 1 ) { 3095 $problem_result{msg} = 'In order to get credit for this problem all answers must be correct.' ; 3096 } 3097 3098 unless ($form_options{answers_submitted} == 1) { 3099 return(\%problem_result,\%problem_state); 3100 } 3101 3102 my $allAnswersCorrectQ=1; 3103 foreach my $ans_name (keys %evaluated_answers) { 3104 # I'm not sure if this check is really useful. 3105 if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) ) { 3106 $allAnswersCorrectQ = 0 unless( 1 == $evaluated_answers{$ans_name}->{score} ); 3107 } 3108 else { 3109 die "Error at file ",__FILE__,"line ", __LINE__,": Answer |$ans_name| is not a hash reference\n". 3110 $evaluated_answers{$ans_name} . 3111 "This probably means that the answer evaluator for this answer\n" . 3112 "is not working correctly."; 3113 $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}"; 3114 } 3115 } 3116 # report the results 3117 $problem_result{score} = $allAnswersCorrectQ; 3118 3119 # I don't like to put in this bit of code. 3120 # It makes it hard to construct error free problem graders 3121 # I would prefer to know that the problem score was numeric. 3122 unless ($problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) { 3123 $problem_state{recorded_score} = 0; # This gets rid of non-numeric scores 3124 } 3125 # 3126 if ($allAnswersCorrectQ == 1 or $problem_state{recorded_score} == 1) { 3127 $problem_state{recorded_score} = 1; 3128 } 3129 else { 3130 $problem_state{recorded_score} = 0; 3131 } 3132 3133 $problem_state{num_of_correct_ans}++ if $allAnswersCorrectQ == 1; 3134 $problem_state{num_of_incorrect_ans}++ if $allAnswersCorrectQ == 0; 3135 (\%problem_result, \%problem_state); 3136 } 3137 3138 #the only difference between the two versions 3139 #is at the end of the subroutine, where std_problem_grader2 3140 #records the attempt only if there have been no syntax errors, 3141 #whereas std_problem_grader records it regardless 3142 sub std_problem_grader2{ 3143 my $rh_evaluated_answers = shift; 3144 my $rh_problem_state = shift; 3145 my %form_options = @_; 3146 my %evaluated_answers = %{$rh_evaluated_answers}; 3147 # The hash $rh_evaluated_answers typically contains: 3148 # 'answer1' => 34, 'answer2'=> 'Mozart', etc. 3149 3150 # By default the old problem state is simply passed back out again. 3151 my %problem_state = %$rh_problem_state; 3152 3153 3154 # %form_options might include 3155 # The user login name 3156 # The permission level of the user 3157 # The studentLogin name for this psvn. 3158 # Whether the form is asking for a refresh or is submitting a new answer. 3159 3160 # initial setup of the answer 3161 my %problem_result = ( score => 0, 3162 errors => '', 3163 type => 'std_problem_grader', 3164 msg => '', 3165 ); 3166 3167 # syntax errors are not counted. 3168 my $record_problem_attempt = 1; 3169 # Checks 3170 3171 my $ansCount = keys %evaluated_answers; # get the number of answers 3172 unless ($ansCount > 0 ) { 3173 $problem_result{msg} = "This problem did not ask any questions."; 3174 return(\%problem_result,\%problem_state); 3175 } 3176 3177 if ($ansCount > 1 ) { 3178 $problem_result{msg} = 'In order to get credit for this problem all answers must be correct.' ; 3179 } 3180 3181 unless ($form_options{answers_submitted} == 1) { 3182 return(\%problem_result,\%problem_state); 3183 } 3184 3185 my $allAnswersCorrectQ=1; 3186 foreach my $ans_name (keys %evaluated_answers) { 3187 # I'm not sure if this check is really useful. 3188 if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) ) { 3189 $allAnswersCorrectQ = 0 unless( 1 == $evaluated_answers{$ans_name}->{score} ); 3190 } 3191 else { 3192 die "Error at file ",__FILE__,"line ", __LINE__,": Answer |$ans_name| is not a hash reference\n". 3193 $evaluated_answers{$ans_name} . 3194 "This probably means that the answer evaluator for this answer\n" . 3195 "is not working correctly."; 3196 $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}"; 3197 } 3198 } 3199 # report the results 3200 $problem_result{score} = $allAnswersCorrectQ; 3201 3202 # I don't like to put in this bit of code. 3203 # It makes it hard to construct error free problem graders 3204 # I would prefer to know that the problem score was numeric. 3205 unless ($problem_state{recorded_score} =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ ) { 3206 $problem_state{recorded_score} = 0; # This gets rid of non-numeric scores 3207 } 3208 # 3209 if ($allAnswersCorrectQ == 1 or $problem_state{recorded_score} == 1) { 3210 $problem_state{recorded_score} = 1; 3211 } 3212 else { 3213 $problem_state{recorded_score} = 0; 3214 } 3215 # record attempt only if there have been no syntax errors. 3216 3217 if ($record_problem_attempt == 1) { 3218 $problem_state{num_of_correct_ans}++ if $allAnswersCorrectQ == 1; 3219 $problem_state{num_of_incorrect_ans}++ if $allAnswersCorrectQ == 0; 3220 } 3221 else { 3222 $problem_result{show_partial_correct_answers} = 0 ; # prevent partial correct answers from being shown for syntax errors. 3223 3224 } 3225 3226 (\%problem_result, \%problem_state); 3227 } 3228 3229 3230 sub avg_problem_grader{ 3231 my $rh_evaluated_answers = shift; 3232 my $rh_problem_state = shift; 3233 my %form_options = @_; 3234 my %evaluated_answers = %{$rh_evaluated_answers}; 3235 # The hash $rh_evaluated_answers typically contains: 3236 # 'answer1' => 34, 'answer2'=> 'Mozart', etc. 3237 3238 # By default the old problem state is simply passed back out again. 3239 my %problem_state = %$rh_problem_state; 3240 3241 3242 # %form_options might include 3243 # The user login name 3244 # The permission level of the user 3245 # The studentLogin name for this psvn. 3246 # Whether the form is asking for a refresh or is submitting a new answer. 3247 3248 # initial setup of the answer 3249 my $total=0; 3250 my %problem_result = ( score => 0, 3251 errors => '', 3252 type => 'avg_problem_grader', 3253 msg => '', 3254 ); 3255 my $count = keys %evaluated_answers; 3256 $problem_result{msg} = 'You can earn partial credit on this problem.' if $count >1; 3257 # Return unless answers have been submitted 3258 unless ($form_options{answers_submitted} == 1) { 3259 return(\%problem_result,\%problem_state); 3260 } 3261 3262 # Answers have been submitted -- process them. 3263 foreach my $ans_name (keys %evaluated_answers) { 3264 # I'm not sure if this check is really useful. 3265 if ( ( ref($evaluated_answers{$ans_name} ) eq 'HASH' ) or ( ref($evaluated_answers{$ans_name}) eq 'AnswerHash' ) ) { 3266 $total += $evaluated_answers{$ans_name}->{score}; 3267 } 3268 else { 3269 die "Error: Answer |$ans_name| is not a hash reference\n". 3270 $evaluated_answers{$ans_name} . 3271 "This probably means that the answer evaluator for this answer\n" . 3272 "is not working correctly."; 3273 $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}"; 3274 } 3275 } 3276 # Calculate score rounded to three places to avoid roundoff problems 3277 $problem_result{score} = $total/$count if $count; 3278 # increase recorded score if the current score is greater. 3279 $problem_state{recorded_score} = $problem_result{score} if $problem_result{score} > $problem_state{recorded_score}; 3280 3281 3282 $problem_state{num_of_correct_ans}++ if $total == $count; 3283 $problem_state{num_of_incorrect_ans}++ if $total < $count ; 3284 warn "Error in grading this problem the total $total is larger than $count" if $total > $count; 3285 (\%problem_result, \%problem_state); 3286 3287 } 3288 3289 3290 3291 ########################################################################### 3292 ### THE FOLLOWING ARE LOCAL SUBROUTINES THAT ARE MEANT TO BE CALLED ONLY FROM THIS SCRIPT. 3293 3294 3295 ## Internal routine that converts variables into the standard array format 3296 ## 3297 ## IN: one of the following: 3298 ## an undefined value (i.e., no variable was specified) 3299 ## a reference to an array of variable names -- [var1, var2] 3300 ## a number (the number of variables desired) -- 3 3301 ## one or more variable names -- (var1, var2) 3302 ## OUT: an array of variable names 3303 sub get_var_array { 3304 my $in = shift @_; 3305 my @out; 3306 3307 if( not defined($in) ) { #if nothing defined, build default array and return 3308 @out = ( $functVarDefault ); 3309 return @out; 3310 } 3311 elsif( ref( $in ) eq 'ARRAY' ) { #if given an array ref, dereference and return 3312 return @{$in}; 3313 } 3314 elsif( $in =~ /^\d+/ ) { #if given a number, set up the array and return 3315 if( $in == 1 ) { 3316 $out[0] = 'x'; 3317 } 3318 elsif( $in == 2 ) { 3319 $out[0] = 'x'; 3320 $out[1] = 'y'; 3321 } 3322 elsif( $in == 3 ) { 3323 $out[0] = 'x'; 3324 $out[1] = 'y'; 3325 $out[2] = 'z'; 3326 } 3327 else { #default to the x_1, x_2, ... convention 3328 my ($i, $tag); 3329 for( $i=0; $i < $in; $i++ ) { 3330 ## akp the above seems to be off by one 1/4/00 3331 $tag = $i + 1; ## akp 1/4/00 3332 $out[$i] = "${functVarDefault}_" . $tag; ## akp 1/4/00 3333 } 3334 } 3335 3336 return @out; 3337 } 3338 else { #if given one or more names, return as an array 3339 unshift( @_, $in ); 3340 3341 return @_; 3342 } 3343 } 3344 3345 ## Internal routine that converts limits into the standard array of arrays format 3346 ## Some of the cases are probably unneccessary, but better safe than sorry 3347 ## 3348 ## IN: one of the following: 3349 ## an undefined value (i.e., no limits were specified) 3350 ## a reference to an array of arrays of limits -- [[llim,ulim], [llim,ulim]] 3351 ## a reference to an array of limits -- [llim, ulim] 3352 ## an array of array references -- ([llim,ulim], [llim,ulim]) 3353 ## an array of limits -- (llim,ulim) 3354 ## OUT: an array of array references -- ([llim,ulim], [llim,ulim]) or ([llim,ulim]) 3355 sub get_limits_array { 3356 my $in = shift @_; 3357 my @out; 3358 3359 if( not defined($in) ) { #if nothing defined, build default array and return 3360 @out = ( [$functLLimitDefault, $functULimitDefault] ); 3361 return @out; 3362 } 3363 elsif( ref($in) eq 'ARRAY' ) { #$in is either ref to array, or ref to array of refs 3364 my @deref = @{$in}; 3365 3366 if( ref( $in->[0] ) eq 'ARRAY' ) { #$in is a ref to an array of array refs 3367 return @deref; 3368 } 3369 else { #$in was just a ref to an array of numbers 3370 @out = ( $in ); 3371 return @out; 3372 } 3373 } 3374 else { #$in was an array of references or numbers 3375 unshift( @_, $in ); 3376 3377 if( ref($_[0]) eq 'ARRAY' ) { #$in was an array of references, so just return it 3378 return @_; 3379 } 3380 else { #$in was an array of numbers 3381 @out = ( \@_ ); 3382 return @out; 3383 } 3384 } 3385 } 3386 3387 sub check_option_list { 3388 my $size = scalar(@_); 3389 if( ( $size % 2 ) != 0 ) { 3390 warn "ERROR in answer evaluator generator:\n" . 3391 "Usage: <CODE>str_cmp([\$ans1, \$ans2],%options)</CODE> 3392 or <CODE> num_cmp([\$num1, \$num2], %options)</CODE><BR> 3393 A list of inputs must be inclosed in square brackets <CODE>[\$ans1, \$ans2]</CODE>"; 3394 } 3395 } 3396 3397 # simple subroutine to display an error message when 3398 # function compares are called with invalid parameters 3399 sub function_invalid_params { 3400 my $correctEqn = shift @_; 3401 my $error_response = sub { 3402 my $PGanswerMessage = "Tell your professor that there is an error with the parameters " . 3403 "to the function answer evaluator"; 3404 return ( 0, $correctEqn, "", $PGanswerMessage ); 3405 }; 3406 3407 return $error_response; 3408 } 3409 3410 # outputs a hash to the screen 3411 sub display_options { 3412 my %options = @_; 3413 my $out_string = ""; 3414 foreach my $key (keys %options) { 3415 $out_string .= " $key => $options{$key},<BR>"; 3416 } 3417 return $out_string; 3418 } 3419 3420 sub is_a_number { 3421 my ($num) = @_; 3422 my $is_a_number = 0; 3423 return $is_a_number unless defined($num); 3424 $num =~ s/^\s*//; ## remove initial spaces 3425 $num =~ s/\s*$//; ## remove trailing spaces 3426 3427 ## the following is copied from the online perl manual 3428 if ($num =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/){ 3429 $is_a_number = 1; 3430 } 3431 3432 return $is_a_number; 3433 } 3434 3435 sub is_a_fraction { 3436 3437 ## does not test for validity, just for allowed characters 3438 ## note that an integer will qualify as a fraction 3439 my ($exp) = @_; 3440 my $is_a_fraction = 0; 3441 return $is_a_fraction unless defined($exp); 3442 if ($exp =~ /^\s*\-?\s*[\/\d\.Ee\s]*$/) { 3443 $is_a_fraction = 1; 3444 } 3445 3446 return $is_a_fraction; 3447 } 3448 3449 sub is_an_arithmetic_expression { 3450 ## does not test for validity, just for allowed characters 3451 my ($exp) = @_; 3452 my $is_an_arithmetic_expression = 0; 3453 if ($exp =~ /^[+\-*\/\^\(\)\[\]\{\}\s\d\.Ee]*$/) { 3454 $is_an_arithmetic_expression = 1; 3455 } 3456 3457 return $is_an_arithmetic_expression; 3458 } 3459 3460 #replaces pi, e, and ^ with their Perl equivalents 3461 sub math_constants { 3462 my($in) = @_; 3463 $in =~s/\bpi\b/(4*atan2(1,1))/ge; 3464 $in =~s/\be\b/(exp(1))/ge; 3465 $in =~s/\^/**/g; 3466 3467 return $in; 3468 } 3469 3470 sub clean_up_error_msg { 3471 my $msg = $_[0]; 3472 $msg =~ s/^\[[^\]]*\][^:]*://; 3473 $msg =~ s/Unquoted string//g; 3474 $msg =~ s/may\s+clash.*/does not make sense here/; 3475 $msg =~ s/\sat.*line [\d]*//g; 3476 $msg = 'error: '. $msg; 3477 3478 return $msg; 3479 } 3480 3481 #formats the student and correct answer as specified 3482 #format must be of a form suitable for sprintf (e.g. '%0.5g'), 3483 #with the exception that a '#' at the end of the string 3484 #will cause trailing zeros in the decimal part to be removed 3485 sub prfmt { 3486 my($number,$format) = @_; # attention, the order of format and number are reversed 3487 my $out; 3488 if ($format) { 3489 warn "Incorrect format used: $format. <BR> Format should look something like %4.5g<BR>" 3490 unless $format =~ /^\s*%\d*\.?\d*\w#?\s*$/; 3491 3492 if( $format =~ s/#\s*$// ) { # remove trailing zeros in the decimal 3493 $out = sprintf( $format, $number ); 3494 $out =~ s/(\.\d*?)0+$/$1/; 3495 $out =~ s/\.$//; # in case all decimal digits were zero, remove the decimal 3496 } 3497 else { 3498 $out = sprintf( $format, $number ); 3499 } 3500 3501 $out =~ s/e/E/g; # only use capital E's for exponents. Little e is for 2.71828... 3502 } 3503 else { 3504 $out = $number; 3505 } 3506 3507 return $out; 3508 } 3509 3510 =head4 3511 3512 pretty_print() 3513 3514 3515 =cut 3516 3517 sub pretty_print { 3518 my $r_input = shift; 3519 my $out = ''; 3520 if ( not ref($r_input) ) { 3521 $out = $r_input; # not a reference 3522 } elsif ("$r_input" =~/hash/i) { # this will pick up objects whose '$self' is hash and so works better than ref($r_iput). 3523 local($^W) = 0; 3524 $out .= "$r_input " ."<TABLE border = \"2\" cellpadding = \"3\" BGCOLOR = \"#FFFFFF\">"; 3525 foreach my $key (lex_sort( keys %$r_input )) { 3526 $out .= "<tr><TD> $key</TD><TD> => </td><td>".pretty_print($r_input->{$key}) . "</td></tr>"; 3527 } 3528 $out .="</table>"; 3529 } elsif (ref($r_input) eq 'ARRAY' ) { 3530 my @array = @$r_input; 3531 $out .= "( " ; 3532 while (@array) { 3533 $out .= pretty_print(shift @array) . " , "; 3534 } 3535 $out .= " )"; 3536 } elsif (ref($r_input) eq 'CODE') { 3537 $out = "$r_input"; 3538 } else { 3539 $out = $r_input; 3540 } 3541 $out; 3542 } 3543 3544 3545 sub check_options { 3546 my $rh_options = shift; 3547 warn "The first entry to check_options must be a reference to the option hash" unless ref($rh_options) eq 'HASH'; 3548 my %default_options = @_; 3549 foreach my $key (keys %$rh_options) { 3550 warn "This option |$key| is not recognized in this subroutine" unless defined($default_options{$key}); 3551 } 3552 foreach my $key (keys %default_options) { 3553 $rh_options->{$key} = $default_options{$key} unless defined($rh_options->{$key}); 3554 } 3555 } 3556 3557 3558 3559 3560 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |