$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.
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.1011001100with 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?)
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.)
$#
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_PGMLin 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.
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).
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
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...11001as 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...11000Note that these are not the same, and the difference between the two is
0.00000000000...00001where 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.