summaryrefslogtreecommitdiff
path: root/doc/context/sources/general/manuals/lowlevel/lowlevel-accuracy.tex
blob: 0048bb0753981564481a50914ebc533a518ee151 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
% language=us runpath=texruns:manuals/lowlevel

\environment lowlevel-style

\startdocument
  [title=accuracy,
   color=darkgray]

\startsectionlevel[title=Introduction]

When you look at \TEX\ and \METAPOST\ output the accuracy of the rendering stands
out, unless of course you do a sloppy job on design and interfere badly with the
system. Much has to do with the fact that calculations are very precise,
especially given the time when \TEX\ was written. Because \TEX\ doesn't rely on
(at that time non|-|portable) floating point calculations, it does all with 32
bit integers, except in the backend where glue calculations are used for
finalizing the glue values. It all changed a bit when we added \LUA\ because
there we mix integers and doubles but in practice it works out okay.

When looking at floating point (and posits) one can end up in discussions about
which one is better, what the flaws fo each are, etc. Here we're only interested
in the fact that posits are more accurate in the ranges where \TEX\ and
\METAPOST\ operate, as well as the fact that we only have 32 bits for floats in
\TEX, unless we patch more heavily. So, it is also very much about storage.

When you work with dimensions like points, they get converted to an integer
number (the \type {sp} unit) and from that it's just integer calculations. The
maximum dimension is \the\maxdimen, which already shows a rounding issue. Of
course when one goes precise for sure there is some loss, but on the average
we're okay. So, in the next example the two last rows are equivalent:

\starttabulate[|Tr|r|r|]
\NC .1pt        \NC \the\dimexpr.1pt     \relax  \NC \number\dimexpr.1pt     \relax sp \NC \NR
\NC .2pt        \NC \the\dimexpr.2pt     \relax  \NC \number\dimexpr.2pt     \relax sp \NC \NR
\NC .3pt        \NC \the\dimexpr.3pt     \relax  \NC \number\dimexpr.3pt     \relax sp \NC \NR
\NC .1pt + .2pt \NC \the\dimexpr.1pt+.2pt\relax  \NC \number\dimexpr.1pt+.2pt\relax sp \NC \NR
\stoptabulate

When we're at the \LUA\ end things are different, there numbers are mapped onto
64 bit floating point variables (doubles) and not all numbers map well. This is
what we get when we work with doubles in \LUA:

\starttabulate[|Tr|r|]
\NC .1      \NC \luaexpr{.1   }  \NC \NR
\NC .2      \NC \luaexpr{.2   }  \NC \NR
\NC .3      \NC \luaexpr{.3   }  \NC \NR
\NC .1 + .2 \NC \luaexpr{.1+.2}  \NC \NR
\stoptabulate

The serialization looks as if all is okay but when we test for equality there
is a problem:

\starttabulate[|Tr|l|]
\NC      .3 == .3 \NC \luaexpr{tostring(     .3 == .3)} \NC \NR
\NC .1 + .2 == .3 \NC \luaexpr{tostring(.1 + .2 == .3)} \NC \NR
\stoptabulate

This means that a test like this can give false positives or negatives unless one
tests the difference against the accuracy (in \METAPOST\ we have the {eps}
variable for that). In \TEX\ clipping of the decimal fraction influences equality.

\starttabulate[|Tr|l|]
\NC \type{\iflua {      .3 == .3 } Y\else N\fi} \NC \iflua{     .3 == .3} equal\else different\fi \NC \NR
\NC \type{\iflua { .1 + .2 == .3 } Y\else N\fi} \NC \iflua{.1 + .2 == .3} equal\else different\fi \NC \NR
\stoptabulate

The serialization above misguides us because the number of digits displayed is
limited. Actually, when we would compare serialized strings the equality holds,
definitely within the accuracy of \TEX. But here is reality:

\startluacode
    local a = 0.1
    local b = 0.2
    local c = 0.3
    local d = a + b
    local NC, NR = context.NC, context.NR
    local function show(f)
        context.NC() context(context.escaped(f))
        context.NC() context(f,c)
        context.NC() context(f,d)
        context.NC() context.NR()
    end
    context.starttabulate { "|T|l|l|" }
        context.NC()
        context.NC() context(".3")
        context.NC() context(".1 + .2")
        context.NC() context.NR()
     -- show("%0.05g",c)
        show("%0.10g",c)
        show("%0.17g",c)
        show("%0.20g",c)
        show("%0.25g",c)
    context.stoptabulate()
\stopluacode

The above examples use $0.1$, $0.2$ and $0.3$ and on a 32 bit float that actually
works out okay, but \LUAMETATEX\ is 64 bit. Is this really important in practice?
There are indeed cases where we are bitten by this. At the \LUA\ end we seldom
test for equality on calculated values but it might impact check for less or
greater then. At the \TEX\ end there are a few cases where we have issues but
these also relate to the limited precision. It is not uncommon to loose a few
scaled points so that has to be taken into account then. So how can we deal with
this? In the next section(s) an alternative approach is discussed. It is not so
much the solution for all problems but who knows.

\stopsectionlevel

\startsectionlevel[title=Posits]

% TODO: don't check for sign (1+2)

The next table shows the same as what we started with but with a different
serialization.

\starttabulate[|Tr|r|]
\NC .1      \NC \positunum{.1     }  \NC \NR
\NC .2      \NC \positunum{.2     }  \NC \NR
\NC .3      \NC \positunum{.3     }  \NC \NR
\NC .1 + .2 \NC \positunum{.1 + .2}  \NC \NR
\stoptabulate

And here we get equality in both cases:

\starttabulate[|Tr|l|]
\NC      .3 == .3 \NC \positunum{     .3 == .3} \NC \NR
\NC .1 + .2 == .3 \NC \positunum{.1 + .2 == .3} \NC \NR
\stoptabulate

The next table shows what we actually are dealing with. The \type {\if}|-|test is
not a primitive but provided by \CONTEXT.

\starttabulate[|Tr|l|]
\NC \type{\ifpositunum {      .3 == .3 } Y\else N\fi} \NC \ifpositunum{     .3 == .3} equal\else different\fi \NC \NR
\NC \type{\ifpositunum { .1 + .2 == .3 } Y\else N\fi} \NC \ifpositunum{.1 + .2 == .3} equal\else different\fi \NC \NR
\stoptabulate

And what happens when we do more complex calculations:

\starttabulate[|Tr|l|]
\NC \type {math .sin(0.1 + 0.2) == math .sin(0.3)} \NC \luaexpr{tostring(math.sin(0.1 + 0.2) == math.sin(0.3))} \NC \NR
\NC \type {posit.sin(0.1 + 0.2) == posit.sin(0.3)} \NC             \positunum{sin(0.1 + 0.2) == sin(0.3)}       \NC \NR
\stoptabulate

Of course other numbers might work out differently! I just took the simple tests
that came to mind.

So what are these posits? Here it's enough to know that they are a different way
to store numbers with fractions. They still can loose precision but a bit less on
smaller values and often we have relative small values in \TEX. Here are some links:

\starttyping
https://www.johngustafson.net/pdfs/BeatingFloatingPoint.pdf
https://posithub.org/conga/2019/docs/14/1130-FlorentDeDinechin.pdf
\stoptyping

There are better explanations out there than I can provide (if at all). When I
first read about these unums (a review of the 2015 book \quotation {The End of
Error Unum Computing}) I was intrigued and when in 2023 I read something about it
in relation to RISCV I decided to just add this playground for the users. After
all we also have decimal support. And interval based solutions might actually be
good for \METAPOST, so that is why we have it as extra number model. There we
need to keep in mind that \METAPOST\ in non scaled models also apply some of the
range checking and clipping that happens in scaled (these magick 4096 tricks).

For now it is enough to know that it's an alternative for floats that {\em could}
work better in some cases but not all. The presentation mentioned above gives
some examples of physics constants where 32 posits are not good enough for
encoding the extremely large or small constants, but for $\pi$ it's all fine.
\footnote {Are 64 bit posits actually being worked on in softposit? There are
some commented sections. We also need to patch some unions to make it compile as
C.} In double mode we actually have quite good precision compared to 32 bit
posits but with 32 bit floats we gain some. Very small numbers and very large
numbers are less precise, but around 1 we gain: the next value after 1 is
1.0000001 for a float and 1.000000008 for a posit (both 32 bit). So, currently
for \METAPOST\ there is no real gain but if we'd add posits to \TEX\ we could
gain some because there a halfword (used for storing data) is 32 bit.

But how about \TEX ? Per April 2023 the \LUAMETATEX\ engine has native support
for floats (this in addition to \LUA\ based floats that we already had in
\CONTEXT). How that works can be demonstrated with some examples. The float
related commands are similar to those for numbers and dimensions: \typ
{\floatdef}, \typ {\float}, \typ {\floatexpr}, \typ {\iffloat}, \typ
{\ifzerofloat} and \typ {\ifintervalfloat}. That means that we also have them as
registers. The \typ {\positdef} primitive is similar to \typ {\dimensiondef}.
When a float (posit) is seen in a dimension context it will be interpreted as
points, and in an integer context it will be a rounded number. As with other
registers we have a \typ {\newfloat} macro. The \typ {\advance}, \typ
{\multiply} and \typ {\divide} primitives accept floats.

\startbuffer[reset]
\scratchdimen=1.23456pt
\scratchfloat=1.23456
\stopbuffer

\typebuffer[reset] \getbuffer[reset]

We now use these two variables in an example:

\startbuffer[dimensions]
\setbox0\hbox to \scratchdimen {x}\the\wd0
\scratchdimen \dimexpr \scratchdimen * 2\relax
\setbox0\hbox to \scratchdimen {x}\the\wd0
\advance \scratchdimen \scratchdimen
\setbox0\hbox to \scratchdimen {x}\the\wd0
\multiply\scratchdimen by 2
\setbox0\hbox to \scratchdimen {x}\the\wd0
\stopbuffer

\typebuffer[dimensions] \startlines\darkblue\tttf\getbuffer[reset,dimensions]\stoplines

When we use floats we get this:

\startbuffer[floats]
\setbox0\hbox to \scratchfloat {x}\the\wd0
\scratchfloat \floatexpr \scratchfloat * 2\relax
\setbox0\hbox to \scratchfloat {x}\the\wd0
\advance \scratchfloat \scratchfloat
\setbox0\hbox to \scratchfloat {x}\the\wd0
\multiply\scratchfloat by 2
\setbox0\hbox to \scratchfloat {x}\the\wd0
\stopbuffer

\typebuffer[floats] \startlines\darkblue\tttf\getbuffer[reset,floats]\stoplines

So which approach is more accurate? At first sight you might think that the
dimensions are better because in the last two lines they indeed duplicate.
However, the next example shows that with dimensions we lost some between steps.

\startbuffer[noboxes]
                                                 \the\scratchfloat
\scratchfloat \floatexpr \scratchfloat * 2\relax \the\scratchfloat
\advance \scratchfloat \scratchfloat             \the\scratchfloat
\multiply\scratchfloat by 2                      \the\scratchfloat
\stopbuffer

\typebuffer[noboxes] \startlines\darkblue\tttf\getbuffer[reset,noboxes]\stoplines

One problem with accuracy is that it can build up. So when one eventually does
some comparison the expectations can be wrong.

\startbuffer
\dimen0=1.2345pt
\dimen2=1.2345pt

\ifdim           \dimen0=\dimen2 S\else D\fi \space +0sp: [dim]
\ifintervaldim0sp\dimen0 \dimen2 O\else D\fi \space +0sp: [0sp]

\advance\dimen2 1sp

\ifdim             \dimen0=\dimen2 S\else D\fi \space +1sp: [dim]
\ifintervaldim 1sp \dimen0 \dimen2 O\else D\fi \space +1sp: [1sp]
\ifintervaldim 1sp \dimen2 \dimen0 O\else D\fi \space +1sp: [1sp]
\ifintervaldim 2sp \dimen0 \dimen2 O\else D\fi \space +1sp: [2sp]
\ifintervaldim 2sp \dimen2 \dimen0 O\else D\fi \space +1sp: [2sp]

\advance\dimen2 1sp

\ifintervaldim 1sp \dimen0\dimen2 O\else D\fi \space +2sp: [1sp]
\ifintervaldim 1sp \dimen2\dimen0 O\else D\fi \space +2sp: [1sp]
\ifintervaldim 5sp \dimen0\dimen2 O\else D\fi \space +2sp: [5sp]
\ifintervaldim 5sp \dimen2\dimen0 O\else D\fi \space +2sp: [5sp]
\stopbuffer

\typebuffer

Here we show a test for overlap in values, the same can be done with integer
numbers (counts) and floats. This interval checking is an experiment and we'll
see it if gets used.

\startpacked\darkblue \tttf \getbuffer \stoppacked

There are also \typ {\ifintervalfloat} and \typ{\ifintervalnum}. Because I have
worked around these few scaled point rounding issues for decades, it might
actually take some time before we see the interval tests being used in \CONTEXT.
After all, there is no reason to touch somewhat tricky mechanism without reason
(read: users complaining).

To come back to posits, just to be clear, we use 32 bit posits and not 32 bit
floats, which we could have but that way we gain some accuracy because less bits
are used by default for the exponential.

In \CONTEXT\ we also provide a bunch of pseudo primitives. These take one float:
\type {\pfsin}, \type {\pfcos}, \type {\pftan}, \type {\pfasin}, \type {\pfacos},
\type {\pfatan}, \type {\pfsinh}, \type {\pfcosh}, \type {\pftanh}, \type
{\pfasinh}, \type {\pfacosh}, \type {\pfatanh}, \type {\pfsqrt}, \type {\pflog},
\type {\pfexp}, \type {\pfceil}, \type {\pffloor}, \type {\pfround}, \type
{\pfabs}, \type {\pfrad} and \type {\pfdeg}, whiel these expect two floats: \type
{\pfatantwo}, \type {\pfpow}, \type {\pfmod} and \type {\pfrem}.

% \pageextragoal = 5sp

\stopsectionlevel

\startsectionlevel[title=\METAPOST]

In addition to the instances \typ {metafun} (double in \LMTX), \typ {scaledfun},
\typ {doublefun}, \typ {decimalfun} we now also have \typ {positfun}. Because we
currently use 32 bit posits in the new number system there is no real gain over
the already present 64 bit doubles. When 64 bit posits show up we might move on
to that.

\stopsectionlevel

\stopdocument