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