[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 6058 - (download) (as text) (annotate)
Thu Jun 25 23:28:44 2009 UTC (10 years, 6 months ago) by gage
File size: 16349 byte(s)
syncing pg HEAD with pg2.4.7 on 6/25/2009

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9