WeBWorK Problems

combining parserMultiAnswer and LABELED_ANS

combining parserMultiAnswer and LABELED_ANS

by Andrew Parker -
Number of replies: 6
I have two student entries that I want to compare in a single evaluator. However, one of the entries is coming from a GeoGebra applet, and is therefore a named answer. As such, I've labeled the text entry box as well, so both answers are named: 'ggbEntry' and 'textEntry'

The parserMultiAnswer.pl documentation suggests that I can use LABELED_ANS (NAMED_ANS) to evaluate against a multi answer object by specifying "$multiAnsObject->with(namedRules=>1)".

However, I am now stuck on the context of calling LABELED_ANS as it seems to expect an evaluator for each named answer.

LABELED_ANS( ?'ggbEntry' & 'textEntry'? , $multiAnsObject->cmp() );

Is there a way to combine both entries to pair with a single multiAnswer comparison?
In reply to Andrew Parker

Re: combining parserMultiAnswer and LABELED_ANS

by Andrew Parker -
I can separately evaluate both 'ggbEntry' and 'textEntry' without error using LABELED_ANS:

LABELED_ANS( 'ggbEntry' => $answer1->cmp(), 'textEntry' => $answer2->cmp() );


However, when constructing a multiAnsObject:

$multians = MultiAnswer($answer1, $answer2)->with(
singleResult => 1,
namedRules => 1,
checker => sub {
my ( $correct, $student, $self ) = @_;
my ( $ggbStu, $eqnStu ) = @{$student};
return 1;
}
);

I get the following errors when attempting to pass multiple named_ans_rules:

LABELED_ANS( 'ggbEntry' => $multians->cmp(), 'textEntry' => $multians->cmp() );

Error in LABELED_ANS:|ggbEntry| -- inputs must be references to AnswerEvaluator objects or subroutines
Error in LABELED_ANS:|AnswerEvaluator=HASH(0x7fa1bdacb1b8)| -- inputs must be references to AnswerEvaluator objects or subroutines
Error in Translator.pm::process_answers: Answer ggbEntry: Unrecognized evaluator type ||

No answer evaluator for the question labeled: textEntry

No answer evaluator for the question labeled: AnSwEr0001

No answer blank provided for answer evaluator AnSwEr0001

Error in Translator.pm::process_answers: Answer AnswerEvaluator=HASH(0x7fa1bdacb1b8): Unrecognized evaluator type ||

No answer blank provided for answer evaluator



And if I attempt it this way - I get fewer errors...

LABELED_ANS( ('ggbEntry', 'textEntry') => $multians->cmp() );
Errors:
Error in LABELED_ANS:|ggbEntry| -- inputs must be references to AnswerEvaluator objects or subroutines

Error in Translator.pm::process_answers: Answer ggbEntry: Unrecognized evaluator type ||

No answer evaluator for the question labeled: textEntry

No answer blank provided for answer evaluator AnSwEr0001

* there are no unlabeled answer blanks, so there should be no AnSwEr0001 in either case.


The following excerpt from parserMultiAnswer.pl seems to suggest that this is totally possible - but I cannot find any examples of the syntax used when calling NAMED_ANS:
 namedRules => 0 or 1 whether to use named rules or default rule names. Use named rules if you need to intersperse other rules with the ones for the MultiAnswer, in which case you must use NAMED_ANS not ANS. (Default: 0)


In reply to Andrew Parker

Re: combining parserMultiAnswer and LABELED_ANS

by Andrew Parker -
Stripped-down sample code that mirrors the errors I'm receiving:

DOCUMENT();

loadMacros(
"PGstandard.pl", # Standard macros for PG language
"MathObjects.pl",
"parserMultiAnswer.pl",
);

# Print problem number and point value (weight) for the problem
TEXT(beginproblem());

# Show which answers are correct and which ones are incorrect
$showPartialCorrectAnswers = 1;

##############################################################
# Setup

Context("Numeric");
Context()->variables->are(x=>"Real", y=>"Real");

$a = Real(non_zero_random(-5,5,1));

$multians = MultiAnswer( $a, $a ) -> with(
singleResult => 1,
namedRules => 1,
checker => sub {
 my ($correct,$student,$self) = @_; # get the parameters
 my ($one,$two) = @{$student}; # extract the student answers
return ($one == $two);
},
);



##############################################################
# Text

Context()->texStrings;
BEGIN_TEXT

Enter the same number in each box:

\{ named_ans_rule( 'one', 6 ) \} = \{ named_ans_rule( 'two', 6 ) \}

END_TEXT
Context()->normalStrings;

##############################################################
# Answers

NAMED_ANS( ('one','two') => $multians->cmp() );


ENDDOCUMENT();

In reply to Andrew Parker

Re: combining parserMultiAnswer and LABELED_ANS

by Davide Cervone -
The MultiAnswer object does not work the way you are hoping it does. You can't attach it to existing answer blanks -- it has to create its own. That goes for named rules as well as standard ones; the named ones are only for the situation where you need to have the MultiAnswer rules be intermixed with other rules (so that they aren't all consecutive rules), but they are still created by using the MultiAnswer's ans_rule() method, not by calling NAMED_ANS_RULE() on your own.

It is possible to do what you want, but you would have to make a subclass of the MultiAnswer object and override its internal ANS_NAME() method that gets the names of the answer rules to use and have it return the pre-defined answer rules that you want to use. Here is an example:

loadMacros("parserMultiAnswer.pl");


#
#  Make a subclass of MultiAnswer
#
package myMultiAnswer;
our @ISA = ('MultiAnswer');

#
#  Override the ANS_NAME method to return the names
#  of the named answer rules that we create separately
#
sub ANS_NAME {
  my $self = shift;
  my $i = shift;
  return ('one', 'two')[$i];
}

package main;

#
#  Create a new instance of our subclass
#
$ma = myMultiAnswer->new(1,2)->with(
  namedRules => 1,
  checker => sub {
    my ($correct,$student) = @_;
    my ($ca,$cb) = @$correct;
    my ($sa,$sb) = @$student;
    return ($ca == $sa ? 1 : 0, $cb == $sb ? 1 : 0);
  }
);

#
#  Create the named rules
#
BEGIN_TEXT
\{NAMED_ANS_RULE('one',5)\} and \{NAMED_ANS_RULE('two',5)\}
END_TEXT

#
#  Insert the answer checkers (MultiAnswer inserts the names
#  and answer checkers for both rules automatically)
#
NAMED_ANS($ma->cmp);
Some things to note:
  • You can't use singleResult => 1 with this approach, because that requires one named answer rule and the rest as answer rule extensions. (If you are creating the second and subsequent answer rules yourself, you could potentially use singleResult, but if the other named rules are created by other code you don't control, then no.)

The macro for creating named answer rules is in upper case, not lower case (NAMED_ANS_RULE() not named_ans_rule()). I'm not sure why that is, and perhaps there are other ways to do it.

The $ma->cmp call actually includes the names and answer checkers needed by NAMED_ANS(), so you don't enter those yourself. (The result of $ma->cmp is a list consisting of one => AnswerEvaluator, two => AnswerEvaluator, which is just what NAMED_ANS() needs.)

Hope that does what you need.

In reply to Davide Cervone

Re: combining parserMultiAnswer and LABELED_ANS

by Andrew Parker -
Thanks so much Davide!

I've been wrestling with this for a few days now - and your answer makes perfect sense. After digging through parserMultiAnswer.pl and PG.pl, I just didn't see a way to brute force my answer names into the checker. It didn't occur to me to subclass MultiAnswer with hardcoded names.

Just to clarify - if I hard-code one answer name (ggbEntry, out of my control) within the subclass, and then I call the ans_rule method on my $multiAnsObj for an additional answer (textEntry, which I only named because I figured that namedRules would require all ans_rules to be named), the $multiAnsObj checker will automatically append the AnSwEr0001 as an answer rule extension and I can use the singleResult option? Would the modified ANS_NAME method above need to be modified in order to allow for the combination of hard-coded and appended extensions?


In reply to Andrew Parker

Re: combining parserMultiAnswer and LABELED_ANS

by Davide Cervone -
It turns out the you can do this. The first answer in the MultiAnswer list must be the one from GeoGebra (named ggbEntry here), since it is the named answer rule, and the others will be formed as extension rules by MultiAnswer. To make things a little easier, I override the new() method as well, in order to set some needed values, as described below the code.
loadMacros("parserMultiAnswer.pl");

package myMultiAnswer;
our @ISA = ('MultiAnswer');

sub new {
  my $self = shift;
  my $ma = $self->SUPER::new(@_);
  $ma->{part} = 1;
  $ma->{answerName} = 'ggbEntry';
  $ma->{id} = $MultiAnswer::answerPrefix.$ma->{answerName};
  $ma->{singleResult} = 1;
  $ma->{namedRules}  = 1;
  return $ma;
}

sub ANS_NAME {
  my $self = shift;
  my $i = shift;
  return ($i == 0 ? $self->{answerName} : $self->{id}.'_'.$i);
}

package main;

$ma = myMultiAnswer->new(1,2)->with(
  checker => sub {
    my ($correct,$student) = @_;
    my ($ca,$cb) = @$correct;
    my ($sa,$sb) = @$student;
    return ($ca == $sa ? 1 : 0, $cb == $sb ? 1 : 0);
  }
);

BEGIN_TEXT
   \{ NAMED_ANS_RULE('ggbEntry',5) \} and  \{ $ma->ans_rule(5) \}
END_TEXT

NAMED_ANS($ma->cmp);
The new() method sets the part to 1, since this is counting the answer blanks, and you are making the first one by hand. It also sets answerName, which is the answer name of the main answer rule (and is used to tie the extensions to the main rule). The id is used to create names for the extensions. Finally, I also set singleResults and namedRules so you don't have to do that by hand later.

The ANS_NAME() method returns the answerName for the first name and the constructed extension name for all the others. That should do it.

Since you are using named answer rules, technically the GeoGebra one could be in any position relative to the others. But note that the order in which they are displayed in the answer table when a student submits the answers will be the GeoGebra one first then the others. If the GeoGebra rule is in a different order, you can use the MultiAnswer format and tex_format options to display the results in the same order as the rules (or in whatever oder makes sense). For example, the following puts the GeoGebra blank last, and reorders the values in the format parameters:

loadMacros("parserMultiAnswer.pl");

package myMultiAnswer;
our @ISA = ('MultiAnswer');

sub new {
  my $self = shift;
  my $ma = $self->SUPER::new(@_);
  $ma->{part} = 1;
  $ma->{answerName} = 'ggbEntry';
  $ma->{id} = $MultiAnswer::answerPrefix.$ma->{answerName};
  $ma->{singleResult} = 1;
  $ma->{namedRules}  = 1;
  return $ma;
}

sub ANS_NAME {
  my $self = shift;
  my $i = shift;
  return ($i == 0 ? $self->{answerName} : $self->{id}.'_'.$i);
}

package main;

$ma = myMultiAnswer->new(1,2)->with(
  format => '%2$s and %1$s',
  tex_format => '%2$s \hbox{ and } %1$s',
  checker => sub {
    my ($correct,$student) = @_;
    my ($ca,$cb) = @$correct;
    my ($sa,$sb) = @$student;
    return ($ca == $sa ? 1 : 0, $cb == $sb ? 1 : 0);
  }
);

BEGIN_TEXT
  \{ $ma->ans_rule(5) \} and \{ NAMED_ANS_RULE('ggbEntry',5) \}
END_TEXT

NAMED_ANS($ma->cmp);
The %2$s in the format means insert the second value here as a string (2$ specifies second value, while %...s means as a string). Similarly, %1$s means insert the first value as a string. So the output is printed in the same order as the answer rules, as "2 and 1" even though the MultiAnswer has them in the order 1 then 2 when it is created.

Hope the makes sense.

In reply to Davide Cervone

Re: combining parserMultiAnswer and LABELED_ANS

by Andrew Parker -
This is perfect. I crafted a little workaround to hide the ggbEntry (as the whole point is to work it out for oneself) using the following in the checker:

$self->{ans}[0]->{preview_text_string} = "hidden";
$self->{ans}[0]->{preview_latex_string} = "hidden";

But now, with the singleResult=>1, I can set the format to not even display either %1 or %2, however the situation requires.

You've been a massive help, thanks again!