[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 3513 - (download) (as text) (annotate)
Sat Aug 13 18:51:54 2005 UTC (14 years, 3 months ago) by jj
File size: 14263 byte(s)
Fixed treatment of options in setting the mode and optional parameters
related to reducing unions.

    1 loadMacros('Parser.pl');
    2 
    3 =head1 NAME
    4 
    5         extraAnswerEvaluators.pl -- located in the courseScripts directory
    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::Default::context{LimitedNumeric}->copy;
  166       $context->operators->redefine(',');
  167       last;
  168     };
  169     /^arith$/i     and do {
  170       $context = $Parser::Context::Default::context{LegacyNumeric}->copy;
  171       $context->functions->disable('All');
  172       last;
  173     };
  174     /^frac$/i    and do {
  175       $context = $Parser::Context::Default::context{'LimitedNumeric-Fraction'}->copy;
  176       $context->operators->redefine(',');
  177       last;
  178     };
  179 
  180     # default
  181     $context = $Parser::Context::Default::context{LegacyNumeric}->copy;
  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::Default::context{'Complex'};
  190   }
  191   $options{tolType} = $options{tolType} || 'relative';
  192   $options{tolerance} = $options{tolerance} || $options{tol} ||
  193     $options{reltol} || $options{relTol} || $options{abstol} || 1;
  194   $options{zeroLevel} = $options{zeroLevel} || $options{zeroLevelTol} ||
  195     $main::numZeroLevelTolDefault;
  196   if ($options{tolType} eq 'absolute' or defined($options{tol})
  197     or defined($options{abstol})) {
  198     $context->flags->set(
  199       tolerance => $options{tolerance},
  200       tolType => 'absolute',
  201       );
  202   } else {
  203     $context->flags->set(
  204       tolerance => .01*$options{tolerance},
  205       tolType => 'relative',
  206       );
  207   }
  208   $context->flags->set(
  209     zeroLevel => $options{zeroLevel},
  210     zeroLevelTol => $options{zeroLevelTol},
  211     );
  212   $context->{format}{number} = $options{'format'} || $main::numFormatDefault;
  213   return($context);
  214 }
  215 
  216 =head3 interval_cmp ()
  217 
  218 Compares an interval or union of intervals.  Typical invocations are
  219 
  220   interval_cmp("(2, 3] U(7, 11)")
  221 
  222 The U is used for union symbol.  In fact, any garbage (or nothing at all)
  223 can go between intervals.  It makes sure open/closed parts of intervals
  224 are correct, unless you don't like that.  To have it ignore the difference
  225 between open and closed endpoints, use
  226 
  227   interval_cmp("(2, 3] U(7, 11)", sloppy=>'yes')
  228 
  229 interval_cmp uses num_cmp on the endpoints.  You can pass optional
  230 arguments for num_cmp, so to change the tolerance, you can use
  231 
  232   interval_cmp("(2, 3] U(3+4, 11)", relTol=>3)
  233 
  234 The intervals can be listed in any order, unless you want to force a
  235 particular order, which is signaled as
  236 
  237   interval_cmp("(2, 3] U(3+4, 11)", ordered=>'strict')
  238 
  239 You can specify infinity as an endpoint.  It will do a case-insensitive
  240 string match looking for I, Infinity, Infty, or Inf.  You can prepend a +
  241 or -, as in
  242 
  243   interval_cmp("(-inf, 3] U [e^10, infinity)")
  244 or
  245   interval_cmp("(-INF, 3] U [e^10, +I)")
  246 
  247 If the question might have an empty set as the answer, you can use
  248 the strings option to allow for it.  So
  249 
  250   interval_cmp("$ans", strings=>['empty'])
  251 
  252 will not generate an error message if the student enters the string
  253 empty.  Better still, it will mark a student answer of "empty" as correct
  254 iff this matches $ans.
  255 
  256 You can use interval_cmp for ordered pairs, or lists of ordered pairs.
  257 Internally, this is just a distinction of whether to put nice union symbols
  258 between intervals, or commas.  To get commas, use
  259 
  260   interval_cmp("(1,2), (2,3), (4,-1)", unions=>'no')
  261 
  262 Note that interval_cmp makes no attempt at simplifying overlapping intervals.
  263 This becomes an important feature when you are really checking lists of
  264 ordered pairs.
  265 
  266 Now we use the Parser package for checking intervals (or lists of
  267 points if unions=>'no').  So, one can specify the Parser options
  268 showCoordinateHints, showHints, partialCredit, and/or showLengthHints
  269 as optional arguments:
  270 
  271   interval_cmp("(1,2), (2,3), (4,-1)", unions=>'no', partialCredit=>1)
  272 
  273 Also, set differences and 'R' for all real numbers now work too since they work
  274 for Parser Intervals and Unions.
  275 
  276 =cut
  277 
  278 sub interval_cmp {
  279   my $correct_ans = shift;
  280 
  281   my %opts = @_;
  282 
  283   my $mode          = $opts{mode} || 'std';
  284   my %options       = (debug => $opts{debug});
  285   my $ans_type = ''; # set to List, Union, or String below
  286 
  287   #
  288   #  Get an apppropriate context based on the mode
  289   #
  290   my $oldContext = Context();
  291   my $context = mode2context($mode, %opts);
  292 
  293   if(defined($opts{unions}) and $opts{unions} eq 'no' ) {
  294     # This is really a list of points, not intervals at all
  295     $ans_type = 'List';
  296     $context->parens->redefine('(');
  297     $context->parens->redefine('[');
  298     $context->parens->redefine('{');
  299     $context->operators->redefine('u',using=>',');
  300     $context->operators->set(u=>{string=>", ", TeX=>',\,'});
  301   } else {
  302     $context->parens->redefine('(', from=>'Interval');
  303     $context->parens->redefine('[', from=>'Interval');
  304     $context->parens->redefine('{', from=>'Interval');
  305 
  306     $context->constants->redefine('R',from=>'Interval');
  307     $context->operators->redefine('U',from=>"Interval");
  308     $context->operators->redefine('u',from=>"Interval",using=>"U");
  309     $ans_type = 'Union';
  310   }
  311   # Take optional arguments intended for List, or Union
  312   for my $o qw( showCoordinateHints showHints partialCredit showLengthHints ) {
  313     $options{$o} = $opts{$o} || 0;
  314   }
  315   $options{showUnionReduceWarnings} = $opts{showUnionReduceWarnings};
  316   $options{studentsMustReduceUnions} = $opts{studentsMustReduceUnions};
  317   if(defined($opts{ordered}) and $opts{ordered}) {
  318     $options{ordered} = 1;
  319     # Force this option if the the union must be ordered
  320     $options{studentsMustReduceUnions} = 1;
  321   }
  322   if (defined($opts{'sloppy'}) && $opts{'sloppy'} eq 'yes') {
  323      $options{requireParenMatch} = 0;
  324   }
  325   # historically we allow more infinities
  326   $context->strings->add(
  327     'i' => {alias=>'infinity'},
  328     'infty' => {alias=>'infinity'},
  329     'minfinity' => {infinite=>1, negative=>1},
  330     'minfty' => {alias=>'minfinity'},
  331     'minf' => {alias=>'minfinity'},
  332     'mi' => {alias=>'minfinity'},
  333   );
  334   # Add any strings
  335   if ($opts{strings}) {
  336     foreach my $string (@{$opts{strings}}) {
  337       $string = uc($string);
  338       $context->strings->add($string) unless
  339         defined($context->strings->get($string));
  340       $ans_type = 'String' if $string eq uc($correct_ans);
  341     }
  342   }
  343   my $ans_eval;
  344   Context($context);
  345   if($ans_type eq 'List') {
  346     $ans_eval = List($correct_ans)->cmp(%options);
  347   } elsif($ans_type eq 'Union') {
  348     $ans_eval = Union($correct_ans)->cmp(%options);
  349   } elsif($ans_type eq 'String') {
  350     $ans_eval = List($correct_ans)->cmp(%options);
  351   } else {
  352     warn "Bug -- should not be here in interval_cmp";
  353   }
  354 
  355   Context($oldContext);
  356   return($ans_eval);
  357 }
  358 
  359 =head3 number_list_cmp ()
  360 
  361 Checks an answer which is a comma-separated list of numbers.  The actual
  362 numbers are fed to num_cmp, so all of the flexibilty of num_cmp carries
  363 over (values can be expressions to be evaluated).  For example,
  364 
  365   number_list_cmp("1, -2")
  366 
  367 will accept "1, -2", "-2, 1", or "-1-1,sqrt(1)".
  368 
  369   number_list_cmp("1^2 + 1, 2^2 + 1, 3^2 + 1", ordered=>'strict')
  370 
  371 will accept "2, 5, 10", but not "5, 2, 10".
  372 
  373 If you want to allow complex number entries, complex=>'ok' will cause it
  374 to use cplx_cmp instead:
  375 
  376   number_list_cmp("2, -2, 2i, -2i", complex=>'ok')
  377 
  378 In cases where you set complex=>'ok', be sure the problem file loads
  379 PGcomplexmacros.pl.
  380 
  381 Optional arguements for num_cmp (resp. cplx_cmp) can be used as well,
  382 such as
  383 
  384   number_list_cmp("cos(3), sqrt(111)", relTol => 3)
  385 
  386 The strings=>['hello'] argument is treated specially.  It can be used to
  387 replace the entire answer.  So
  388 
  389   number_list_cmp("cos(3), sqrt(111)", strings=>['none'])
  390 
  391 will mark "none" wrong, but not generate an error.  On the other hand,
  392 
  393   number_list_cmp("none", strings=>['none'])
  394 
  395 will mark "none" as correct.
  396 
  397 One can also specify optionnal arguments for Parser's List checker: showHints,
  398 partialCredit, and showLengthHints, as in:
  399 
  400   number_list_cmp("cos(3), sqrt(111)", partialCredit=>1)
  401 
  402 =cut
  403 
  404 sub number_list_cmp {
  405   my $list = shift;
  406 
  407   my %num_params = @_;
  408 
  409   my $mode      = $num_params{mode} || 'std';
  410   my %options     = (debug => $num_params{debug});
  411 
  412   #
  413   #  Get an apppropriate context based on the mode
  414   #
  415   my $oldContext = Context();
  416   my $context = mode2context($mode, %num_params);
  417 
  418   #$context->strings->clear;
  419   if ($num_params{strings}) {
  420     foreach my $string (@{$num_params{strings}}) {
  421       my %tex = ($string =~ m/(-?)inf(inity)?/i)? (TeX => "$1\\infty"): ();
  422       $string = uc($string);
  423       $context->strings->add($string => {%tex}) unless
  424         defined($context->strings->get($string));
  425     }
  426   }
  427 
  428   $options{ordered} = 1 if(defined($num_params{ordered}) and $opts{ordered});
  429   # These didn't exist before in number_list_cmp so they behaved like
  430   # in List()->cmp.  Now they can be optionally set
  431   for my $o qw( showHints partialCredit showLengthHints ) {
  432     $options{$o} = $num_params{$o} || 0;
  433   }
  434 
  435   Context($context);
  436   my $ans_eval = List($list)->cmp(%options);
  437   Context($oldContext);
  438   return($ans_eval);
  439 }
  440 
  441 
  442 =head3 equation_cmp ()
  443 
  444 Compares an equation.  This really piggy-backs off of fun_cmp.  It looks
  445 at LHS-RHS of the equations to see if they agree up to constant multiple.
  446 It also guards against an answer of 0=0 (which technically gives a constant
  447 multiple of any equation).  It is best suited to situations such as checking
  448 the equation of a line which might be vertical and you don't want to give
  449 that away, or checking equations of ellipses where the students answer should
  450 be quadratic.
  451 
  452 Typical invocation would be:
  453 
  454   equation_com("x^2+(y-1)^2 = 11", vars=>['x','y'])
  455 
  456 =cut
  457 
  458 sub equation_cmp {
  459   Equation_eval::equation_cmp(@_);
  460 }
  461 

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9