[system] / trunk / pg / macros / extraAnswerEvaluators.pl Repository: Repository Listing bbplugincoursesdistsnplrochestersystemwww

Sun Aug 19 00:56:25 2007 UTC (12 years, 6 months ago) by dpvc
File size: 14818 byte(s)
```Changed Parser.pl to MathObjects.pl
```

```    1 loadMacros('MathObjects.pl');
2
4
6
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
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
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
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   );
339   if (\$opts{strings}) {
340     foreach my \$string (@{\$opts{strings}}) {
341       \$string = uc(\$string);
343         defined(\$context->strings->get(\$string));
344       \$ans_type = 'String' if \$string eq uc(\$correct_ans);
345     }
346   }
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}}) {
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
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
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);
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
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
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
```