Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download

lab2

136 views
Kernel: Python 3 (Anaconda)

Lab 2: Errors

In Lab 1, we discussed the represenation of binary floating point numbers. One of the key results we discussed was the fact that there are only a finite number of bits, usually 64, used to store a floating point number.

Today, we're going to explore the often surprising consequences of this fact. First, we'll recall how digits are stored: in an IEEE single precision (32 bit) float, a number is represented as

(1)s1.f2eB,(-1)^s 1.f 2^{e-B},

where the bias B=127B = 127. For example,

sef
01000001001000000000000000000000

What decimal number does this represent? Work it out on paper!

Adding Floating point numbers

To begin to see how error creeps in just from our finite floating point representation, let's consider adding two numbers in regular, decimal scientific notation with bias B=0B= 0. If we had say 7+3×1057 + 3 \times 10^{-5}, we can easily see the answer is 7.000037.00003. If we use double precision numbers (as python does by default), this poses no challenge to the computer either:

print(7+3e-5)
7.00003
x = '01000000000000000000000' print(len(x)) y = 2**21 print(y) print(2**7) print((1.2097152)*(2**3))
23 2097152 128 9.6777216

However, let's consider doing the same thing, but now we'll assume we have only 4 digits of precision in the mantissa of our numbers. That gives us 7.0007.000 and 3.000×1053.000 \times 10^{-5}. So far, so good. Floating point allows us to represent both of these quantities with the same number of digits of precision.

When we go to add them, the computer will do that by shifting the smaller number such that it has the same exponent of as the larger number, in this case 10010^0. Remember, we can represent the same number many ways--but we must keep only 4 digits of precision:

3.000×105=0.300×104=0.030×103=0.003×1023.000 \times 10^{-5} = 0.300 \times 10^{-4} = 0.030 \times 10^{-3} = 0.003 \times 10^{-2}

You can see where this is going. We shift one more time, and we end up with 0.000×1010.000 \times 10^{-1}. This is not good, since this is zero, and we still have one more shift to do! We end up with

7.000×100+0.000×1007.000 \times 10^0 + 0.000 \times 10^0 = 7.000×100,7.000 \times 10^0, which is clearly the wrong answer.

Machine Precision

This leads us to a very important definition: there exists a number ϵm\epsilon_m such that 1c+ϵm=1c1_c + \epsilon_m = 1_c, where that subscript cc means "as represented on the computer" rather than "the mathematical object 1".

Let's find an approximation for ϵm\epsilon_m experimentally. In doing so, we're going to meet another kind of loop, called the while loop. A while loop tests an expression at the top, just like an if statement. If the expression is true, then the loop body executes. If it is false, the loop stops. Be careful! While loops may never stop! This is sometimes what you want, but often not. You must make sure that the expression at the top changes!

hint

If your while loop seems to never end, you can go to the Kernel menu in the Jupyter notebook and choose Interrupt Kernel. That should cause your code to stop and an error message to be printed. You can then fix your loop and restart. Any calculation like these ones should be more or less instantaneous. Later in the class, we'll have calculations that will take longer.

eps = 1. test = 1. + eps print(test) while test != 1.: # so this loop will run as long as test+eps is not exactly equal to one. eps = eps/2. # <---- here we *change* eps! test = 1.+eps # <---- here we *change* test! print(eps)
2.0 1.1102230246251565e-16

So what does this code do? How does it find epsilon?

That expression above was using standard python floating point numbers, which are double precision. We can use numpy to create single precision numbers. Here's how:

import numpy as np eps = np.float32(1.) test = np.float32(1.) + eps while test != np.float32(1.): eps = eps/np.float32(2) test = np.float32(1.)+eps print(eps)
5.96046e-08

Note we have to be careful! Every number in python is double precision unless you say otherwise. Also, python is smart enough to automatically "promote" a variable like eps to double precision if you add, subtract, multiply, or divide by something of double precision. That's why I've carefully set all numbers to be np.float32s in the above cell.

Questions

What is the difference in ϵm\epsilon_m between single and double precision? Try to explain what is going on in terms of what you've learned about floating point numbers, comparing them to numbers written in scientific notation with a limited precision.

Subtractive cancellation

By hand, subtract 5.3245.324 from 5.3215.321. How many digits of precision did you start with? How many did you end with? What does that tell you about your new answer? Think about these questions for a minute before proceeding.

This is called subtractive cancellation: you are cancelling off all of your more precise digits, and leaving only the least precise one (in this case). The subtraction you compute may be exact itself, but if there is any error in the starting values (5.3245.324 and 5.3215.321) it becomes amplified. The technical term for this is "bad."

Example 1

Calculate y=x+1xy = \sqrt{x + 1} - \sqrt{x} for x=10ix = 10^i with i=1,2,3,,20i = 1,2,3,\ldots,20. Be careful: you want xx to be a float, not an int. To do that make sure you use 10.**i Store the answers in a numpy array called y1.

y1 = [] x = [] import numpy as np for i in range (1,21): x1 = 10.**i x.append(x1) ans = (np.sqrt (x1 + 1. ) - np.sqrt(x1)) y1.append(ans) print(y1, x)
[0.15434713018702029, 0.049875621120889946, 0.015807437428957627, 0.0049998750062485442, 0.0015811348772558631, 0.00049999987504634191, 0.00015811387902431306, 5.0000000555883162e-05, 1.5811390767339617e-05, 4.9999944167211652e-06, 1.5811529010534286e-06, 5.0000380724668503e-07, 1.5785917639732361e-07, 5.029141902923584e-08, 1.862645149230957e-08, 0.0, 0.0, 0.0, 0.0, 0.0] [10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 10000000.0, 100000000.0, 1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0, 10000000000000.0, 100000000000000.0, 1000000000000000.0, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20]

Now do the same thing, but get rid of the subtraction. Multiply yy by

x+1+xx+1+x\frac{\sqrt{x + 1} + \sqrt{x}}{\sqrt{x + 1} + \sqrt{x}}

You should get a new expression for yy with no subtractions. Now, using your new formulation, calculate yy again, and store it in a new numpy array y2.

y2 = [] x = [] for i in range (1,21): x1 = 10.**i x.append(x1) y1 = (np.sqrt (x1 + 1 ) - np.sqrt(x1)) answer = y1*((np.sqrt ((x1) + 1 ) + np.sqrt(x1)))/(np.sqrt ((x1) + 1 ) + np.sqrt(x1)) y2.append(answer) print(y2, x)
[0.15434713018702029, 0.049875621120889946, 0.015807437428957627, 0.0049998750062485442, 0.0015811348772558631, 0.00049999987504634191, 0.00015811387902431306, 5.0000000555883162e-05, 1.5811390767339617e-05, 4.9999944167211652e-06, 1.5811529010534284e-06, 5.0000380724668503e-07, 1.5785917639732361e-07, 5.029141902923584e-08, 1.862645149230957e-08, 0.0, 0.0, 0.0, 0.0, 0.0] [10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 10000000.0, 100000000.0, 1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0, 10000000000000.0, 100000000000000.0, 1000000000000000.0, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20]

Which one of these is more accurate? If it's not obvious, you probably made a coding mistake.

The less accurate one is called unstable. The errors caused by subtractive cancellation grow as you move to certain values of xx, in this case, large values.

Now using pyplot, plot the answer with subtraction y1 and the answer without subtraction y2 as a function of x. Code to do this is in the cell below. Look at the code and see the sytle of the plot.

%matplotlib inline import matplotlib.pyplot as plt plt.style.use('ggplot')
plt.semilogx(x,y1, label='with subtraction') plt.semilogy(x,y1, label='with subtraction') plt.loglog(x,y1, label='with subtraction') plt.semilogx(x,y2, 'o', label='without subtraction') plt.semilogy(x,y2, 'o', label='without subtraction') plt.loglog(x,y2, 'o', label='without subtraction') plt.xlabel("x") plt.ylabel("$\sqrt{x+1} - \sqrt{x}$") plt.legend()
<matplotlib.legend.Legend at 0x7fe290fec588>
Image in a Jupyter notebook

What's wrong with this plot? one thig to note is that the xx axis clearly varies by a lot. Using a logarithmic plot is helpful here. Matplotlib allows log plots on the x, y, or both axes easily:

plt.semilogx(x,y) plt.semilogy(x,y) plt.loglog(x,y)

But even if you try that you might still not be able to see a clear difference between them. How could you fix this? hint: it's something you learned in 107, if not before that

Fix it, and show a plot that displays how much worse one solution is than the other. Make sure to label all axes on the plot!

Example 2

You need to compute the following

y=sinθ1sin2θy = \frac{\sin \theta}{\sqrt{1 - \sin^2 \theta}}

for θπ/2\theta \to \pi/2.

Your first thought is to simply solve the equation for a set of θ\theta, something like θ=[π/2nϵ,π/2(n1)ϵ,π/2(n2)ϵ,π/2ϵ]\theta = [\pi/2 - n \epsilon, \pi/2 - (n-1) \epsilon,\pi/2 - (n-2) \epsilon\ldots,\pi/2-\epsilon], where ϵ<1\epsilon < 1 and n200n \simeq 200. Don't use θ=π/2\theta = \pi/2! Ask yourself why not before continuing.

First, set up your θ\theta variable as a numpy array called theta. Make sure the values get closer to θ\theta. Try experimenting with different values of ϵ<1\epsilon < 1.

import numpy as np from math import pi arr = [] n=200 for i in range(1,50): arr.append(-1 *((pi/2) -n*(i/100))) print(len(arr))
49

Now that you have theta, compute yy using the above formula.

import numpy as np length_array = len(arr) for i in range(0, length_array): x = ((np.sin(arr[i]))/np.sqrt(1. - (np.sin(arr[i]))**2)) print(x)
0.45765755436 0.863691154451 -3.43635300418 0.147065063949 1.54235104536 -1.5726734064 -0.13803371984 3.32632319564 -0.879264875779 -0.446995108949 112.973210356 -0.468406738868 -0.848353751666 3.55328644813 -0.156119952162 -1.51284547681 1.60385190641 0.129024418279 -3.22258738833 0.895082917638 0.436416706075 -56.4821793502 0.479245430495 0.833244979427 -3.67781445085 0.16519990127 1.4841196952 -1.63592842178 -0.120035671925 3.12460562224 -0.911153612881 -0.425919717661 37.6498684413 -0.490176471849 -0.818357447865 3.81072324371 -0.174306444354 -1.45613886367 1.66894756629 0.111066006643 -3.03189793258 0.927485643181 0.415501581251 -28.2322373255 0.50120278338 0.803684050313 -3.95291138957 0.183441131845 1.42887015672

Now, use your knowledge of trigonometry to find an equivalent form for yy. Hint: it's a single trig function

numpy has that function built-in. Run it on your theta array, and print the difference between your computed y and the answer given by the numpy function. This is called the "absolute error", EabsxtruexcalcE_{abs} \equiv |x_{true} - x_{calc}|.

import numpy as np y = np.tan(arr) print(y)
[ 4.57657554e-01 -8.63691154e-01 3.43635300e+00 1.47065064e-01 -1.54235105e+00 1.57267341e+00 -1.38033720e-01 -3.32632320e+00 8.79264876e-01 -4.46995109e-01 -1.12973210e+02 4.68406739e-01 -8.48353752e-01 3.55328645e+00 1.56119952e-01 -1.51284548e+00 1.60385191e+00 -1.29024418e-01 -3.22258739e+00 8.95082918e-01 -4.36416706e-01 -5.64821794e+01 4.79245430e-01 -8.33244979e-01 3.67781445e+00 1.65199901e-01 -1.48411970e+00 1.63592842e+00 -1.20035672e-01 -3.12460562e+00 9.11153613e-01 -4.25919718e-01 -3.76498684e+01 4.90176472e-01 -8.18357448e-01 3.81072324e+00 1.74306444e-01 -1.45613886e+00 1.66894757e+00 -1.11066007e-01 -3.03189793e+00 9.27485643e-01 -4.15501581e-01 -2.82322373e+01 5.01202783e-01 -8.03684050e-01 3.95291139e+00 1.83441132e-01 -1.42887016e+00]

Now compute the relative error (you may have called this the "percent error" before). Erelxtruexcalc/xtrueE_{rel} \equiv |x_{true} - x_{calc}|/x_{true}.

Finally, plot the relative error as a function of θ\theta.

import numpy as np e_rel = np.absolute(x - y)/x print(e_rel)
[ 0.67970669 1.60445741 1.40494421 0.89707598 2.07942001 0.10064123 1.0966034 3.32793944 0.38464326 1.31283116 80.06471405 0.67218383 1.59372347 1.4867805 0.89073888 2.05877043 0.12246162 1.09029821 3.25533956 0.37357295 1.30542783 40.52925959 0.66459833 1.58314954 1.57393188 0.88438425 2.03866659 0.14491048 1.0840074 3.18676666 0.36232581 1.29808147 27.34939799 0.65694821 1.57273045 1.66694859 0.878011 2.0190841 0.16801905 1.07772995 3.12188485 0.35089578 1.2907903 20.75843445 0.6492314 1.56246122 1.76645948 0.87161805 2. ]

Very important note: The true solution is often unknown, and in any real problem is totally unknown. If you knew the true solution, you wouldn't be bothering to run a computer simulation! It's very important for you to remember that the built in numpy function you just calculated is also an approximation, but in this case, it's a much, much better approximation.

Every computational answer is wrong, but some wrong answers are useful!

%matplotlib inline import matplotlib.pyplot as plt plt.style.use('ggplot') plt.plot(arr, e_rel)
[<matplotlib.lines.Line2D at 0x7fd07cf46a20>]
Image in a Jupyter notebook