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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5585 - (download) (as text) (annotate)
Sat Nov 10 20:55:23 2007 UTC (12 years, 1 month ago) by gage
File size: 40294 byte(s)
Made changes in the way the default values for answer evaluators are set.
(They were frequently undefined.)

We now get them from the envir variable which seems to work.  For example:

$functAbsTolDefault            = PG_restricted_eval(q/$envir{functAbsTolDefault}/);

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright  2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: pg/macros/PGfunctionevaluators.pl,v 1.1 2007/11/08 00:00:15 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 PGfunctionevaluators.pl - Macros that generate function answer evaluators.
   20 
   21 =head1 SYNOPSIS
   22 
   23   ANS(fun_cmp($answer_or_answer_array_ref, %options));
   24 
   25   ANS(function_cmp($correctEqn, $var, $llimit, $ulimit, $relTol, $numPoints, $zeroLevel,
   26                    $zeroLevelTol));
   27   ANS(function_cmp_up_to_constant($correctEqn, $var, $llimit, $ulimit, $relpercentTol,
   28                                   $numOfPoints, $maxConstantOfIntegration, $zeroLevel,
   29                                   $zeroLevelTol));
   30   ANS(function_cmp_abs($correctFunction, $var, $llimit, $ulimit, $absTol, $numOfPoints));
   31   ANS(function_cmp_up_to_constant_abs($correctFunction, $var, $llimit, $ulimit,
   32                                       $absTol, $numOfPoints, $maxConstantOfIntegration));
   33 
   34 =head1 DESCRIPTION
   35 
   36 Function answer evaluators take in a function, compare it numerically to a
   37 correct function, and return a score. They can require an exactly equivalent
   38 function, or one that is equal up to a constant. They can accept or reject an
   39 answer based on specified tolerances for numerical deviation.
   40 
   41 The general function answer evaluator is fun_cmp(). It takes a hash of named
   42 options as parameters. There are also several specific function_cmp_*() answer
   43 evaluators for use in common situations which feature a simplified syntax.
   44 
   45 =head2 MathObjects and answer evaluators
   46 
   47 The MathObjects system provides a Formula->cmp() method that produce answer
   48 evaluators for function comparisons. fun_cmp() has been rewritten to use
   49 Formula->cmp() to produce the answer evaluator. It is recommended that you use
   50 the Formula object's cmp() method directly if possible.
   51 
   52 =cut
   53 
   54 BEGIN { be_strict() }
   55 
   56 # Until we get the PG cacheing business sorted out, we need to use
   57 # PG_restricted_eval to get the correct values for some(?) PG environment
   58 # variables. We do this once here and place the values in lexicals for later
   59 # access.
   60 my $Context;
   61 my $functAbsTolDefault;
   62 my $functLLimitDefault;
   63 my $functMaxConstantOfIntegration;
   64 my $functNumOfPoints;
   65 my $functRelPercentTolDefault;
   66 my $functULimitDefault;
   67 my $functVarDefault;
   68 my $functZeroLevelDefault;
   69 my $functZeroLevelTolDefault;
   70 my $inputs_ref;
   71 my $useOldAnswerMacros;
   72 my $user_context;
   73 sub _PGfunctionevaluators_init {
   74   $functAbsTolDefault            = PG_restricted_eval(q/$envir{functAbsTolDefault}/);
   75   $functLLimitDefault            = PG_restricted_eval(q/$envir{functLLimitDefault}/);
   76   $functMaxConstantOfIntegration = PG_restricted_eval(q/$envir{functMaxConstantOfIntegration}/);
   77   $functNumOfPoints              = PG_restricted_eval(q/$envir{functNumOfPoints}/);
   78   $functRelPercentTolDefault     = PG_restricted_eval(q/$envir{functRelPercentTolDefault}/);
   79   $functULimitDefault            = PG_restricted_eval(q/$envir{functULimitDefault}/);
   80   $functVarDefault               = PG_restricted_eval(q/$envir{functVarDefault}/);
   81   $functZeroLevelDefault         = PG_restricted_eval(q/$envir{functZeroLevelDefault}/);
   82   $functZeroLevelTolDefault      = PG_restricted_eval(q/$envir{functZeroLevelTolDefault}/);
   83   $inputs_ref                    = PG_restricted_eval(q/$envir{inputs_ref}/);
   84   $useOldAnswerMacros            = PG_restricted_eval(q/$envir{useOldAnswerMacros}/);
   85   unless ($useOldAnswerMacros) {
   86     $user_context = PG_restricted_eval(q/\%context/);
   87     $Context = sub { Parser::Context->current($user_context, @_) };
   88   }
   89 }
   90 
   91 =head1 fun_cmp
   92 
   93   ANS(fun_cmp($answer_or_answer_array_ref, %options));
   94 
   95 Compares a function or a list of functions, using a named hash of options to set
   96 parameters. This can make for more readable code than using the function_cmp()
   97 style, but some people find one or the other easier to remember.
   98 
   99 =head2 Options
  100 
  101 $answer_or_answer_array_ref can either be a string scalar representing the
  102 correct formula or a reference to an array of string scalars. If multiple
  103 formulas are provided, fun_cmp() will return a list of answer evaluators, one
  104 for each answer specified. The answer can contain functions, pi, e, and
  105 arithmetic operations. However, the correct answer string follows a slightly
  106 stricter syntax than student answers; specifically, there is no implicit
  107 multiplication. So the correct answer must be "3*x" rather than "3 x". Students
  108 can still enter "3 x".
  109 
  110 %options is a hash containing options that affect the way the comparison is
  111 performed. All hash items are optional. Allowed options are:
  112 
  113 =over
  114 
  115 =item mode
  116 
  117 This determines the evaluation mode. The recognized modes are:
  118 
  119 =over
  120 
  121 =item std (default)
  122 
  123 Function must match exactly.
  124 
  125 =item antider
  126 
  127 Function must match up to a constant.
  128 
  129 =back
  130 
  131 =item tol
  132 
  133 An absolute tolerance value. When the student and correct functions are
  134 evaluated,  the result for each evaluation point must be within a fixed distance
  135 from the correct answer to qualify. For example, an absolute tolerance of 5
  136 means that any result which is +-5 of the correct answer qualifies as correct.
  137 abstol is accepted as a synonym for tol.
  138 
  139 =item relTol
  140 
  141 A relative tolerance. Relative tolerances are given in percentages. A relative
  142 tolerance of 1 indicates that when the student's function are evaluated, the
  143 result of evaluation at each point must be within within 1% of the correct
  144 answer to qualify as correct. In other words, a student answer is correct when
  145 
  146   abs(studentAnswer - correctAnswer) <= abs(.01*relTol*correctAnswer)
  147 
  148 tol and relTol are mutually exclusive. reltol is also accpeted as a synonym for
  149 relTol.
  150 
  151 =item zeroLevel, zeroLevelTol
  152 
  153 zeroLevel and zeroLevelTol specify a alternative absolute tolerance to use when
  154 the correct answer is very close to zero.
  155 
  156 If the correct answer has an absolute value less than or equal to zeroLevel,
  157 then the student answer must be, in absolute terms, within zeroLevelTol of
  158 correctAnswer, i.e.,
  159 
  160   abs(studentAnswer - correctAnswer) <= zeroLevelTol
  161 
  162 In other words, if the correct answer is very near zero, an absolute tolerance
  163 will be used. One must do this to handle floating point answers very near zero,
  164 because of the inaccuracy of floating point arithmetic. However, the default
  165 values are almost always adequate.
  166 
  167 =item var
  168 
  169 The var parameter can contain a number, a string, or a reference to an array of
  170 variable names. If it contains a number, the variables are named automatically
  171 as follows:
  172 
  173    var | variables used
  174   -----+--------------------
  175    1   | x
  176    2   | x, y
  177    3   | x, y, z
  178    4+  | x_1, x_2, x_3, ...
  179 
  180 If the var parameter contains a reference to an array of variable names, then
  181 the number of variables is determined by the number of items in the array. For example:
  182 
  183   var=>['r','s','t']
  184 
  185 If the var parameter contains a string, the string is used as the name of a
  186 single variable. Hence, the following are equivalent:
  187 
  188   var=>['t']
  189   var=>'t'
  190 
  191 vars is recognied as a synonym for var. The default is a single variable, x.
  192 
  193 =item limits
  194 
  195 Limits are specified with the limits parameter. If you specify limits for one
  196 variable, you must specify them for all variables. The limit parameter must be a
  197 reference to an array of arrays of the form C<[$lower_limit. $upper_limit]>,
  198 each array corresponding to the lower and upper endpoints of the (half-open)
  199 domain of one variable. For example,
  200 
  201   vars=>2, limits=>[[0,2], [-3,8]]
  202 
  203 would cause x to be evaluated in [0,2) and y to be evaluated in [-3,8). If only
  204 one variable is being used, you can write either:
  205 
  206   limits => [[0,3]]
  207   limits => [0,3]
  208 
  209 domain is recognized as a synonym for limits.
  210 
  211 =item test_points
  212 
  213 In some cases, the problem writer may want to specify the points used to check a
  214 particular function.  For example, if you want to use only integer values, they
  215 can be specified.  With one variable, either of these two forms work:
  216 
  217   test_points=>[1,4,5,6]
  218   test_points=>[[1,4,5,6]]
  219 
  220 With more variables, specify the list for the first variable, then the second,
  221 and so on:
  222 
  223   vars=>['x','y'], test_points=>[[1,4,5],[7,14,29]]".
  224 
  225 If the problem writer wants random values which need to meet some special
  226 restrictions (such as being integers), they can be generated in the problem:
  227 
  228   test_points=>[random(1,50), random(1,50), random(1,50), random(1,50)]
  229 
  230 Note that test_points should not be used for function checks which involve
  231 parameters (either explicitly given by "params", or as antiderivatives).
  232 
  233 
  234 =item numPoints
  235 
  236 The number of sample points to use when evaluating the function.
  237 
  238 =item maxConstantOfIntegration
  239 
  240 Maximum size for the constant of integration (in antider mode).
  241 
  242 =item params
  243 
  244 A reference to an array of "free" parameters which can be used to adapt the
  245 correct answer to the submitted answer. (e.g. ['c'] for a constant of
  246 integration in the answer x^3/3+c.
  247 
  248 =item debug
  249 
  250 If set to one, extra debugging information will be output.
  251 
  252 =back
  253 
  254 =head2 Examples
  255 
  256   # standard compare, variable is x
  257   fun_cmp("3*x");
  258 
  259   # standard compare, defaults used for all three functions
  260   fun_cmp(["3*x", "4*x+3", "3*x**2"]);
  261 
  262   # standard compare, variable is t
  263   fun_cmp("3*t", var=>'t');
  264 
  265   # x, y and z are the variables
  266   fun_cmp("5*x*y*z", var=>3);
  267 
  268   # student answer must match up to constant (i.e., 5x+C)
  269   fun_cmp("5*x", mode=>'antider');
  270 
  271   # x is evaluated in [0,2), y in [5,7)
  272   fun_cmp(["3*x*y", "4*x*y"], limits=>[[0,2], [5,7]]);
  273 
  274 =cut
  275 
  276 sub fun_cmp {
  277   my $correctAnswer = shift @_;
  278   my %opt           = @_;
  279 
  280     assign_option_aliases( \%opt,
  281         'vars'    =>  'var',    # set the standard option 'var' to the one specified as vars
  282           'domain'  =>  'limits', # set the standard option 'limits' to the one specified as domain
  283           'reltol'    =>  'relTol',
  284           'param'   =>  'params',
  285     );
  286 
  287     set_default_options(  \%opt,
  288         'var'         =>  $functVarDefault,
  289             'params'        =>  [],
  290         'limits'        =>  [[$functLLimitDefault, $functULimitDefault]],
  291         'test_points'   => undef,
  292         'mode'          =>  'std',
  293         'tolType'       =>    (defined($opt{tol}) ) ? 'absolute' : 'relative',
  294         'tol'         =>  .01, # default mode should be relative, to obtain this tol must not be defined
  295             'relTol'        =>  $functRelPercentTolDefault,
  296         'numPoints'       =>  $functNumOfPoints,
  297         'maxConstantOfIntegration'  =>  $functMaxConstantOfIntegration,
  298         'zeroLevel'       =>  $functZeroLevelDefault,
  299         'zeroLevelTol'      =>  $functZeroLevelTolDefault,
  300             'debug'         =>  0,
  301             'diagnostics'                           =>      undef,
  302      );
  303 
  304     # allow var => 'x' as an abbreviation for var => ['x']
  305   my %out_options = %opt;
  306   unless ( ref($out_options{var}) eq 'ARRAY' || $out_options{var} =~ m/^\d+$/) {
  307     $out_options{var} = [$out_options{var}];
  308   }
  309   # allow params => 'c' as an abbreviation for params => ['c']
  310   unless ( ref($out_options{params}) eq 'ARRAY' ) {
  311     $out_options{params} = [$out_options{params}];
  312   }
  313   my ($tolType, $tol);
  314     if ($out_options{tolType} eq 'absolute') {
  315     $tolType = 'absolute';
  316     $tol = $out_options{'tol'};
  317     delete($out_options{'relTol'}) if exists( $out_options{'relTol'} );
  318   } else {
  319     $tolType = 'relative';
  320     $tol = $out_options{'relTol'};
  321     delete($out_options{'tol'}) if exists( $out_options{'tol'} );
  322   }
  323 
  324   my @output_list = ();
  325   # thread over lists
  326   my @ans_list = ();
  327 
  328   if ( ref($correctAnswer) eq 'ARRAY' ) {
  329     @ans_list = @{$correctAnswer};
  330   }
  331   else {
  332     push( @ans_list, $correctAnswer );
  333   }
  334 
  335   # produce answer evaluators
  336   foreach my $ans (@ans_list) {
  337     push(@output_list,
  338       FUNCTION_CMP(
  339           'correctEqn'    =>  $ans,
  340           'var'       =>  $out_options{'var'},
  341           'limits'      =>  $out_options{'limits'},
  342           'tolerance'     =>  $tol,
  343           'tolType'     =>  $tolType,
  344           'numPoints'     =>  $out_options{'numPoints'},
  345           'test_points' =>  $out_options{'test_points'},
  346           'mode'        =>  $out_options{'mode'},
  347           'maxConstantOfIntegration'  =>  $out_options{'maxConstantOfIntegration'},
  348           'zeroLevel'     =>  $out_options{'zeroLevel'},
  349           'zeroLevelTol'    =>  $out_options{'zeroLevelTol'},
  350           'params'      =>  $out_options{'params'},
  351           'debug'       =>  $out_options{'debug'},
  352                 'diagnostics'             =>  $out_options{'diagnostics'} ,
  353       ),
  354     );
  355   }
  356 
  357   return (wantarray) ? @output_list : $output_list[0];
  358 }
  359 
  360 =head1 Single-variable Function Comparisons
  361 
  362 There are four single-variable function answer evaluators: "normal," absolute
  363 tolerance, antiderivative, and antiderivative with absolute tolerance. All
  364 parameters (other than the correct equation) are optional.
  365 
  366 =head2 function_cmp
  367 
  368   ANS(function_cmp($correctEqn, $var, $llimit, $ulimit, $relTol, $numPoints,
  369                    $zeroLevel, $zeroLevelTol));
  370 
  371 function_cmp() uses standard comparison and relative tolerance. It takes a
  372 string representing a single-variable function and compares the student answer
  373 to that function numerically. $var, $relTol, $numPoints, $zeroLevel, and
  374 $zeroLevelTol are equivalent to the identically-named options to fun_cmp(),
  375 above. $llimit and $ulimit are combined to form the value of limits above.
  376 
  377 =cut
  378 
  379 sub function_cmp {
  380   my ($correctEqn,$var,$llimit,$ulimit,$relPercentTol,$numPoints,$zeroLevel,$zeroLevelTol) = @_;
  381 
  382   if ( (scalar(@_) == 3) or (scalar(@_) > 8) or (scalar(@_) == 0) ) {
  383     function_invalid_params( $correctEqn );
  384   }
  385   else {
  386     FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
  387         'var'           =>  $var,
  388         'limits'          =>  [$llimit, $ulimit],
  389         'tolerance'         =>  $relPercentTol,
  390         'tolType'         =>  'relative',
  391         'numPoints'         =>  $numPoints,
  392         'mode'            =>  'std',
  393         'maxConstantOfIntegration'      =>  0,
  394         'zeroLevel'         =>  $zeroLevel,
  395         'zeroLevelTol'          =>  $zeroLevelTol
  396           );
  397   }
  398 }
  399 
  400 =head2 function_cmp_up_to_constant
  401 
  402   ANS(function_cmp_up_to_constant($correctEqn, $var, $llimit, $ulimit,
  403                                   $relpercentTol, $numOfPoints,
  404                                   $maxConstantOfIntegration, $zeroLevel,
  405                                   $zeroLevelTol));
  406 
  407 function_cmp_up_to_constant() uses antiderivative compare and relative
  408 tolerance. All but the first argument are optional. All options work exactly
  409 like function_cmp(), except of course $maxConstantOfIntegration. It will accept
  410 as correct any function which differs from $correctEqn by at most a constant;
  411 that is, if
  412 
  413   $studentEqn = $correctEqn + C, where C <= $maxConstantOfIntegration
  414 
  415 the answer is correct.
  416 
  417 =cut
  418 
  419 sub function_cmp_up_to_constant { ## for antiderivative problems
  420   my ($correctEqn,$var,$llimit,$ulimit,$relPercentTol,$numPoints,$maxConstantOfIntegration,$zeroLevel,$zeroLevelTol) = @_;
  421 
  422   if ( (scalar(@_) == 3) or (scalar(@_) > 9) or (scalar(@_) == 0) ) {
  423     function_invalid_params( $correctEqn );
  424   }
  425   else {
  426     FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
  427         'var'           =>  $var,
  428         'limits'          =>  [$llimit, $ulimit],
  429         'tolerance'         =>  $relPercentTol,
  430         'tolType'         =>  'relative',
  431         'numPoints'         =>  $numPoints,
  432         'mode'            =>  'antider',
  433         'maxConstantOfIntegration'      =>  $maxConstantOfIntegration,
  434         'zeroLevel'         =>  $zeroLevel,
  435         'zeroLevelTol'          =>  $zeroLevelTol
  436           );
  437   }
  438 }
  439 
  440 =head2 function_cmp_abs
  441 
  442   ANS(function_cmp_abs($correctFunction, $var, $llimit, $ulimit, $absTol, $numOfPoints));
  443 
  444 function_cmp_abs() uses standard compare and absolute tolerance. All but the
  445 first argument are optional. $absTol defines the absolute tolerance value. See
  446 the corresponding option to fun_cmp(), above. All other options work exactly as
  447 for function_cmp().
  448 
  449 =cut
  450 
  451 sub function_cmp_abs {      ## similar to function_cmp but uses absolute tolerance
  452   my ($correctEqn,$var,$llimit,$ulimit,$absTol,$numPoints) = @_;
  453 
  454   if ( (scalar(@_) == 3) or (scalar(@_) > 6) or (scalar(@_) == 0) ) {
  455     function_invalid_params( $correctEqn );
  456   }
  457   else {
  458     FUNCTION_CMP( 'correctEqn'      =>  $correctEqn,
  459         'var'       =>  $var,
  460         'limits'      =>  [$llimit, $ulimit],
  461         'tolerance'     =>  $absTol,
  462         'tolType'     =>  'absolute',
  463         'numPoints'     =>  $numPoints,
  464         'mode'        =>  'std',
  465         'maxConstantOfIntegration'  =>  0,
  466         'zeroLevel'     =>  0,
  467         'zeroLevelTol'      =>  0
  468     );
  469   }
  470 }
  471 
  472 =head2 function_cmp_up_to_constant_abs
  473 
  474   ANS(function_cmp_up_to_constant_abs($correctFunction, $var, $llimit,
  475                                       $ulimit, $absTol, $numOfPoints,
  476                                       $maxConstantOfIntegration));
  477 
  478 function_cmp_up_to_constant_abs() uses antiderivative compare and absolute
  479 tolerance. All but the first argument are optional. $absTol defines the absolute
  480 tolerance value. See the corresponding option to fun_cmp(), above. All other
  481 options work exactly as with function_cmp_up_to_constant().
  482 
  483 =cut
  484 
  485 sub function_cmp_up_to_constant_abs  {  ## for antiderivative problems
  486                     ## similar to function_cmp_up_to_constant
  487                     ## but uses absolute tolerance
  488   my ($correctEqn,$var,$llimit,$ulimit,$absTol,$numPoints,$maxConstantOfIntegration) = @_;
  489 
  490   if ( (scalar(@_) == 3) or (scalar(@_) > 7) or (scalar(@_) == 0) ) {
  491     function_invalid_params( $correctEqn );
  492   }
  493 
  494   else {
  495     FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
  496         'var'           =>  $var,
  497         'limits'          =>  [$llimit, $ulimit],
  498         'tolerance'         =>  $absTol,
  499         'tolType'         =>  'absolute',
  500         'numPoints'         =>  $numPoints,
  501         'mode'            =>  'antider',
  502         'maxConstantOfIntegration'      =>  $maxConstantOfIntegration,
  503         'zeroLevel'         =>  0,
  504         'zeroLevelTol'          =>  0
  505     );
  506   }
  507 }
  508 
  509 =head2 adaptive_function_cmp
  510 
  511 FIXME undocumented.
  512 
  513 =cut
  514 
  515 sub adaptive_function_cmp {
  516   my $correctEqn = shift;
  517   my %options = @_;
  518   set_default_options(  \%options,
  519       'vars'      =>  [qw( x y )],
  520                   'params'    =>  [],
  521                   'limits'    =>  [ [0,1], [0,1]],
  522                   'reltol'    =>  $functRelPercentTolDefault,
  523                   'numPoints'   =>  $functNumOfPoints,
  524                   'zeroLevel'   =>  $functZeroLevelDefault,
  525                   'zeroLevelTol'  =>  $functZeroLevelTolDefault,
  526                   'debug'     =>  0,
  527             'diagnostics'           =>      undef,
  528   );
  529 
  530     my $var_ref = $options{'vars'};
  531     my $ra_params = $options{ 'params'};
  532     my $limit_ref = $options{'limits'};
  533     my $relPercentTol= $options{'reltol'};
  534     my $numPoints = $options{'numPoints'};
  535     my $zeroLevel = $options{'zeroLevel'};
  536     my $zeroLevelTol = $options{'zeroLevelTol'};
  537 
  538   FUNCTION_CMP( 'correctEqn'          =>  $correctEqn,
  539       'var'           =>  $var_ref,
  540       'limits'          =>  $limit_ref,
  541       'tolerance'         =>  $relPercentTol,
  542       'tolType'         =>  'relative',
  543       'numPoints'         =>  $numPoints,
  544       'mode'            =>  'std',
  545       'maxConstantOfIntegration'      =>  10**100,
  546       'zeroLevel'         =>  $zeroLevel,
  547       'zeroLevelTol'          =>  $zeroLevelTol,
  548       'scale_norm'                      =>    1,
  549       'params'                          =>    $ra_params,
  550       'debug'               =>  $options{debug} ,
  551       'diagnostics'           =>  $options{diagnostics} ,
  552   );
  553 }
  554 
  555 =head1 Multi-variable Function Comparisons
  556 
  557 =head2 [DEPRECATED] multivar_function_cmp
  558 
  559   ANS(multivar_function_cmp($correctFunction, $var, $limits, $relTol, $numPoints, $zeroLevel, $zeroLevelTol));
  560 
  561 This function is deprecated. Use fun_cmp instead:
  562 
  563   ANS(fun_cmp($correctFunction, var=>$var, limits=>$limits, ...));
  564 
  565 =cut
  566 
  567 ## The following answer evaluator for comparing multivarable functions was
  568 ## contributed by Professor William K. Ziemer
  569 ## (Note: most of the multivariable functionality provided by Professor Ziemer
  570 ## has now been integrated into fun_cmp and FUNCTION_CMP)
  571 ############################
  572 # W.K. Ziemer, Sep. 1999
  573 # Math Dept. CSULB
  574 # email: wziemer@csulb.edu
  575 ############################
  576 
  577 sub multivar_function_cmp {
  578   my ($correctEqn,$var_ref,$limit_ref,$relPercentTol,$numPoints,$zeroLevel,$zeroLevelTol) = @_;
  579 
  580   if ( (scalar(@_) > 7) or (scalar(@_) < 2) ) {
  581     function_invalid_params( $correctEqn );
  582   }
  583 
  584   FUNCTION_CMP( 'correctEqn'      =>  $correctEqn,
  585       'var'       =>  $var_ref,
  586       'limits'      =>  $limit_ref,
  587       'tolerance'     =>  $relPercentTol,
  588       'tolType'     =>  'relative',
  589       'numPoints'     =>  $numPoints,
  590       'mode'        =>  'std',
  591       'maxConstantOfIntegration'  =>  0,
  592       'zeroLevel'     =>  $zeroLevel,
  593       'zeroLevelTol'      =>  $zeroLevelTol
  594   );
  595 }
  596 
  597 ## LOW-LEVEL ROUTINE -- NOT NORMALLY FOR END USERS -- USE WITH CAUTION
  598 ## NOTE: PG_answer_eval is used instead of PG_restricted_eval in order to insure that the answer
  599 ## evaluated within the context of the package the problem was originally defined in.
  600 ## Includes multivariable modifications contributed by Professor William K. Ziemer
  601 ##
  602 ## IN:  a hash consisting of the following keys (error checking to be added later?)
  603 ##      correctEqn      --  the correct equation as a string
  604 ##      var       --  the variable name as a string,
  605 ##                or a reference to an array of variables
  606 ##      limits        --  reference to an array of arrays of type [lower,upper]
  607 ##      tolerance     --  the allowable margin of error
  608 ##      tolType       --  'relative' or 'absolute'
  609 ##      numPoints     --  the number of points to evaluate the function at
  610 ##      mode        --  'std' or 'antider'
  611 ##      maxConstantOfIntegration  --  maximum size of the constant of integration
  612 ##      zeroLevel     --  if the correct answer is this close to zero,
  613 ##                        then zeroLevelTol applies
  614 ##      zeroLevelTol      --  absolute tolerance to allow when answer is close to zero
  615 ##      test_points     --  user supplied points to use for testing the
  616 ##                          function, either array of arrays, or optionally
  617 ##                          reference to single array (for one variable)
  618 
  619 
  620 sub FUNCTION_CMP {
  621   return ORIGINAL_FUNCTION_CMP(@_)
  622     if $useOldAnswerMacros;
  623 
  624   my %func_params = @_;
  625 
  626   my $correctEqn               = $func_params{'correctEqn'};
  627   my $var                      = $func_params{'var'};
  628   my $ra_limits                = $func_params{'limits'};
  629   my $tol                      = $func_params{'tolerance'};
  630   my $tolType                  = $func_params{'tolType'};
  631   my $numPoints                = $func_params{'numPoints'};
  632   my $mode                     = $func_params{'mode'};
  633   my $maxConstantOfIntegration = $func_params{'maxConstantOfIntegration'};
  634   my $zeroLevel                = $func_params{'zeroLevel'};
  635   my $zeroLevelTol             = $func_params{'zeroLevelTol'};
  636   my $testPoints               = $func_params{'test_points'};
  637 
  638   #
  639   #  Check that everything is defined:
  640   #
  641   $func_params{debug} = 0 unless defined $func_params{debug};
  642   $mode = 'std' unless defined $mode;
  643   my @VARS   = get_var_array($var);
  644   my @limits = get_limits_array($ra_limits);
  645   my @PARAMS = @{$func_params{'params'} || []};
  646 
  647   if ($tolType eq 'relative') {
  648     $tol = $functRelPercentTolDefault unless defined $tol;
  649     $tol *= .01;
  650   } else {
  651     $tol = $functAbsTolDefault unless defined $tol;
  652   }
  653 
  654   #
  655   #  Ensure that the number of limits matches number of variables
  656   #
  657   foreach my $i (0..scalar(@VARS)-1) {
  658     $limits[$i][0] = $functLLimitDefault unless defined $limits[$i][0];
  659     $limits[$i][1] = $functULimitDefault unless defined $limits[$i][1];
  660   }
  661 
  662   #
  663   #  Check that the test points are array references with the right number of coordinates
  664   #
  665   if ($testPoints) {
  666     my $n = scalar(@VARS); my $s = ($n != 1)? "s": "";
  667     foreach my $p (@{$testPoints}) {
  668       $p = [$p] unless ref($p) eq 'ARRAY';
  669       warn "Test point (".join(',',@{$p}).") should have $n coordiante$s"
  670         unless scalar(@{$p}) == $n;
  671     }
  672   }
  673 
  674   #
  675   #  Reorder variables, limits, and test_points if the variables are not in alphabetical order
  676   #
  677   if (scalar(@VARS) > 1 && join('',@VARS) ne join('',lex_sort(@VARS))) {
  678     my %order; foreach my $i (0..$#VARS) {$order{$VARS[$i]} = $i}
  679     @VARS = lex_sort(@VARS);
  680     @limits = map {$limits[$order{$_}]} @VARS;
  681     if ($testPoints) {foreach my $p (@{$testPoints}) {$p = [map {$p->[$order{$_}]} @VARS]}}
  682   }
  683 
  684   $numPoints                = $functNumOfPoints              unless defined $numPoints;
  685   $maxConstantOfIntegration = $functMaxConstantOfIntegration unless defined $maxConstantOfIntegration;
  686   $zeroLevel                = $functZeroLevelDefault         unless defined $zeroLevel;
  687   $zeroLevelTol             = $functZeroLevelTolDefault      unless defined $zeroLevelTol;
  688 
  689   $func_params{'var'}                      = \@VARS;
  690         $func_params{'params'}                   = \@PARAMS;
  691   $func_params{'limits'}                   = \@limits;
  692   $func_params{'tolerance'}                = $tol;
  693   $func_params{'tolType'}                  = $tolType;
  694   $func_params{'numPoints'}                = $numPoints;
  695   $func_params{'mode'}                     = $mode;
  696   $func_params{'maxConstantOfIntegration'} = $maxConstantOfIntegration;
  697   $func_params{'zeroLevel'}                = $zeroLevel;
  698   $func_params{'zeroLevelTol'}             = $zeroLevelTol;
  699 
  700   ########################################################
  701   #   End of cleanup of calling parameters
  702   ########################################################
  703 
  704         my %options = (
  705     debug => $func_params{'debug'},
  706           diagnostics => $func_params{'diagnostics'},
  707         );
  708 
  709   #
  710   #  Initialize the context for the formula
  711   #
  712   my $context = Parser::Context->getCopy($user_context,"LegacyNumeric");
  713   $context->flags->set(
  714     tolerance    => $func_params{'tolerance'},
  715     tolType      => $func_params{'tolType'},
  716     zeroLevel    => $func_params{'zeroLevel'},
  717     zeroLevelTol => $func_params{'zeroLevelTol'},
  718     num_points   => $func_params{'numPoints'},
  719   );
  720   if ($func_params{'mode'} eq 'antider') {
  721     $context->flags->set(max_adapt => $func_params{'maxConstantOfIntegration'});
  722     $options{upToConstant} = 1;
  723   }
  724 
  725   #
  726   #  Add the variables and parameters to the context
  727   #
  728   my %variables; my $x;
  729   foreach $x (@{$func_params{'var'}}) {
  730     if (length($x) > 1) {
  731       $context->{_variables}->{pattern} = $context->{_variables}->{namePattern} =
  732         $x . '|' . $context->{_variables}->{pattern};
  733       $context->update;
  734     }
  735     $variables{$x} = 'Real';
  736   }
  737   foreach $x (@{$func_params{'params'}}) {$variables{$x} = 'Parameter'}
  738   $context->variables->are(%variables);
  739 
  740   #
  741   #  Create the Formula object and get its answer checker
  742   #
  743   my $oldContext = &$Context(); &$Context($context);
  744   my $f = new Value::Formula($correctEqn);
  745   $f->{limits}      = $func_params{'limits'};
  746   $f->{test_points} = $func_params{'test_points'};
  747         $f->{correct_ans} = $correctEqn;
  748   my $cmp = $f->cmp(%options);
  749   &$Context($oldContext);
  750 
  751   #
  752   #  Get previous answer from hidden field of form
  753   #
  754   $cmp->install_pre_filter(
  755     sub {
  756       my $rh_ans = shift;
  757       $rh_ans->{_filter_name} = "fetch_previous_answer";
  758       my $prev_ans_label = "previous_".$rh_ans->{ans_label};
  759       $rh_ans->{prev_ans} =
  760         (defined $inputs_ref->{$prev_ans_label} and
  761          $inputs_ref->{$prev_ans_label} =~/\S/) ? $inputs_ref->{$prev_ans_label} : undef;
  762       $rh_ans;
  763     }
  764   );
  765 
  766   #
  767   #  Parse the previous answer, if any
  768   #
  769   $cmp->install_evaluator(
  770     sub {
  771       my $rh_ans = shift;
  772       $rh_ans->{_filter_name} = "parse_previous_answer";
  773       return $rh_ans unless defined $rh_ans->{prev_ans};
  774       my $oldContext = &$Context();
  775       &$Context($rh_ans->{correct_value}{context});
  776       $rh_ans->{prev_formula} = Parser::Formula($rh_ans->{prev_ans});
  777       &$Context($oldContext);
  778       $rh_ans;
  779     }
  780   );
  781 
  782   #
  783   #  Check if previous answer equals this current one
  784   #
  785   $cmp->install_evaluator(
  786     sub {
  787       my $rh_ans = shift;
  788       $rh_ans->{_filter_name} = "compare_to_previous_answer";
  789       return $rh_ans unless defined($rh_ans->{prev_formula}) && defined($rh_ans->{student_formula});
  790       $rh_ans->{prev_equals_current} =
  791         Value::cmp_compare($rh_ans->{student_formula},$rh_ans->{prev_formula},{});
  792       $rh_ans;
  793     }
  794   );
  795 
  796   #
  797   #  Show a message when the answer is equivalent to the previous answer.
  798   #
  799   #  We want to show the message when we're not in preview mode AND the
  800   #  answers are equivalent AND the answers are not identical. We DON'T CARE
  801   #  whether the answers are correct or not, because that leaks information in
  802   #  multipart questions when $showPartialCorrectAnswers is off.
  803   #
  804   $cmp->install_post_filter(
  805     sub {
  806       my $rh_ans = shift;
  807       $rh_ans->{_filter_name} = "produce_equivalence_message";
  808 
  809       return $rh_ans unless !$rh_ans->{isPreview} # not preview mode
  810         and $rh_ans->{prev_equals_current} # equivalent
  811         and $rh_ans->{prev_ans} ne $rh_ans->{original_student_ans}; # not identical
  812 
  813       $rh_ans->{ans_message} = "This answer is equivalent to the one you just submitted.";
  814       $rh_ans;
  815     }
  816   );
  817 
  818   return $cmp;
  819 }
  820 
  821 #
  822 #  The original version, for backward compatibility
  823 #  (can be removed when the Parser-based version is more fully tested.)
  824 #
  825 sub ORIGINAL_FUNCTION_CMP {
  826   my %func_params = @_;
  827 
  828   my $correctEqn               = $func_params{'correctEqn'};
  829   my $var                      = $func_params{'var'};
  830   my $ra_limits                = $func_params{'limits'};
  831   my $tol                      = $func_params{'tolerance'};
  832   my $tolType                  = $func_params{'tolType'};
  833   my $numPoints                = $func_params{'numPoints'};
  834   my $mode                     = $func_params{'mode'};
  835   my $maxConstantOfIntegration = $func_params{'maxConstantOfIntegration'};
  836   my $zeroLevel                = $func_params{'zeroLevel'};
  837   my $zeroLevelTol             = $func_params{'zeroLevelTol'};
  838   my $ra_test_points           = $func_params{'test_points'};
  839 
  840     # Check that everything is defined:
  841     $func_params{debug} = 0 unless defined $func_params{debug};
  842     $mode = 'std' unless defined $mode;
  843     my @VARS = get_var_array($var);
  844   my @limits = get_limits_array($ra_limits);
  845   my @PARAMS = ();
  846   @PARAMS = @{$func_params{'params'}} if defined $func_params{'params'};
  847 
  848   my @evaluation_points;
  849   if(defined $ra_test_points) {
  850     # see if this is the standard format
  851     if(ref $ra_test_points->[0] eq 'ARRAY') {
  852       $numPoints = scalar @{$ra_test_points->[0]};
  853       # now a little sanity check
  854       my $j;
  855       for $j (@{$ra_test_points}) {
  856         warn "Test points do not give the same number of values for each variable"
  857           unless(scalar(@{$j}) == $numPoints);
  858       }
  859       warn "Test points do not match the number of variables"
  860         unless scalar @{$ra_test_points} == scalar @VARS;
  861     } else { # we are got the one-variable format
  862       $ra_test_points = [$ra_test_points];
  863       $numPoints = scalar $ra_test_points->[0];
  864     }
  865     # The input format for test points is the transpose of what is used
  866     # internally below, so take care of that now.
  867     my ($j1, $j2);
  868     for ($j1 = 0; $j1 < scalar @{$ra_test_points}; $j1++) {
  869       for ($j2 = 0; $j2 < scalar @{$ra_test_points->[$j1]}; $j2++) {
  870         $evaluation_points[$j2][$j1] = $ra_test_points->[$j1][$j2];
  871       }
  872     }
  873   } # end of handling of user supplied evaluation points
  874 
  875   if ($mode eq 'antider') {
  876     # doctor the equation to allow addition of a constant
  877     my $CONSTANT_PARAM = 'Q'; # unfortunately parameters must be single letters.
  878                               # There is the possibility of conflict here.
  879                               #  'Q' seemed less dangerous than  'C'.
  880     $correctEqn = "( $correctEqn ) + $CONSTANT_PARAM";
  881     push @PARAMS, $CONSTANT_PARAM;
  882   }
  883     my $dim_of_param_space = @PARAMS;      # dimension of equivalence space
  884 
  885   if($tolType eq 'relative') {
  886     $tol = $functRelPercentTolDefault unless defined $tol;
  887     $tol *= .01;
  888   } else {
  889     $tol = $functAbsTolDefault unless defined $tol;
  890   }
  891 
  892   #loop ensures that number of limits matches number of variables
  893   for(my $i = 0; $i < scalar @VARS; $i++) {
  894     $limits[$i][0] = $functLLimitDefault unless defined $limits[$i][0];
  895     $limits[$i][1] = $functULimitDefault unless defined $limits[$i][1];
  896   }
  897   $numPoints                = $functNumOfPoints              unless defined $numPoints;
  898   $maxConstantOfIntegration = $functMaxConstantOfIntegration unless defined $maxConstantOfIntegration;
  899   $zeroLevel                = $functZeroLevelDefault         unless defined $zeroLevel;
  900   $zeroLevelTol             = $functZeroLevelTolDefault      unless defined $zeroLevelTol;
  901 
  902   $func_params{'var'}                      = $var;
  903   $func_params{'limits'}                   = \@limits;
  904   $func_params{'tolerance'}                = $tol;
  905   $func_params{'tolType'}                  = $tolType;
  906   $func_params{'numPoints'}                = $numPoints;
  907   $func_params{'mode'}                     = $mode;
  908   $func_params{'maxConstantOfIntegration'} = $maxConstantOfIntegration;
  909   $func_params{'zeroLevel'}                = $zeroLevel;
  910   $func_params{'zeroLevelTol'}             = $zeroLevelTol;
  911 
  912   ########################################################
  913   #   End of cleanup of calling parameters
  914   ########################################################
  915 
  916   my $i; # for use with loops
  917   my $PGanswerMessage = "";
  918   my $originalCorrEqn = $correctEqn;
  919 
  920   ######################################################################
  921   # prepare the correct answer and check its syntax
  922   ######################################################################
  923 
  924     my $rh_correct_ans = new AnswerHash;
  925   $rh_correct_ans->input($correctEqn);
  926   $rh_correct_ans = check_syntax($rh_correct_ans);
  927   warn  $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag};
  928   $rh_correct_ans->clear_error();
  929   $rh_correct_ans = function_from_string2($rh_correct_ans,
  930     ra_vars => [ @VARS, @PARAMS ],
  931     stdout  => 'rf_correct_ans',
  932     debug   => $func_params{debug}
  933   );
  934   my $correct_eqn_sub = $rh_correct_ans->{rf_correct_ans};
  935   warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag};
  936 
  937   ######################################################################
  938   # define the points at which the functions are to be evaluated
  939   ######################################################################
  940 
  941   if(not defined $ra_test_points) {
  942     #create the evaluation points
  943     my $random_for_answers = new PGrandom($main::PG_original_problemSeed);
  944     my $NUMBER_OF_STEPS_IN_RANDOM = 1000; # determines the granularity of the random_for_answers number generator
  945     for(my $count = 0; $count < @PARAMS+1+$numPoints; $count++) {
  946         my (@vars,$iteration_limit);
  947       for(my $i = 0; $i < @VARS; $i++) {
  948         my $iteration_limit = 10;
  949         while (0 < --$iteration_limit) {  # make sure that the endpoints of the interval are not included
  950             $vars[$i] = $random_for_answers->random($limits[$i][0], $limits[$i][1], abs($limits[$i][1] - $limits[$i][0])/$NUMBER_OF_STEPS_IN_RANDOM);
  951             last if $vars[$i]!=$limits[$i][0] and $vars[$i]!=$limits[$i][1];
  952         }
  953         warn "Unable to properly choose  evaluation points for this function in the interval ( $limits[$i][0] , $limits[$i][1] )"
  954           if $iteration_limit == 0;
  955       }
  956 
  957       push @evaluation_points, \@vars;
  958     }
  959   }
  960   my $evaluation_points = Matrix->new_from_array_ref(\@evaluation_points);
  961 
  962   #my $COEFFS = determine_param_coeffs($correct_eqn_sub,$evaluation_points[0],$numOfParameters);
  963   #warn "coeff", join(" | ", @{$COEFFS});
  964 
  965   #construct the answer evaluator
  966     my $answer_evaluator = new AnswerEvaluator;
  967     $answer_evaluator->{debug} = $func_params{debug};
  968     $answer_evaluator->ans_hash(
  969     correct_ans       => $originalCorrEqn,
  970     rf_correct_ans    => $rh_correct_ans->{rf_correct_ans},
  971     evaluation_points => \@evaluation_points,
  972     ra_param_vars     => \@PARAMS,
  973     ra_vars           => \@VARS,
  974     type              => 'function',
  975     score             => 0,
  976     );
  977 
  978     #########################################################
  979     # Prepare the previous answer for evaluation, discard errors
  980     #########################################################
  981 
  982   $answer_evaluator->install_pre_filter(
  983     sub {
  984       my $rh_ans = shift;
  985       $rh_ans->{_filter_name} = "fetch_previous_answer";
  986       my $prev_ans_label = "previous_".$rh_ans->{ans_label};
  987       $rh_ans->{prev_ans} = (defined $inputs_ref->{$prev_ans_label} and $inputs_ref->{$prev_ans_label} =~/\S/)
  988         ? $inputs_ref->{$prev_ans_label}
  989         : undef;
  990       $rh_ans;
  991     }
  992   );
  993 
  994   $answer_evaluator->install_pre_filter(
  995     sub {
  996       my $rh_ans = shift;
  997       return $rh_ans unless defined $rh_ans->{prev_ans};
  998       check_syntax($rh_ans,
  999         stdin          => 'prev_ans',
 1000         stdout         => 'prev_ans',
 1001         error_msg_flag => 0
 1002       );
 1003       $rh_ans->{_filter_name} = "check_syntax_of_previous_answer";
 1004       $rh_ans;
 1005     }
 1006   );
 1007 
 1008   $answer_evaluator->install_pre_filter(
 1009     sub {
 1010       my $rh_ans = shift;
 1011       return $rh_ans unless defined $rh_ans->{prev_ans};
 1012       function_from_string2($rh_ans,
 1013         stdin   => 'prev_ans',
 1014         stdout  => 'rf_prev_ans',
 1015         ra_vars => \@VARS,
 1016         debug   => $func_params{debug}
 1017       );
 1018       $rh_ans->{_filter_name} = "compile_previous_answer";
 1019       $rh_ans;
 1020     }
 1021   );
 1022 
 1023     #########################################################
 1024     # Prepare the current answer for evaluation
 1025     #########################################################
 1026 
 1027   $answer_evaluator->install_pre_filter(\&check_syntax);
 1028   $answer_evaluator->install_pre_filter(\&function_from_string2,
 1029     ra_vars => \@VARS,
 1030     debug   => $func_params{debug}
 1031     ); # @VARS has been guaranteed to be an array, $var might be a single string.
 1032 
 1033     #########################################################
 1034     # Compare the previous and current answer.  Discard errors
 1035     #########################################################
 1036 
 1037   $answer_evaluator->install_evaluator(
 1038     sub {
 1039       my $rh_ans = shift;
 1040       return $rh_ans unless defined $rh_ans->{rf_prev_ans};
 1041       calculate_difference_vector($rh_ans,
 1042         %func_params,
 1043         stdin1         => 'rf_student_ans',
 1044         stdin2         => 'rf_prev_ans',
 1045         stdout         => 'ra_diff_with_prev_ans',
 1046         error_msg_flag => 0,
 1047       );
 1048       $rh_ans->{_filter_name} = "calculate_difference_vector_of_previous_answer";
 1049       $rh_ans;
 1050     }
 1051   );
 1052 
 1053   $answer_evaluator->install_evaluator(
 1054     sub {
 1055       my $rh_ans = shift;
 1056       return $rh_ans unless defined $rh_ans->{ra_diff_with_prev_ans};
 1057       ##
 1058       ## DPVC -- only give the message if the answer is specified differently
 1059       ##
 1060       return $rh_ans if $rh_ans->{prev_ans} eq $rh_ans->{student_ans};
 1061       ##
 1062       ## /DPVC
 1063       ##
 1064       is_zero_array($rh_ans,
 1065         stdin  => 'ra_diff_with_prev_ans',
 1066         stdout => 'ans_equals_prev_ans'
 1067       );
 1068     }
 1069   );
 1070 
 1071     #########################################################
 1072     # Calculate values for approximation parameters and
 1073     # compare the current answer with the correct answer.  Keep errors this time.
 1074     #########################################################
 1075 
 1076     $answer_evaluator->install_pre_filter(\&best_approx_parameters, %func_params, param_vars => \@PARAMS);
 1077     $answer_evaluator->install_evaluator(\&calculate_difference_vector, %func_params);
 1078     $answer_evaluator->install_evaluator(\&is_zero_array, tolerance => $tol );
 1079 
 1080     $answer_evaluator->install_post_filter(
 1081       sub {
 1082         my $rh_ans = shift;
 1083         $rh_ans->clear_error('SYNTAX');
 1084         $rh_ans;
 1085       }
 1086     );
 1087 
 1088   $answer_evaluator->install_post_filter(
 1089     sub {
 1090       my $rh_ans = shift;
 1091       if ($rh_ans->catch_error('EVAL')) {
 1092         $rh_ans->{ans_message} = $rh_ans->{error_message};
 1093         $rh_ans->clear_error('EVAL');
 1094       }
 1095       $rh_ans;
 1096     }
 1097   );
 1098 
 1099   #
 1100   #  Show a message when the answer is equivalent to the previous answer.
 1101   #
 1102   #  We want to show the message when we're not in preview mode AND the
 1103   #  answers are equivalent AND the answers are not identical. We DON'T CARE
 1104   #  whether the answers are correct or not, because that leaks information in
 1105   #  multipart questions when $showPartialCorrectAnswers is off.
 1106   #
 1107   $answer_evaluator->install_post_filter(
 1108     sub {
 1109       my $rh_ans = shift;
 1110 
 1111       my $isPreview = $inputs_ref->{previewAnswers} || ($inputs_ref->{action} =~ m/^Preview/);
 1112       return $rh_ans unless !$isPreview # not preview mode
 1113         and $rh_ans->{ans_equals_prev_ans} # equivalent
 1114         and $rh_ans->{prev_ans} ne $rh_ans->{original_student_ans}; # not identical
 1115 
 1116       $rh_ans->{ans_message} = "This answer is equivalent to the one you just submitted.";
 1117       return $rh_ans;
 1118     }
 1119   );
 1120 
 1121   $answer_evaluator;
 1122 }
 1123 
 1124 =head1 SEE ALSO
 1125 
 1126 L<PGanswermacros.pl>, L<MathObjects>.
 1127 
 1128 =cut
 1129 
 1130 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9