[system] / trunk / pg / macros / extraAnswerEvaluators.pl Repository:
ViewVC logotype

View of /trunk/pg/macros/extraAnswerEvaluators.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5568 - (download) (as text) (annotate)
Thu Oct 25 17:11:59 2007 UTC (12 years, 3 months ago) by sh002i
File size: 15658 byte(s)
new/improved documentation

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9