WeBWorK Problems

Controlling Simplifying in a Function

Controlling Simplifying in a Function

by Gregory Varner -
Number of replies: 12

I am wanting to force students to simplify an expression like log_2(4) to be 2. Otherwise I have been able to force students to fully expand the logarithm. I feel like it should be a fairly easy fix (and I may have overlooked it), but I have been unable thus far to figure it out.


Here is what I have:

########################################################################


DOCUMENT();      


loadMacros(

   "PGstandard.pl",     # Standard macros for PG language

   "MathObjects.pl",

   "PGML.pl",

  "answerHints.pl",

  "PGML.pl",

  "PGcourse.pl",

  "parserFunction.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")->flags->set(reduceConstantFunctions=>1);

Parser::Number::NoDecimals();


##y is ln(base1), z is ln(base2)

Context()->variables->add(x=>'Real',y=>'Real',z=>'Real');

Context()->variables->set(x=>{limits=>[2,3]},y=>{limits=>[2,3]},z=>{limits=>[2,3]});


parserFunction("log2(x)" => "log(x)/log(2)");


$power1 = 2;

$power2 = 3;

$power3 = 1;

$coef = 4;


$base = Formula("log2((x**$power1 y**$power2)/($coef*z**$power3))")->reduce;


#$base2 = $base**$power3;


$ans = Formula("$power1 * log2(x) + $power2 * log2(y) - log($coef)/log(2) - $power3*log2(z)")->reduce;


Context()->operators->undefine("/","^","**");


ANS($ans->cmp());

##############################################################

#

#  Text

#

#


BEGIN_PGML


Expand the logarithm.


>> [`` [$base] ``] <<


The expanded form is  [_______________]


Your answer should use [`` \log_2 (x) ``] but you need to enter it as [`` log2(x) ``].


END_PGML


##############################################################

#



ENDDOCUMENT();        

In reply to Gregory Varner

Re: Controlling Simplifying in a Function

by Alex Jordan -

Since WeBWorK is not a CAS, it has no special knowledge about the log2() function that would automatically simplify something like log2(8) to 3. You would like to distinguish "log2(3) + 2" from "log2(3) + log2(4)". But again, to do that _honestly_ would require some CAS abilities that WeBWorK does not have.

However you could do it "dishonestly" by changing the meaning of log2(). So if you, the problem author, arrange for the answer to be Formula("log2(3) + 2") and redefine log2() to be some other function so that log2(4) is not actually 2, then that meets your objective.

You would want the context flag reduceConstantFunctions to be 0, or else log2(3) would be displayed as some decimal.

It would be ideal to do this in a custom answer checker where you first establish in the student answer is correct under the regular meaning of log2(), then change its meaning and check again. This way you could have feedback messages like "your answer is correct but not in the expected fully simplified form".


In reply to Alex Jordan

Re: Controlling Simplifying in a Function

by Gregory Varner -

That is disappointing, I was hoping that an option similar to what they have for controlling simplifying in polynomials would work.

I have not done much with custom checkers, but I think that I understand the basics.

I am not sure how I would go about changing the meaning of the function inside the checker, though I am still looking into this.


In the meantime, any help would be appreciated.

Thank you!

In reply to Gregory Varner

Re: Controlling Simplifying in a Function

by Alex Jordan -
I could always be mistaken and someone will have a better idea. With polynomials, I am not aware of something that automates simplifying "x+x" to "2x". Simplifications exist that do arithmetic on plain numbers, so "(1+2)x^(3+4)" will become "3x^7". And also that turn "1x" into "x" and similar things. And there is a context that can recognize if a power is used more than once and reject "x^2+2x^2" but it still does not simplify that to "3x^2".

Here is something that appears to work, with limited testing:

DOCUMENT();     

loadMacros(
   "PGstandard.pl",
   "MathObjects.pl",
   "PGML.pl",
   "PGcourse.pl",
);

TEXT(beginproblem());

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

Context("Numeric")->flags->set(
    reduceConstantFunctions=>0,
    formatStudentAnswer=>'parsed',
);

package log2;
our @ISA = qw(Parser::Function::numeric);
sub log2 {
  shift; my $x = shift;
  return CORE::log($x)/CORE::log(2);
}

package decoy;
our @ISA = qw(Parser::Function::numeric);
sub log2 {
  shift; my $x = shift;
  return CORE::sin($x)+pi**$x;
}

package main;

# Make it work on formulas as well as numbers
sub log2 {Parser::Function->call('log2',@_)}

#  Add the new function to the Context
Context()->functions->add(
  log2 => {class => 'log2',
           TeX => '\log_2'}, );

# Note the "2" and the "3" need to be that way, not "log2(4)" and "log2(8)"
$answer = Formula("(log2(3)+2)/(log2(5)+3)");

$answercmp = $answer->cmp(
    checker => sub {
       my ($correct,$student,$ansHash) = @_;
       # make sure $studnet is a Formula, not immediately converted to Real
       $student = Formula($ansHash->{student_formula});
       return 0 if ($correct != $student);
       delete $correct->{test_values}, $student->{test_values};
       # switch to decoy
       Context()->functions->set(
           log2 => {class => 'decoy',
                TeX => '\log_2'}
       );
       if ($correct == $student) {
           # clean up, then award credit
           delete $correct->{test_values}, $student->{test_values};
           Context()->functions->set(
               log2 => {class => 'log2',
                    TeX => '\log_2'}
           );
           return 1;
       } else {
           Value::Error("Your answer is equivalent to the correct answer,
                         but not in the expected simplified form.");
       }
    }
);

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

BEGIN_PGML

Enter [``[$answer]``]. Your answer should use [` \log_2(x) `] but you need to enter it as [|log2(x)|]*.

[_]{$answercmp}{40}

END_PGML

ENDDOCUMENT(); 

In reply to Alex Jordan

Re: Controlling Simplifying in a Function

by Gregory Varner -

Thank you very much.

That is working much better than what I was putting together.

I did have a question about the set-up that you used, just to be sure that I understand it well. Did you use the package method for defining the functions to ease switching between the versions or was there a bigger advantage?

In other words, is there a larger advantage that I am missing that is gained by the package approach versus defining two different functions (as I had previously been defining functions) and switching to the decoy function?

In reply to Gregory Varner

Re: Controlling Simplifying in a Function

by Alex Jordan -

I used that method instead of parserFunction because I couldn't see a way to make the log2 function print as `\log_2` in math output. But making `log2` be a regular function in the context, you can see how to control its TeX output.

In reply to Alex Jordan

Re: Controlling Simplifying in a Function

by Gregory Varner -

That makes sense. I was struggling with that bit myself.

Do you know if the custom checker would work for redefining a function that Webwork already has built in. For instance, to force a student to reduce ln(e) also?

For instance, I could define a decoy still (not needing to define another function)

************************************************************************

Context("Numeric")->flags->set(

    reduceConstantFunctions=>0,

    formatStudentAnswer=>'parsed',

);

package decoy;

our @ISA = qw(Parser::Function::numeric);

sub decoy {

  shift; my $x = shift;

  return CORE::sin($x)+pi**$x;

}

package main;

# Make it work on formulas as well as numbers

sub decoy {Parser::Function->call('decoy',@_)}

*********************************************************************************

Then the custom checker would need to take ln and redefine it as decoy.

Context()->functions->set(

           ln => {class => 'decoy',

                TeX => '\ln'}


Sorry for all of the questions. Custom checkers are new to me.

Thanks again. You have been a life saver.

In reply to Gregory Varner

Re: Controlling Simplifying in a Function

by Alex Jordan -

You're welcome, always happy to pay it forward on this forum for help in the past.

I think what you outline would work fine, but your code is not right. You don't want to make a "decoy" subroutine; you would make a "sin" subroutine in the "decoy" package. (Side note: "decoy" is maybe not the most apt name, but it's what I wrote quickly.) And so then you would not declare a "decoy" subroutine in main either. You'd only need to set the class of "sin" to "decoy" in the middle of the custom answer checker. And it is best practice to set it back (to "numeric") at the end of the answer checker, to keep the effects contained.

I'd be curious if anyone else following the thread has better or alternative ideas.

And if you are going to do this for a fair number of problems, I recommend finding a way to put it all into a macro library. There could be a context for all of this, with alternate "bizarro" versions of functions, and in that context the Formula answer checker always does the switch. It would still be up to the author to supply answers like "2" and not "ln(e^2)" though.

NB: we have bizarroArithmetic.pl that is this same idea, except it only applies to the five arithmetic operators (plus optionally sqrt and nth root). It's a bit more complicated than these log functions since it meddles with more fundamental things like what "+" means. But the core idea is a workaround for WW not having more advanced CAS properties and it's stood up over time.

In reply to Alex Jordan

Re: Controlling Simplifying in a Function

by Gregory Varner -

I must be incredibly dense. I am not getting your suggestion to work.

I tried defining the sin subroutine of the decoy package. I then attempted to set the class of sin to decoy, but I am having no luck in doing so. My problem runs fine, but it must not be switching over correct as it counts ln(e) correct, which is shouldn't.

Perhaps I am misunderstanding how to switch the class of sin to decoy. 

Any help is of course appreciated.

Thank you

In reply to Gregory Varner

Re: Controlling Simplifying in a Function

by Alex Jordan -

Hi Gregory,

Could you post a small example? It could be something subtle and if I can see your whole problem file, it would be good to know about whatever that subtlety is.

Alex

In reply to Alex Jordan

Re: Controlling Simplifying in a Function

by Gregory Varner -

I am sure that is me not understanding something about the way to define the package and switch them.

Trying to remove defining it inside of main kept giving me point evaluation errors.

I have tried a few different things under the #switch to decoy# and none of them seem to be working.


I am guessing that it is my unfamiliarity with defining packages and working within them and it is probably something quite simple.


DOCUMENT();      


loadMacros(

   "PGstandard.pl",     # Standard macros for PG language

   "MathObjects.pl",

   "PGML.pl",

  "answerHints.pl",

  "PGML.pl",

  "PGcourse.pl",

  "parserFunction.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")->flags->set(

    reduceConstantFunctions=>0,

    formatStudentAnswer=>'parsed',

);


package decoy;

our @ISA = qw(Parser::Function::numeric);

sub decoy {

  shift; my $x = shift;

  return CORE::sin($x)+pi**$x;

}


package main;

# Make it work on formulas as well as numbers

sub decoy {Parser::Function->call('decoy',@_)}


##y is ln(base1), z is ln(base2)

Context()->variables->add(x=>'Real',y=>'Real',z=>'Real');

Context()->variables->set(x=>{limits=>[2,3]},y=>{limits=>[2,3]},z=>{limits=>[2,3]});

$power1 = 3;

$power2 = -2;

$power3 = 3;

$power4a = 5;

$power4b = 2;

$power5 = 4;

$answer = Formula("$power5*$power1 * ln(x) + $power5*$power2 * ln(y) - $power5*$power3 - $power5*($power4a/$power4b)*ln(z)")->reduce;

Context()->operators->undefine("/","^","**");

$answercmp = $answer->cmp(

    checker => sub {

       my ($correct,$student,$ansHash) = @_;

       # make sure $student is a Formula, not immediately converted to Real

       $student = Formula($ansHash->{student_formula});

       return 0 if ($correct != $student);

       delete $correct->{test_values}, $student->{test_values};

       # switch to decoy

       Context()->functions->set(

           ln => {class => 'decoy',

                TeX => '\ln'}

       );

       if ($correct == $student) {

           # clean up, then award credit

           delete $correct->{test_values}, $student->{test_values};

           Context()->functions->set(

               ln => {class => 'ln',

                    TeX => '\ln'}

           );

           return 1;

       } else {

           Value::Error("Your answer is equivalent to the correct answer,

                         but not in the expected simplified form.");

       }

    }

);

##############################################################

#

#  Text

#

#

BEGIN_PGML

Expand the logarithm.

>> [`` \ln\left(\frac{x^{[$power1]}y^{[$power2]}}{e^{[$power3]}z^{[$power4a]/[$power4b]}}\right)^{[$power5]} ``] <<


The expanded form is  [_]{$answercmp}{40}

END_PGML


##############################################################

#



ENDDOCUMENT();        

In reply to Gregory Varner

Re: Controlling Simplifying in a Function

by Alex Jordan -
Hi Gregory,

This still has the same issues I mentioned in an earlier post:

> I think what you outline would work fine, but your code is not right. You don't want to make a "decoy" subroutine; you would make a "sin" subroutine in the "decoy" package. (Side note: "decoy" is maybe not the most apt name, but it's what I wrote quickly.) And so then you would not declare a "decoy" subroutine in main either. You'd only need to set the class of "sin" to "decoy" in the middle of the custom answer checker. And it is best practice to set it back (to "numeric") at the end of the answer checker, to keep the effects contained.

Except it looks like I was confused for a bit there about whether this was to rewire "ln" or "sin".

Change:
package decoy;
our @ISA = qw(Parser::Function::numeric);
sub decoy {
shift; my $x = shift;
return CORE::sin($x)+pi**$x;
}

To:
package decoy;
our @ISA = qw(Parser::Function::numeric);
sub ln {
shift; my $x = shift;
return CORE::sin($x)+pi**$x;
}

And remove entirely:
# Make it work on formulas as well as numbers
sub decoy {Parser::Function->call('decoy',@_)}

In the answer checker, change:
Context()->functions->set(
ln => {class => 'ln',
TeX => '\ln'}
);

to:
Context()->functions->set(
ln => {class => 'Parser::Function::numeric',
TeX => '\ln'}
);

Separately, when I tested just now, there is an error/warning from this line:
Context()->variables->add(x=>'Real',y=>'Real',z=>'Real');
which should be:
Context()->variables->are(x=>'Real',y=>'Real',z=>'Real');
since 'x' is already a variable. Or you could just do:
Context()->variables->add(y=>'Real',z=>'Real');

I made these changes and ran the problem. It accepted "12ln(x)-8ln(y)-12-10ln(z)" as correct, but gave the expected feedback for "12ln(x)-8ln(y)-12ln(e)-10ln(z)".
In reply to Alex Jordan

Re: Controlling Simplifying in a Function

by Gregory Varner -

That did it!

Thank you very much for your help. It looks like the problem was in my understanding.

Interestingly, I do not get the error code from "adding" x, though I do realize that x is already inherently defined so it is not necessary.