Numerically checking equality is a practical way to be confident (to a degree) of a right/wrong answer. It is usually faster than symbolic analysis. There is a small probability that the check will fail. And numerical analysis comes into play. There are some ill-conditioned problems for which there is a high probability that a correct answer would be rejected. Mathematica has some sophisticated tools you can use if you wish the check to be virtually always correct. However, ill-conditioned problems might not come up very often in homework, and you might be willing to deal with computer mistakes on a case-by-case basis. This sort of checking needs to be viewed as a practical matter, and you need to decide what is worth worrying about and what isn't.
Simple numerical check
Here is a simple numerical check to test if two expressions are equal. Usage: simpleNEqual[e1, e2, {x, a, b}]
or simpleNEqual[e1, e2, {x, a, b}, {y, c, d}]
etc., with however many standard iterators {x, a, b}
like in Integrate
that you need, one for each variable. (This could be extended to regions, using RandomPoint
instead of RandomReal
, if rectangular domains won't work.)
Weaknesses:
- It checks numerical equality at 100 points (
npts = 100
). It is possible that two unequal expressions agree on 100 random points. Possible but unlikely.
- It uses arbitrary-precision numbers of 32 digits precision (
precision = 32
). It is possible that with round-off error, an expression might lose all digits of precision and the calculated value become unreliable. This happens with single-digit floating point number (~8 digits of precision) with enough frequency that when double-precision (~16 digits) became common in CPUs, the people rejoiced. However, everyone who does numerical computation finds occasionally that 16 digits is not enough. With 32 digits, it should be rare that precision loss leads to an unreliable result, but it can happen.
Code:
ClearAll[simpleNEqual];
simpleNEqual[e1_, e2_, dom : {_, _, _} ..] :=
Module[{vars, domains, testpts, npts, precision},
(* set up *)
vars = {dom}[[All, 1]];
domains = {dom}[[All, 2 ;; 3]] /. {-Infinity -> -1000, Infinity -> 1000}; (* impose finite limits on Infinity *)
(* internal parameters - could be arguments/options *)
npts = 100; (* enough points? *)
precision = 32; (* too low, might fail; too high, waste time *)
(* actual checks *)
testpts = Transpose[
RandomReal[#, npts, WorkingPrecision -> precision] & /@ domains
];
AllTrue[testpts, e1 == e2 /. Thread[vars -> #] &]
];
Examples:
simpleNEqual[fstudent[x, y], fbook[x, y], {x, 0, Infinity}, {y, 0, Infinity}] // AbsoluteTiming
(* {0.0036, True} *)
simpleNEqual[fstudent[x, y], (1 + 10^-29) fbook[x, y], (* the small difference is detected *)
{x, 0, Infinity}, {y, 0, Infinity}]
simpleNEqual[fstudent[x, y], (1 + 10^-30) fbook[x, y], (* the difference is too small and is undetected *)
{x, 0, Infinity}, {y, 0, Infinity}]
(*
False
True
*)
Sprucing the code up a bit.
You could add options to control the value of npts
and precision
. You could also put in checks to see if there were problems and report via Sow
the problem and the input that caused the problem; use Reap
to get the information. For instance, define an error message and change the AllTrue[..]
code as follows:
simpleNEqual::prec = "Less than two-digit precision at ``.";
AllTrue[testpts,
With[{msg = $MessageList},
Check[res = {e1, e2} /. Thread[vars -> #], (* check if evaluation of the expressions gives errors/warnings *)
Sow[# -> Complement[$MessageList, msg], "error"]];
If[Precision[res] < 2 && res[[1]] != 0 && res[[2]] != 0, (* check if too much precision loss *)
Message[simpleNEqual::prec, Sow[Thread[vars -> #], "precision"]]];
res = Equal @@ res;
If[! res, Sow[Thread[vars -> #], "unequal"]]; (* Sow the input that proves e1 != e2 *)
res] &]