Why is vpa not converting double accurately?

24 views (last 30 days)
I ran across this example that I don't understand. I don't use the Symbolic Toolbox much, but I thought vpa by default converted doubles to symbolic representations that were an accurate conversion up to so many decimal digits. I.e., an exact floating point binary to decimal conversion up to the specified number of decimal digits. But the following did not match my expectations:
format long
digits 50
hf = '40108c55a5de2880'; % making sure I am starting from the value I think I am
f = hex2num(hf)
f =
4.137045470890484
sf = sprintf('%.50f',f)
sf = '4.13704547089048446650849655270576477050781250000000'
str2sym(sf)
ans = 
4.1370454708904844665084965527057647705078125
vpa(f)
ans = 
4.1370454708905203952304813814278565783465431783307
double(vpa(f)) - f
ans =
3.552713678800501e-14
vpa(f,50) % Using this form of function call doesn't help
ans = 
4.1370454708905203952304813814278565783465431783307
digits 200 % this doesn't help either
vpa(f)
ans = 
4.1370454708905203952304813814278565783465431783307106731417481082606305993533207364559999183814049903246844172718745791741538190785178147855046143279189519799161307279637872963853860781001401307209969
double(vpa(f)) - f % back conversion to double doesn't match either
ans =
3.552713678800501e-14
What is going on here? Obviously vpa can store the exact floating point binary to decimal conversion of f:
vpa(str2sym(sf))
ans = 
4.1370454708904844665084965527057647705078125
double(ans) - f
ans =
0
I have to use fprintf with str2sym to get an accurate conversion. Why doesn't vpa do that with f in the first place? Is there some setting I am unaware of? And what are all those digits pouring out of vpa for this f?
Is this some type of p/q morphing going on in the background?
Other random values generally match my expectations. E.g.,
r = rand*100-50
r =
36.006640442455534
fprintf('%.50f\n',r)
36.00664044245553441214724443852901458740234375000000
vpa(r)
ans = 
36.00664044245553441214724443852901458740234375
r = rand*100-50
r =
-47.047616571398464
fprintf('%.50f\n',r)
-47.04761657139846420250250957906246185302734375000000
vpa(r)
ans = 
r = rand*100-50
r =
4.524320317487962
fprintf('%.50f\n',r)
4.52432031748796248393773566931486129760742187500000
vpa(r)
ans = 
4.524320317487962483937735669314861297607421875

Accepted Answer

John D'Errico
John D'Errico on 22 Mar 2025
Edited: John D'Errico on 22 Mar 2025
Quite interesting. When I first saw the title, I assumed it would be an obvious mistake. Then I saw the poster, and knew it would be interesting. My first assumption is the problem must lie in VPA, which sometimes seems to stand for:
VPA - Very Poor Arithmetic
If I look at what is happening though...
format long
digits 50
hf = '40108c55a5de2880'; % making sure I am starting from the value I think I am
f = hex2num(hf)
f =
4.137045470890484
num2hex(f)
ans = '40108c55a5de2880'
Good. That works.
vf = vpa(f)
vf = 
4.1370454708905203952304813814278565783465431783307
num2hex(double(vf))
ans = '40108c55a5de28a8'
and clearly that is not the same number. So, is the problem in VPA? Instead, I'll try this.
sym(f)
ans = 
Lol. That is sort of interesting. And a bit of a surprise.
digits 200
vpa(sym(f))
ans = 
4.1370454708905203952304813814278565783465431783307106731417481082606305993533207364559999183814049903246844172718745791741538190785178147855046143279189519799161307279637872963853860781001401307209969
Hmm. Is that really what is happening? What does that strange number with the radicals resolve to as a decimal? I'll try HPF, since I know exactly what HPF is doing. Hey, I trust HPF implicitly, since I know the guy who wrote it.
DefaultNumberOfDigits 200
x = sqrt(hpf(241))*sqrt(hpf(16499))/hpf(482)
x =
4.1370454708905203952304813814278565783465431783307106731417481082606305993533207364559999183814049903246844172718745791741538190785178147855046143279189519799161307279637872963853860781001401307209969
Ah. So now I know what happened. Well, I think I do, I think I do.
When you do this:
vf = vpa(f)
MATLAB FIRST converts the number to a sym, because VPA only understands symbolic numbers. Effectively, it expands it as
vf = vpa(sym(f))
But what does sym do? It decides the result of sym(f) is sqrt(241)*sqrt(16499)/482. After all, that is what we would all do, right? The obvious form. Then, and ONLY then, does it throw it into VPA.
So the problem is not in VPA. The problem lies in the decision to approximate that floating point number as a sym in the form it chose. The problem lies in sym.
My head hurts, just a little. ;-)
  19 Comments
James Tursa
James Tursa on 13 May 2025
Edited: James Tursa on 15 May 2025
There are clues in the examples posted in this thread, which led me to an investigation. I have now convinced myself that vpa( ) in the background stores numbers in an extended binary (or hex?) floating point format. I'm guessing the value itself uses a whole number of bytes or words. So when you set digits or convert a number using a certain number of requested digits, what actually must happen in the background is a calculation for how many binary bits are needed in a significand to ensure the requested number of decimal digits is met (and add in room for sign and exponent). Then extend that number to get you on a byte or word boundary, and add guard bytes or words. And that is what you actually end up with in the background. All vpa arithmetic in the background is probably binary (or equivalent) floating point arithmetic. And, if that is the case, then we should expect to see binary floating point artifacts in the results. And we do. E.g.,
format longg
0.1 + 0.2 - 0.3
ans =
5.55111512312578e-17
digits 20
vpa(0.1) + vpa(0.2) - vpa(0.3)
ans = 
5.0487097934144755546e-29
digits 21
vpa(0.1) + vpa(0.2) - vpa(0.3)
ans = 
0.0
digits 22
vpa(0.1) + vpa(0.2) - vpa(0.3)
ans = 
digits 23
vpa(0.1) + vpa(0.2) - vpa(0.3)
ans = 
0.0
The double precision calculation is a common one to use for this demonstration. But note that vpa has some very similar artifacts, depending on how many decimal digits were requested, which I presume affected the actual number of bytes/words used in the background.
The fact that the residuals are precisely the value of a binary bit lends credence to this hypothesis:
2^(-94)
ans =
5.04870979341448e-29
2^(-100)
ans =
7.88860905221012e-31
I find it interesting that the number of effective binary bits where the residual occurred in the second case was 6 bits away from the first case instead of 8.
Examining this example in more detail:
digits 20
vpa(0.1) + vpa(0.2) - vpa(0.3)
ans = 
5.0487097934144755546e-29
digits 100
vpa(ans)
ans = 
0.0000000000000000000000000000504870979341447555463506281780983186990852118469774723052978515625
fprintf('%.100f\n',2^(-94))
0.0000000000000000000000000000504870979341447555463506281780983186990852118469774723052978515625000000
And yes, you can see that the residual value stored is in fact exactly the value of a binary bit.
Walter Roberson
Walter Roberson on 13 May 2025
If it uses the same strategy as Maple does (MuPAD was designed as a Maple work-alike) then the strategy is something like using chains of 32 bit integers, out of which only 30 bits are (normally) used. Maple does not use base 100,000,000 (a number slightly below 2^30); it uses base 2^30 if I recall correctly.
At the moment, I do not recall how the decimal place information is stored.

Sign in to comment.

More Answers (0)

Tags

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!