WeBWorK Main Forum

Correct zero answer not being accepted

Correct zero answer not being accepted

by Michael Shulman -
Number of replies: 11
In a problem I have the following code:

$L = random(-20,20,.1);
$M = non_zero_random(-20,20,.1);
$LplusM = $L + $M;

BEGIN_PGML
Answer: [_____]{$LplusM}
END_PGML

For one student, it happened that $L=7.7 and $M=-7.7, so the answer should be zero; but when she enters 0 it is not accepted. The answer that is accepted is -5.34, while displaying the value of $LplusM yields 3.5527136788005e−15.

My best guess as to what happened is that (1) somehow the real number randomness or arithmetic is not exact, so that $LplusM produces a very tiny number instead of zero, and then (2) when comparing answers, the perl number "3.5527136788005e−15" is not passed to webwork directly but goes by way of its string representation, and webwork then fails to recognize that string as scientific notation and instead interprets it as 3.5527136788005 times the number e (2.71828...), minus 15, obtaining -5.34. So:

(1) Why is $LplusM not identically zero?

(2) Why is webwork not seeing $LplusM as a perl number directly?

(3) How do I fix this problem?

The best solution I have found so far is to write instead
$LplusM = Real($L + $M);
to force webwork to see $L+$M as a perl number instead of stringifying it first (in which case the inexactness of $LplusM is not a problem as long as the tolerance is nonzero). Is this the best/only way? I would really like to be able to make $LplusM come out identically zero too, in case I wanted a zero tolerance.

In reply to Michael Shulman

Re: Correct zero answer not being accepted

by Sreyas Chintapalli -
1. Both numbers are being generated randomly, perhaps try 

$M=Real(-1*$L);
 to ensure that the sum is 0.

The Real(' XXXX ') declaration will help with that as well.
In reply to Sreyas Chintapalli

Re: Correct zero answer not being accepted

by Michael Shulman -
I *want* both numbers to be generated randomly. The question is about a situation when it *happened*, for one student, that the two randomly generated numbers ought to have had sum zero.
In reply to Michael Shulman

Re: Correct zero answer not being accepted

by Danny Glin -
The answer to your first question probably has to do with floating point arithmetic.  Because of the way floating point numbers are stored there is the possibility of such rounding errors.  In a case such as yours I generally try to do as much of the arithmetic using integers.  I would do something like

$tenL = random(-200,200,1);
$tenM = random(-200,200,1);
$L = $tenL/10;
$M = $tenM/10;
$LplusM = Compute(($tenL + $tenM)/10);

This way the addition being performed is of integers and not floating point numbers, meaning less of a chance of rounding error.

For your second question (and Davide can correct me if I'm wrong here), you are passing a perl variable and not a MathObject to the answer checker.  PG reads in the variable, and tries to covert it to a MathObject.  As you suspect, it reads "3.5527136788005e−15" and then parses this as multiplication by e.  The remedy is basically as you suggest, which is to define the variable $LplusM as a MathObject using either "Compute" or "Real".  In general it is better to use "Compute" than "Real", as this does a better job with things like the correct answer string.  In fact, it is probably safer to never pass a raw perl number or string to an answer checker, as you have seen in your example.

I think the code I provided above should solve your problem.  Respond back if it does not.

Danny
In reply to Michael Shulman

Re: Correct zero answer not being accepted

by Alex Jordan -
(1) Why is $LplusM not identically zero?

It is as you suspect. Rounding error creeps in when converting back and forth between decimal and binary. What looks nice and simple like 7.7 + -7.7 in decimal is something like:
111.1011001101 + -111.1011001100
with both binary expressions truncated, and you are left with something like 0.0000000001 in binary, which is 2^(-10), which is very small. If you take log-base 2 of your "3.5527136788005e−15", you get -48 on the dot. So the issue is like my example, except happening out in the 48th binary slot.


(2) Why is webwork not seeing $LplusM as a perl number directly?

When you use PGML as you are doing and simply pass a perl scalar to the answer checker, MathObjects applies Compute() to it, and it's stringified to be passed to the parser. I guess Compute() could be changed to treat numerical scalars differently from string scalars, but Davide might know reasons why we shouldn't do that.

(3) How do I fix this problem?

I would do what you did:
$LplusM = Real($L + $M);

You note that you are concerned with the nonzero error tolerance this allows for your students, but you cannot get away from that. It's not a WeBWorK issue really. It's always an issue converting from binary to decimal.

If you really want $LplusM to have the option of being exactly zero, you could do:

$L = random(-20,20,.1);
$M = non_zero_random(-20,20,.1);
$LplusM = sprintf("%.1f", $L + $M);

which would round the result to one decimal place. So it will now be "0". If course the students will still be able to enter 10^-50 as an answer and be counted correct (but why would they?)


In reply to Alex Jordan

Re: Correct zero answer not being accepted

by Paul Pearson -
Hi Mike,

Many good answers have already been posted.  Under these circumstances, I suggest using good old double quotes:

Answer: [_____]{"$L + $M"}

The values for $L and $M will be substituted into the Perl string "$L + $M" but the Perl string "$L + $M" will not be interpreted by Perl (and thus not reduced to zero by Perl).  The answer checker will first convert this raw Perl string "$L + $M" to the MathObject Compute("$L + $M") and then proceed to check the answer using proper zero level tolerances and reduction rules for MathObjects.  Further, when the user chooses to show correct answers, they will see the correct answer listed as 7.7+(-7.7) instead of 0, which might be beneficial since it is more like a solution than just an answer.  

Also, in the future please post a minimal working example (complete .pg file) with the random seed number so that others are able to reproduce what you're reporting.

Best regards,

Paul Pearson


In reply to Paul Pearson

Re: Correct zero answer not being accepted

by Michael Shulman -
Thanks everyone for your answers! I notice a disagreement: Danny suggests never passing a raw number or string to an answer checker, while Paul explicitly suggests passing a string. Personally, I would want the answer to be displayed as 0 in this case, since that is the answer; if I wanted to show the addition as part of the answer, I would include it explicitly. Are there other reasons to choose one or the other?

I'm also puzzled as to why the rounding/binary issue crops up here, when Perl seems to have no trouble with it on its own:

$ perl -e 'print 7.7 + (- 7.7)'
0

(And it definitely seems like a bug to me that if you pass a raw perl number like 3.5527136788005e−15 to an answer checker, it gets stringified and then parsed into -5.34. Raw perl numbers should either be treated correctly or refused as an error, not silently turned into different numbers.)
In reply to Michael Shulman

Re: Correct zero answer not being accepted

by Davide Cervone -
This issue has come up before, although one of the solutions suggested there no longer applies, since the $# variable was removed in perl v5.10 (which is a shame). Alex, Paul, and Danny are correct in pointing out what is going on.

Since you are using PGML, when you pass a scalar value (a perl real or string), PGML will first convert it to a MathObject and then take the MathObject's answer checker. To do that, it calls Compute() on the value you gave it, and Compute() turns it into a string (if it isn't already) and then parses the string using the parsing rules codified in the current Context. The default Numeric context has e defined as the base of the natural logarithm, and so something like 3.5527e−15 is interpreted as (3.4427*e)-15, and you get the -5.34 that you are seeing. For this reason, in WeBWorK answers, exponential notation is supposed to be given using a capital E not a lower case e.

That does make this situation a difficult one to handle. There are a couple of ways to approach it. One is Alex's solution of using Real() to force the answer to be treated as a real number rather than a string to be parsed, and that is probably the most effective solution. If you always do your computations with MathObjects, you will not end up in this problem. So using

$L = Real(random(-20,20,.1));
$M = Real(non_zero_random(-20,20,.1));
$LplusM = $L + $M;
would be another way to handle it.

An alternative would be to use

BEGIN_PGML
Answer: [_____]{uc($LplusM)}
END_PGML
in order to force the lower-case e to become an upper-case one before its string is parsed.

It would be best if we could force perl to stringify its numbers using upper-case E, but that is no longer easy to do (the old $# variable could be used to do that, but as I mentioned above, it has been removed in current versions of perl). It would be possible to use overload::constants to cause perl numbers to produce MathObjects rather than perl reals, but we would not want to use MathObject Reals for that, since MathObject Reals have fuzzy comparisons, which would break a lot of code. Instead, we'd need a new PerlReal class that implemented all the usual perl operations, but that stringify using capital E. This could be done, but I'm not sure the overhead is worth it, and it could have unexpected side-effects. Having problem authors use MathObject Reals more regularly would probably be a better solution.

As for passing strings to answer checkers, I'm not sure if Danny's suggestion was meant for PGML or not. It is correct and perfectly reasonable to pass a string to PGML, and to Compute() in general. That is the whole purpose of Compute(); you give it a string in student-answer form, and it produces the associated MathObject for you to work with. The idea is that you and the student both enter mathematics in the same way, so your answers are sure to be something they can enter. On the other hand, passing a number to PGML or Compute()does run the risk of misinterpreting exponential notation as you have seen here. For that reason, I still use Real() when converting a number to a MathObject rather than Compute(), since the latter converts to a string first and the former doesn't (or rather does but checks if it looks like a number after doing so).

You say that "raw perl numbers should either be treated correctly or refused as an error, not silently turned into different numbers." The problem is that perl doesn't make a distinction between numbers and strings; it converts between the two as needed, and the author is responsible for keeping the two straight. That is one reason for having separate string comparisons (lt, eq, etc.) from numeric comparisons (<, ==, etc.) and string concatenation as . rather than +. If a number is used with eq, then it is stringified first, and if a string is used with ==, then it is converted to a number first.

This means that a scalar variable like $LplusM could be either a number or a string, and perl gives you no way to tell (to my knowledge), because it doesn't care. So there is no way for Compute() or PGML to know if the value in $LplusM is a number or a string. (Only you as the author know that.) So MathObjects treats everything as a string to be parsed. There really isn't much choice.

This is why I suggest using Real($LplusM) rather than using the perl scalar directly.

I will answer your question about why the result isn't exactly 0 in a separate message, though Alex has already given you the essence of it.

In reply to Davide Cervone

Re: Correct zero answer not being accepted

by Michael Shulman -
Thanks for the very detailed answer!

Why does "random" return a perl number rather than a MathObjects Real in the first place?

As for distinguishing between numbers and strings in Perl, a quick google search turned up this, which suggests using Devel::Peek or XOR (see "isnum" at the very bottom).
In reply to Michael Shulman

Re: Correct zero answer not being accepted

by Davide Cervone -
Why does "random" return a perl number rather than a MathObjects Real in the first place?

Because random() long predates the development of the MathObjects library.

The isnum() approach does look useful for this. I will put it on the to-do list.

Davide
In reply to Michael Shulman

Re: Correct zero answer not being accepted

by Davide Cervone -
I'm also puzzled as to why the rounding/binary issue crops up here, when Perl seems to have no trouble with it on its own

The reason is that the values obtained from random() and non_zero_random() are not the same as the ones obtained from the literal 7.7 and -7.7. To see this, you have to know how random() produces its numbers (non_zero_random() uses random() internally). The random number is obtained by taking the lower limit (-20 in your case) and adding some integer number times the increment (.1 in your case), where that integer is obtained randomly in a range that is dependent on the lower and upper limits so that the result is within the given range.

What this means is that your 7.7 is actually obtained from -20 + 277 * .1 while -7.7 is obtained from -20 + 123 * .1. Note that .1 is a repeating "decimal" in binary, namely 0.00011001100110011..., so any computation with it (in floating-point binary) will be imprecise.

To make the example easier, consider the situation for your $L being .1 and $M being -.1 That is, $L = -20 + 201 * .1 or $L = 20.1 - 20 and $M = -20 + 199 * .1 = -(20 - 19.9). [Although there is a difference between 201 * .1 and 20.1, it does not play a role here, nor does changing the order or factoring out the negative.]

In binary, 20 is 10100, 20.1 is 10100.0001100110011... and 19.9 is 10011.11100110011.... Numbers in perl are stored in double-precision IEEE floating-point format, and that means that there are 53 digits stored (plus 11 used for an exponent and one for a sign; an interesting question is why this is 65 rather than 64 bits). For 20.1 there are six digits before the start of the four repeating digits 0011 so the last few digits stored are 0011001. If you carry out the subtraction 20.1 - 20 in this case, you get

      10100.00011001100...11001
     -10100.00000000000...00000
     --------------------------
          0.00011001100...11001
as expected. Similarly, for 19.9, the last few digits stored are 00110 (note that 19.9 has the 0's and 1's exchanged in comparison to 20.1), so for 20 - 19.9 you get
      10100.00000000000...00000
     -10011.11100110011...00110
     --------------------------
          0.00011001100...11000
Note that these are not the same, and the difference between the two is
          0.00000000000...00001
where that 1 is in the 48th decimal place (48 decimals plus the leading 5 for 10100 is the 53 digits used in IEEE floating-point reals). That is the 2^-48 = 3.5527136788005e−15 that you are seeing in the computation.

There is really nothing to be done about that, as that is the way floating-point numbers work. It is one of those things that you have to deal with, and be aware of when you write problems, as there is no escaping it.