26

In this answer, Oleksandr R. mentioned an undocumented function Internal`CompareNumeric and briefly explained its usage as follows:

Internal`CompareNumeric[prec, a, b] returns -1, 0, or 1 according to whether a is less, equal, or greater than b when compared at the precision of a or b (whichever is less) minus prec decimal digits of "tolerance". It is the fundamental operation underlying Less, Equal, Greater, LessEqual etc. for finite-precision numeric types.

…To be honest, I failed to understand this description. In my eyes, it seems to suggest that, Internal`CompareNumeric will compare

With[{minPre = Precision /@ {a, b} // Min}, N[{a, b}, minPre - prec]]

but it's not true, because

Internal`CompareNumeric[1, 1.1`2, 1.2`2]
(* -1 *)

while the output should be 0 in my understanding.

Can someone explain the usage of Internal`CompareNumeric in a more detailed way?

xzczd
  • 65,995
  • 9
  • 163
  • 468
  • my guess the comparison is actually made directly on the binary representation and the "decimal digits" count only approximately translates to a specific number of base 2 digits. – george2079 Mar 20 '17 at 22:03
  • @george2079 Hmm… but Internal`CompareNumeric[1, 11`2, 12`2] still gives -1. – xzczd Mar 21 '17 at 03:13
  • 4
    FYI, the split point of -1 and 0 is around7.3456427360042 – vapor Mar 26 '17 at 09:10
  • I was discussing this with yode the other day. Internal`CompareNumeric[n,a,b] basically answers the question, "how do a and b related ignoring n points of precision". So if you have two numbers and drop $MachinePrecision points of precision it's basically comparing them at the integer level. e.g. Internal`CompareNumeric[IntegerPart@$MachinePrecision, 2.1, 2] (because my $MachinePrecision isn't an int) gives 0, but Internal`CompareNumeric[IntegerPart@$MachinePrecision - 1, 2.1, 2] gives 1. – b3m2a1 Mar 29 '17 at 06:13
  • @MB1965 Well, but how to explain the behavior of the last code piece in my question? – xzczd Mar 29 '17 at 06:19
  • I just figured it assumes all numbers have $MachinePrecision points of precision. Because internally they do. That precision specification is just some top-level thing. It's an internal function after all. So it only uses the 1.1 from 1.1`2. I'm betting when you have something like 1.0001`2. == 1.0 it's really Internal`CompareNumeric[$MachinePrecision-2, 1.0001, 1.0]. – b3m2a1 Mar 29 '17 at 06:26
  • Although as @happyfish notes that's not actually right because that would give equal in that case. – b3m2a1 Mar 29 '17 at 06:29
  • @MB1965 Sorry, but I don't understand your last comment. What's "not actually right"? And I'm afraid you've lost the bet because, for example, Internal`CompareNumeric[9, 1.0001`2, 1.0] gives 0, while Internal`CompareNumeric[9, 1.0001, 1.0] gives 1. – xzczd Mar 29 '17 at 06:36
  • That's what I meant. That formula isn't right, because there's that odd switch point that @happyfish found. – b3m2a1 Mar 29 '17 at 06:37
  • @MB1965 Yeah. I tried to make formulas according to the docs with attempts on binary and logarithm, but yet not able to get that number – vapor Mar 29 '17 at 06:39
  • @happyfish any idea given that that the $MachinePrecision docs suggest it'll be 53 Log10[2.]? (which I realize now is just Log10[2^53]...) – b3m2a1 Mar 29 '17 at 06:48
  • @MB1965 Yes, the split point for all machine precision integers has that split point – vapor Mar 29 '17 at 07:27
  • @happyfish if you plot the number of points you have to remove past the ~15 done in the answer given it's clearly linear starting at $MachinePrecision. Before that it has a weird log shape. I'll post a supplementary answer to that maybe will give you ideas. No log function I've tried fits it though. – b3m2a1 Mar 29 '17 at 07:31
  • @MB1965 thank you very much – vapor Mar 29 '17 at 07:37
  • 1
    Additional question: Why does Internal`CompareNumeric give an error message when prec is set smaller than $MinPrecision? – TimRias Mar 29 '17 at 14:21
  • Your 500 rep is so hot,which is out of my expectation.But it looks like it's going to be expired soon? :) – yode Apr 02 '17 at 13:15
  • @yode Out of my expection, too :D . To be honest I'm now having a hard time judging the answers I recieved 囧. Anyway I'll award it in time, just as I always did :) . – xzczd Apr 02 '17 at 13:56

4 Answers4

7

This is just to highlight another aspect of the other answer given. I would have expected a linear progression from the definition. I.e., if Precision[n]=p then we'd expect the switch point for CompareNumeric[x, n, n+epsilon] to be p. And this holds once you get past $MachinePrecision. Probably happens when Mathematica has to go from 64 bit numbers to whatever its internal infinite-precision numeric is. We can see this here:

pr[x_] :=
  SelectFirst[
   Range[1, 100, .1],
   Internal`CompareNumeric[#, SetPrecision[1.1`, x], 1.2`100] == 0 &
   ];

Show[
 Plot[x, {x, 0, 25}, PlotStyle -> Gray],
 {#, pr@#} & /@ Range[1, 25, .01] // ListPlot
 ]

blah

I looked at the part for n < $MachinePrecision and thought: "oh, a log-type function (probably log2)". But no simple scaling on Log2 worked for me.

Show[ListLinePlot@Table[Table[x*N@Log2[n], {n, 15}], {x, 5}], 
 ListPlot[{#, pr@#} & /@ Range[1, 15, .01]]]

log2

So maybe someone else can tackle what sort of pre-form that is. Once we have that we can probably piece back how Mathematica is interpreting things for its sub-machine precision numbers.

Update

As found by happy fish, $MachinePrecision - $MachinePrecision/x fits this really well:

Plot[{
  pr[x],
  $MachinePrecision - $MachinePrecision/x
  },
 {x, 1, $MachinePrecision}
 ]

fitted

Noting that $MachinePrecision is 53Log10[2] by simple log rearrangements we have Log10[2^(53*(1 - 1/x))]. Since $MachinePrecision comes from how a 64 bit number is stored (there are 53 bits of precision), this could be saying it's using 53*(1 - 1/x) bits of precision for these numbers with Precision specified below $MachinePrecision. Why that would be, I have no idea. But it's a possibility.

Update 2

Shadowray points out this is even odder when you have fewer points of precision available before they would be equivalent

For example, make pr multivariate:

pr[x_, y_: 1] :=
  SelectFirst[
   Range[1, 100, .1],
   With[{
     a = SetPrecision[1. + (10^(-y)), x],
     b = SetPrecision[1. + (2*10^(-y)), 100]
     },
    Internal`CompareNumeric[#, a, b] == 0 &
    ]
   ];

Now plotting this for different amounts of reduction:

Show[
 Plot[x, {x, 0, 25}, PlotStyle -> Gray],
 Table[
   {#, pr[#, n]} & /@ Range[1, 25, .05],
   {n, 17}
   ] // ListPlot
 ]

changingprec

Note that the first 6 curves actually continue. They just jump up to y=x after $MachinePrecision (as I would naively have predicted).

If we use a near-continuous change:

Show[
 Plot[x, {x, 0, 25}, PlotStyle -> Gray],
 Table[
   {#, pr[#, n]} & /@ Range[1, 25, .05],
   {n, Range[1, 17, .1]}
   ] // ListPlot
 ]

continuous

The pre-discontinuity part can still be easily formulated:

Show[
 Plot[x, {x, 0, 25}, PlotStyle -> Gray],
 Plot[
  Evaluate@Table[$MachinePrecision (1 - n/x),
    {n, 16}],
  {x, 1, $MachinePrecision}],
 Table[
   {#, pr[#, n]} & /@ Range[1, 25, .1],
   {n, 17}
   ] // ListPlot
 ]

shifting

What's happening at the discontinuities is somewhat less clear. Note that the oddity at 17 is from the fact that 10^-17 is smaller than 10^-$MachinePrecision and so the system just treats the addition as doing nothing. The jumps need more explanation though.

In any case, if we return to the storage argument, now we have 53 n/x bits being dropped, assuming the interpreting the pre-jump part of the curves as showing Log10[2^(53 (1 - n/x))] is valid.

b3m2a1
  • 46,870
  • 3
  • 92
  • 239
  • @xzczd made the pr much more sensitive. Shows the real discontinuity at $MachinePrecision now. – b3m2a1 Mar 29 '17 at 08:06
  • I got similar plots. Although I used FindFormula to get very close fits, but those forms and coefficients do not make sense... – vapor Mar 29 '17 at 08:13
  • @happyfish It honestly looks like a -1/r thing (but shifted vertically). Simple (16+-1/r) is way too steep though. – b3m2a1 Mar 29 '17 at 08:21
  • $MachinePrecision -17.217894/x no idea – vapor Mar 29 '17 at 08:27
  • @happyfish Wow. That fits perfectly. Who knows if it's right. Maybe someone brighter than myself will see that and be able to explain why it works. Maybe it's supposed to be $MachinePrecision - $MachinePrecision / x? That's just as good a fit, I think. – b3m2a1 Mar 29 '17 at 08:29
  • I think I have found the exact formula, proceeding to the final step... – vapor Mar 29 '17 at 10:31
  • What if one uses Internal`CompareNumeric[#, SetPrecision[1.0000000001`, x], 1.0000000002`100] in pr function? – Ray Shadow Mar 29 '17 at 11:15
  • @Shadowray fun point. I think it'll be possible to explain that. – b3m2a1 Mar 29 '17 at 14:18
5

After some experiments I found some of the "rules" with which Internal`CompareNumeric[c, a, b] operates. If you find any exceptions please notify me. Lets denote minpr = Min[Precision[a],Precision[b]]. If c is greater than pr[minpr] then Internal`CompareNumeric[c, a, b] kind of throw away anything after the point. The function pr depends on a and b, the only symmetry I was able to detect is that if minpr>15, pr[minpr]=minpr (maybe connected to MachinePrecision?). The following figure is a possible pr[].

enter image description here

xzczd
  • 65,995
  • 9
  • 163
  • 468
  • 1
    What is pr[]? – Michael E2 Mar 29 '17 at 11:28
  • @MichaelE2 An assumed function relationship between minpr and the threshold? – xzczd Mar 30 '17 at 09:25
  • @xzczd There's no definition of pr[] in your OP, nor code for it. I notice MB1965 has a code for it (but no definition other than the code). Is it the same as used here? Since the ordering of answers changes over time (this was the top answer when I read it), it would be helpful to point to the definition of pr[] or give the code, if it's different. In the answer, "The following figure is a possible pr[]" suggests to me that there might be various ways to define pr[] and the plot is a possible one. – Michael E2 Mar 30 '17 at 11:03
  • @MichaelE2 In my understanding, pr[] is more of a guess in this answer,. (BTW this is indeed the earlest answer. ) Anyway let's wait for Most Perfect Username's clarification :) – xzczd Mar 30 '17 at 11:10
  • @xzczd A guess at what? There's no def. But yes, I think the OP can easily clarify. – Michael E2 Mar 30 '17 at 11:13
  • @MichaelE2 and @xzczd, pr[] is just a function with which help Internal`CompareNumeric works (imho), as I mentioned I don't know it's full form (@MB1965 answer is more complete at this regards), but nevertheless it's easy to get pr[] empiric, for concrete a, b, c (I think the method is obvious, but if you want I can write examples). The connection between Internal`CompareNumeric and pr[] is in the answer. – Most Perfect Username Mar 30 '17 at 11:26
  • Thanks for your reply, but I'm afraid I'm still clueless and starting to feel stupid. You, MB1965, and @xzczd seem to know what pr[] represents and how to compute it. It does not show up in GeneralUtilities`PrintDefinitions@Internal`CompareNumeric (nothing does). Let me ask another way: What is the code for your plot of pr[]? How were the coordinates of the points computed? – Michael E2 Mar 30 '17 at 11:54
  • @MichaelE2 , lets look at an example, Internal`CompareNumeric[c, 2`2, 2.1`40] depending on c it gives 0 or -1 , plot. In this case pr[Min[Precision[2`2],Precision[2`40]]]=5, If you get all results for Internal`CompareNumeric[c, a`pra, b`prb] for any pra, prb you get pr[].(I have difficulties in adding the plot...) – Most Perfect Username Mar 30 '17 at 12:15
  • Thanks, again. So is this a definition of pr[]?: "pr[a, b] returns the least prec for which Internal`CompareNumeric[prec, a, b] returns 0." (That's the sort of thing I was looking for.) So it might be computed with FindRoot[Abs@Internal`CompareNumeric[c, 2`2, 2.1`40] == 1/2, {c, 0, 1000}] (ignoring the unimportant error messages)? – Michael E2 Mar 30 '17 at 12:46
  • @MichaelE2 yes, but I think you mean "returns not 0"? For the second question` also yes (from first glance, if I have time I will think more about it). – Most Perfect Username Mar 30 '17 at 12:53
5

In this answer I will present an overall simulation of Internal`CompareNumeric's behavior. Throughout this answer, there are four variables: Internal`CompareNumeric[tol, a, b], first=Min[Abs[a],Abs[b]] diff=Abs[a-b] and prec is the minimum precision of the two specified. Here's my findings so far

  • For exact arithmetic, Internal`CompareNumeric gives correct answer regardless of tol.
  • For machine precision integers (1., 2.), the split point is always $MachinePrecision.
  • The order and sign of a, b does not matter, the minimum precision is used for calculation(this can be implied from my definition above).
  • The smallest prec that makes the two numbers comparable is $-\log_{10}(\frac{diff}{first+diff})$ (this number comes from the definition of Precision), i.e. when $prec>-\log_{10}(\frac{diff}{first+diff})$, there exist a tol, such that the function returns -1 or 1; when prec is less than this number, the function returns 0 for all tol settings.
  • When $prec\in [-\log_{10}(\frac{diff}{first+diff}),\$MachinePrecision]$, there exist a split point $y$, such that when $tol<y$, the function returns -1 or 1, when $tol>y$, the function returns 0. The analytical formula with first, diff, prec as parameters is $$y(first,diff,prec)=\$MachinePrecision (1+\frac{1}{prec}\log_{10}(\frac{diff}{first+diff}))$$
  • For prec>$MachinePrecision, the split point of tol is exactly the same as prec.
  • For prec>$MachinePrecision, the formula above is incorrect. Some experiments show that when the ratio of first and diff is big enough, the comparison is converted to exact arithmetic(independent of tol). There should be two such points when diff/first is small and big, I haven't worked on it yet. A quick fixed of the formula is y[prec,first,diff]=prec-If[#>8,#,0]&[-Log10[diff/(first+diff)]], I will make careful research later when I have time.

The following is the code version of the findings above:

fishCompareNumeric[tol_, a_, b_] := 
 With[{first = Min[Abs@a, Abs@b], diff = Abs[a - b], prec = Precision@{a, b}},
  Module[{split},
   Catch[
    split = Piecewise[{
       {Throw@Sign[a - b], prec === Infinity},
       {$MachinePrecision, prec === MachinePrecision},
       {Throw@0, prec < -Quiet@Log10[diff/(first + diff)]},
       {$MachinePrecision (1 + Log10[diff/(first + diff)]/prec), -Log10[diff/(
           first + diff)] <= prec <= $MachinePrecision},
       {prec, prec > $MachinePrecision}
       }];
    Piecewise[{
      {Sign[a - b], tol < split}
      }]]]]

Here is a plot of my binary search for approximate values, with parameters(OP's example) first=1.1, diff=0.1, horizontal axis being prec and vertical axis being split point (of tol).

enter image description here

You may notice the two vertical lines at around 1.07 and 16, they are introduced by the sudden value changes, at the two points in my formula.

Here is a plot of my exact formula with same parameters enter image description here

Code for binary search approximation:

splitPointBinary[{{val1_, val2_}, {low_, up_}}] := 
 With[{mid = Internal`CompareNumeric[(low + up)/2, val1, val2]}, 
  If[Internal`CompareNumeric[low, val1, val2] == 
    mid, {{val1, val2}, {(low + up)/2, up}}, {{val1, 
     val2}, {low, (low + up)/2}}]]
splitPointApprox[first_, diff_, prec_] := 
 FixedPoint[
   splitPointBinary, {{SetPrecision[first, prec], 
     SetPrecision[first + diff, prec]}, {0.1, 1000}}][[2, 1]]

Code for my formula:

splitPoint[first_, diff_, prec_] := 
 Piecewise[{{$MachinePrecision (1 + Log10[diff/(first + diff)]/prec), 
    -Log10[diff/(first + diff)] < prec < $MachinePrecision}, {prec, 
    prec > $MachinePrecision}}]

For OP's example,

splitPointApprox[1.1, 0.1, 2]
(*7.34564*)
splitPoint[1.1, 0.1, 2]
(*7.34564*)

Another one,

splitPointApprox[2.2, 0.5, 6]
(*14.0071*)
splitPoint[2.2, 0.5, 6]
(*14.0071*)
vapor
  • 7,911
  • 2
  • 22
  • 55
  • This looks strange for splitPointApprox[1.1, 0.0000000000001, 2] – Ray Shadow Mar 29 '17 at 11:14
  • @xzczd answer updated. – vapor Mar 31 '17 at 06:26
  • I took the liberty to add a function interpreted from your findings to your answer, feel free to edit or roll back if you don't like it :) . – xzczd Apr 02 '17 at 14:03
  • @xzczd I like it :) – vapor Apr 02 '17 at 14:41
  • Since I haven't think out a systematic enough way to judge the answer, I'll leave it unaccepted for a while, but this is undoubtfully the most plausible answer I received so far, +51 :) . – xzczd Apr 03 '17 at 03:21
  • @xzczd thanks for you generous award! I hope these can be researched more deeply and proved more rigorously in the future:) – vapor Apr 03 '17 at 04:16
  • Try fishCompareNumeric[40, 1.`40 10^10, 1.`40 10^10 + 10^1000] – TimRias Apr 03 '17 at 07:46
  • Actually, that is not. The same happens at the a lot less extreme fishCompareNumeric[40, 1.`40 10^10, 1.`40 10^10 + 10^3], where num = Log10[10^3/(1.40 10^10+10^3)]is not small at all. Moreover, there is an issue at ``fishCompareNumeric[35, 1.40 10^10, 1.40 10^10 + 10^2]``. In general, your formula does not seem to reproduce the correct switch over point indiffforprec > $MachinePrecision`. – TimRias Apr 03 '17 at 09:15
  • @mmeent Thanks for pointing that out. It does appear the offset of the split point formula is incorrect in some cases after machine precision (but the slope is still one). I admit I did not think much on the formula for this part. Since I do not have much spare time on weekdays, so I will update my answer by this Sunday. – vapor Apr 03 '17 at 10:06
2

Let's consider a slightly different approach than above. For Internal`CompareNumeric[prec, a, a+ε], lets fix prec and a and find the threshold value for ε for which this switches from 0 to -1. For this purpose I define the following function

tf[p_?NumericQ, y_?NumericQ, z_?NumericQ] :=
   Internal`CompareNumeric[
     p,
     SetPrecision[1, y],
     SetPrecision[1, y] + 10^-SetPrecision[z, 100]
]

That is we take a = SetPrecision[1, y] and ε = 10^-SetPrecision[p, 100]. (The SetPrecision in the definition of ε is there to avoid that the precision of z in the input can affect the outcome in anyway.)

We can then find the threshold value using simple bisection. For example abusing FindRoot:

tf2[p_?NumericQ, y_?NumericQ] := (z /. 
  FindRoot[tf[p, y, z] == -1/2, {z, -10, p + y + 1}, 
  WorkingPrecision -> 100, MaxIterations -> 20])

Plotting the threshold value as a function of y for values of p we get enter image description here

The top curve (p=0) is simply given by z = y. More generally the behavior of tf2 is modelled precisely by

Max[
 Piecewise[
  {
   {y - p, y > MP},
   {((MP - p)/MP) y, y < MP}
  }
 ],
 MP/2 UnitStep[y - Max[MP, p]]
]

where

MP=$MachinePrecision

What does this tell us about the behavior of Internal`CompareNumeric[prec,a,b]? Well, if the precision of a and b is set higher than machine precision and prec is not too large, the behavior is exactly as described by Oleksandr R.

It becomes a bit weirder when prec is large (compared to the precision of a and b), in which case there is an apparent upperbound of on the threshold difference of ε = 10^(-MP/2).

I don't quite understand the behavior of the threshold when the precision of a and b is smaller than machine precision. Maybe somebody else can shed some light.

Update:

Based on the above we can venture to propose a mock version of Internal`CompareNumeric[prec, a, b]:

MockCompareNumeric[p_,a_,b_]:=With[
 {ip = Min[Precision[a], Precision[b]]},
  With[
   {ep = Which[
    ip === MachinePrecision, MP-p
    ip >= Max[MP, p], Max[ip - p, MP/2],
    True, Max[(1 - p/MP) ip, 0]
    ]
   },
   If[N[ Abs[a - b]/Max[Abs[a], Abs[b]]] < N[10^-ep], 0, Sign[a-b]] 
 ]
];

I have tested the behavior of this mock function with the actual Internal`CompareNumeric over a large range of prec, a b, and they match. (edit 3.4.2017: adjusted the mock function to get the correct behavior for MachinePrecision input.)

Addendum

Ofcourse, the actual Internal`CompareNumeric would not work like this. Instead it probably uses some low level functions working directly on the internal representation of the arbitrary precision numbers. (It certainly is a lot faster) However, the mock function gives us a fairly good idea of the behavior of Internal`CompareNumeric[prec,a,b].

In short it determines some effective precision (ep in the mock function) from prec and the minimal precision of the inputs a and b and then determines whether a > b within a precision tolerance set by ep.

The difference between the behavior of ep dependent on whether the input precision is larger or smaller than machine precision probably arises due to differences in implementation leveraging the fact that for precision smaller than machine precision, Mathematica represents its arbitrary precision numbers simply as a pair of machine numbers representing the value and the precision of the number, in which case it can leverage various machine operations to do the comparison. I suspect that similarly the lower bounds on ep arise some checks that can be down efficiently using low level functions. The scaling of ep when ip < MP presumably is some design choice relating to the desired functionality, possibly related with wanting the effective precision to be larger than zero for reasonable values of prec.

TimRias
  • 3,160
  • 13
  • 17
  • Have you seen this, by any chance? – J. M.'s missing motivation Mar 29 '17 at 13:25
  • @ J.M. No I hadn't. I am also not sure why you are pointing to it. Could you explain? – TimRias Mar 29 '17 at 13:55
  • You mentioned wanting to find the threshold value where two numbers are no longer the same, so I think a function that would give the "next" floating point number might suit your needs. – J. M.'s missing motivation Mar 29 '17 at 13:58
  • @ J.M. Ah, I see. Well, in this case we are not interested in finding the threshold where the two numbers are no longer considered the "same" by Internal`CompareNumeric which is a slightly different question, than find the threshold where the internal representation becomes the same. (Arbitrary precision numbers in Mathematica can have many more digits in their internal representation than their actual precision value.) – TimRias Mar 29 '17 at 14:06
  • What if one evaluates MockCompareNumeric[5, -0.9252872333493522561`2., 0.9289648686205458361`3.] ? – Ray Shadow Mar 30 '17 at 09:36
  • @Shadowray: I guess the denominator may need to be Abs[a]+Abs[b] rather than Abs[a+b] – TimRias Mar 30 '17 at 11:11
  • Note that the mock function easily reproduces the odd behavior observed in the other answers. – TimRias Mar 30 '17 at 11:37
  • I'm afraid the guess ε = 10^(-$MachinePrecision/2) is incorrect, if you modify tf to tf[tol_?NumericQ, prec_?NumericQ, digit_?NumericQ] := Internal`CompareNumeric[tol, SetPrecision[100, prec], SetPrecision[100, prec] + 10^-SetPrecision[digit, 100]], you will find the split digit seems to become $MachinePrecision/2 - 2 – xzczd Apr 02 '17 at 07:44
  • @xzczd It is indeed the case that the absolute tolerance ε is no longer 10^(-$MachinePrecision/2) in that case. But the "effective precision" ep as it appears in the mock function is still $MachinePrecision/2. That is where the division by Abs[a]+Abs[b] comes in. (Actually I've found that it is not quite $MachinePrecision/2, but somewhere close to that, i.e. within 1%. – TimRias Apr 03 '17 at 07:14