[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 4963 - (download) (as text) (annotate)
Tue May 22 17:46:52 2007 UTC (12 years, 6 months ago) by dpvc
File size: 14813 byte(s)
When complex=>'ok' is used, make a COPY of the Complex context before
modifying it.  (Otherwise we modify the ORIGINAL copy, and that is
persistent from problem to problem within one httpd child, which would
mean one problem could affect later ones.)

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9