PDA

View Full Version : aargh... floats are killing me




MrFusion
Mar 22, 2008, 01:54 PM
Actually it's the limitations in representing numbers.
I am working on a custom NSView and drawRect function. Cocoa, in particular the NSBezierPath only cares about floats up to a few digits after the decimal point.

At some point in the drawing, I calculate a set of positions for my path - based on the previous positions - and compare them to some predefined maximum value.
e.g.
0.1 < 1.5
0.1+0.2 < 1.5

Fine, floats can handle that I thought.

---
<edit>:
Safari, damn it. Apple-] is for indenting, not for posting half baked questions.
(see other post for the actual question)



lee1210
Mar 22, 2008, 02:22 PM
You might want to post a code snippet where that sort of code is failing.

I at least believe something is failing, you didn't ask a question. You shouldn't need to parenthesize those kind of statements as + has a higher precedence than <. Since i don't know what the particular case is you are working on, i made sure that essentially that exact code worked as expected, but with variables rather than literals. I'm not sure what compiler you're working with and even if I did I may not be able to quickly find documentation on whether or not real number literals are parsed as doubles or floats. If doubles, and you are comparing a literal to some float variables that could be causing some strange issue, but it really shouldn't. Things should be implicitly casted as needed, but you might want to throw explicit casts to float in to be positive.

Again, feel free to toss some specific code up and I'm sure someone can point you in the right direction.

-Lee

MrFusion
Mar 22, 2008, 02:24 PM
I am working on a custom NSView and drawRect function. Cocoa, in particular the NSBezierPath only cares about floats up to a few digits after the decimal point.

At some point in the drawing, I calculate a set of positions for my path - based on the previous positions - and compare them to some predefined maximum value.
The problem is that e.g. 0.1 is not represented as 0.1 but as 0.100000001. Since small errors keep adding up, my end value is slightly over the top. e.g. 0.800000234 <= 0.8 should be yes for me.

Such accuracy is ridiculously for NSBezierPath's and I also don't need it.
I tried encapsulating it in NSNumber and using compare: to no avail.
I tried roundf(100*value) <= roundf(100*maxValue) to no avail. Even if the representation is not accurately, it should be the same representation.

Now I appreciate the comment from my numerical analysis teacher whom told the class to stay away from programming our own numerical software. No, I didn't do CS and usually I am already happy if my experiments and data give me the order of magnitude of some measured or calculated variable.

Any help or advice for this idiot is welcome. :)

MDMstudios
Mar 22, 2008, 02:45 PM
Is there a way you can use a double value instead of a float value?

MrFusion
Mar 22, 2008, 02:52 PM
Is there a way you can use a double value instead of a float value?

I probably could, but double also has the same problem, it's just less of an error percentage wise.
Instead of 0.1000001 you have 0.1000000000000001 with double.
Besides NSBezierpath's expect float values.
Comparing two floats for equality can be done by subtracting them and checking if they are smaller than some small number e.g. fabsf(a-b)< c
That much I know.

MDMstudios
Mar 22, 2008, 02:55 PM
I probably could, but double also has the same problem, it's just less of an error percentage wise.
Instead of 0.1000001 you have 0.1000000000000001 with double.
Besides NSBezierpath's expect float values.
Comparing two floats for equality can be done by subtracting them and checking if they are smaller than some small number e.g. fabsf(a-b)< c
That much I know.
Hmm, is there some type of method you can use for rounding?

MrFusion
Mar 22, 2008, 03:09 PM
hmm, is there some type of method you can use for rounding?

roundf()
I mentioned it in my original post, but that also doesn't work well.

MDMstudios
Mar 22, 2008, 03:13 PM
Is it possible you could make a method like this?

int x;
float y;

x = y * 10;
y = x * .1;

I have used a method like this a few times, and over all it seems like it works pretty good.

MrFusion
Mar 22, 2008, 03:22 PM
Is it possible you could make a method like this?

int x;
float y;

x = y;
y = x * .1;

I have used a method like this a few times, and over all it seems like it works pretty good.

int x1 = 100*y;
int x2 = 100*max;
if (x1 <= x2) {;}

I could do this in my code, I think, but is it safe to just cast a float as an int? I'll play around with this.

Thanks for the comments, MDMstudios, it helps.

MDMstudios
Mar 22, 2008, 03:26 PM
int x1 = 100*y;
int x2 = 100*max;
if (x1 <= x2) {;}

I could do this in my code, I think, but is it safe to just cast a float as an int? I'll play around with this.

Thanks for the comments, MDMstudios, it helps.

I think it should be safe, and if it puts up a error or warning you could allways use the method
[y intValue];
[x floatValue];

Glad you found my comments helpful.

lee1210
Mar 22, 2008, 03:30 PM
this may work better than rounding, per se.
multiply the float by 10^# digits that matter to the right of the decimal. Add .5. This helps in the case that the in-memory representation is a little less rather than a little more than the exact value. Call floor on the result, and cast the result of floor to an int, and assign to a temp int. Repeat for all of the values, then compare to 10^# sig dig to the right of the decimal times the limit. Then do an integer <. Use a long or unsigned int if needed for the temp values.

-lee

Ps I'd write some code, but am on my phone. Hopefully the above is good enough to get you started.

.. Too slow. The above should work.

gnasher729
Mar 22, 2008, 03:46 PM
If you write a loop like

for (x = 0.0; x <= 1.5; x += 0.1) ...

you can never be sure how often it will be executed, because eventually x will have a value that is very close to 1.5, but it could be a tiny bit less, or a tiny bit more. Using double or long double instead of float won't help, x will be a lot closer to 1.5, but it will still be a tiny bit less or a tiny bit more. The solution is very simple: Use an integer variable.

int i;
double x;

for (i = 0; i <= 15; ++i) { x = i * 0.1; ... }

MrFusion
Mar 22, 2008, 04:02 PM
You might want to post a code snippet where that sort of code is failing.
Again, feel free to toss some specific code up and I'm sure someone can point you in the right direction.

-Lee

Sure, here is some of my 1000+ lines of spaghetti code (of version 6 or so). I am not sure I am happy with this version, too much cruft because of these floating point errors.

The idea is to have a NSView that can:
plot any number of arbitrary 2D xy-data,
plot any number of arbitrary 2D functions,
plot linear or logaritmic scales
scale the data (e.i. different units: Hz, kHz, mHz)
plot not only lines between two points, but also have a symbol (circle, square, x,+,...) at that point
Plot the legend with the correct label, symbol, line thickness, color
track the mouse position, and display it on screen
with as many user adjustable options as reasonable possible
zoom in

These are things that in some way or another are working.

Is not possible:
arbitrary positioning of title, labels, legend
editing title, labels, legend directly on the PlotView

The next steps is to include:
converting mouse clicks to marked points

A subclass could be
2D colorplots or barplots

I am not going to do 3D plots. Such plots are also usually pretty horrendous.

There will also be an inspector, while most responsibility of the PlotView is offloaded to a delegate and datasource in such a way that the user doesn't have to see the PlotView code. Like an NSTableView works.

Yes, I know a few other frameworks are out there, but these frameworks don't seem to be orientated towards scientific plots, and besides it's fun to do and I am learning a lot about software design.
When I am either happy or bored with it, maybe I put it in a repository.

Yes, I know there is also exell, numbers, origin, gnuplot, octave, (R, mathematica,) which I use for my real data.


-(void) drawYAxis {
if ([delegate showYAxis]){
//get/set (path) attributes
NSBezierPath *path = [NSBezierPath bezierPath];
[path setLineWidth:[delegate borderThickness]];
[[delegate borderColor] set];
float ticLength = [delegate axisLength];

//minimum font & rect size (ensure enough spacing between labels)
NSSize minimumsize = [self minimumLabelsize];
NSRect rect = NSMakeRect(0,
plotarea.origin.y-3*minimumsize.height,
0,
minimumsize.height);
if (yRange.type == 1){
//linear
//starting point
float minCoordStart = fmaxf(nearbyintf(yRange.min),ceilf(yRange.min));
//draw
float y;
float yCoord = minCoordStart;
while (roundf(100*yCoord) <= roundf(100*yRange.max)){
//convert
y = [self transformY:yCoord
to:ATRPlotcoordinates];
//path
[path moveToPoint:NSMakePoint(plotarea.origin.x,y)];
[path lineToPoint:NSMakePoint(plotarea.origin.x+ticLength/2,y)];
//text
NSAttributedString *str = [self allocLabel:yCoord
forAxis:yaxis];//DO NOT FORGET TO RELEASE str
if (roundf(100*y) > roundf(100*(rect.origin.y+rect.size.height+minimumsize.height))) {
//enough clearning in relation to previous string
//update rect
NSRect newRect;
newRect.origin.x = plotarea.origin.x-padding-[str size].width;
newRect.origin.y = y-[str size].height/2;
newRect.size.width = [str size].width;
newRect.size.height = [str size].height+minimumsize.height;
if ([self rectFallsWithinCanvasarea:newRect]) {
//rect falls within canvasarea, will be displayed completely
newRect.size.height -= minimumsize.height;
[str drawInRect:newRect];
rect = newRect;
[path lineToPoint:NSMakePoint(plotarea.origin.x+ticLength,y)]; //full tic length for labeled tics
}
}
[str release];
yCoord+=yRange.interval;
}

} else if (yRange.type == 2){
//log
//starting point
float minCoordStart = yRange.min;

//magnitude -> interval
float y;
int magnitude = [self orderOfMagnitude:minCoordStart];
float interval=powf(10,magnitude);
//startvalue
float yCoord = interval;
int i = 1;
while (yCoord < minCoordStart){
yCoord +=interval;
i++;
}

while (roundf(100*yCoord) <= roundf(100*yRange.max)){
//convert
y = [self transformY:yCoord
to:ATRPlotcoordinates];
//path
[path moveToPoint:NSMakePoint(plotarea.origin.x,y)];
if (i == 1)
[path lineToPoint:NSMakePoint(plotarea.origin.x+ticLength,y)];
else
[path lineToPoint:NSMakePoint(plotarea.origin.x+ticLength/2,y)];
//text
if (i == 1 | i == 5){
//text
NSAttributedString *str = [self allocLabel:yCoord
forAxis:yaxis];
if (roundf(100*y) > roundf(100*(rect.origin.y+rect.size.height+minimumsize.height))) {
//enough clearning in relation to previous string
//update rect
NSRect newRect;
newRect.origin.x = plotarea.origin.x-padding-[str size].width;
newRect.origin.y = y - [str size].height/2;
newRect.size.width = [str size].width;
newRect.size.height = [str size].height + minimumsize.height;
if ([self rectFallsWithinCanvasarea:newRect]) {
//rect falls within canvasarea
newRect.size.height -= minimumsize.height;
[str drawInRect:newRect];
rect = newRect;
}
}
[str release];
}
//adjust interval as appropiate
i++;
yCoord += interval;
if (i > 9) {
i=1;
interval = 10*interval;
}
}
}
[path stroke];
}
}

-(BOOL) rectFallsWithinCanvasarea:(NSRect) rect {
//is rect located in the total available area?
//top and right side fall within available area (origin + size)
if (roundf(100*(rect.origin.x+rect.size.width)) < roundf(100*canvasarea.origin.x))
return NO; //todo: is view origin not always (0,0)? -> just check for sign?
if (roundf(100*(rect.origin.y+rect.size.height)) < roundf(100*canvasarea.origin.y))
return NO;
if (roundf(100*(rect.origin.x+rect.size.width)) > roundf(100*(canvasarea.origin.x+canvasarea.size.width)))
return NO;
if (roundf(100*(rect.origin.y+rect.size.height)) > roundf(100*(canvasarea.origin.y+canvasarea.size.height)))
return NO;
//does origin fall within canvasarea
return [self pointFallsWithinCanvasarea:rect.origin];
}
-(int) orderOfMagnitude:(float)value {
int magnitude = 0;
if (value < 1) {
while (value < 1){
magnitude--;
value = value*10;
}
} else {
float integralPart;
modff(value,&integralPart);
div_t breuk = div(integralPart,10);
while (breuk.quot > 0){
magnitude++;
breuk = div(breuk.quot,10);
}
}
return magnitude;
}

-(void) prepareScalefactors {
//this should not take scaling into account. Scaling is only for datapoints, this fct is for all points
//x scale
if (xRange.type == ATRLinear)
ratio.x = plotarea.size.width/(xRange.max-xRange.min); //linear
else if (xRange.type == ATRLogaritmic)
ratio.x = plotarea.size.width/(log10f(xRange.max)-log10f(xRange.min)); //log
//y scale
if (yRange.type == ATRLinear)
ratio.y = plotarea.size.height/(yRange.max-yRange.min); //linear
else if (yRange.type == ATRLogaritmic)
ratio.y = plotarea.size.height/(log10f(yRange.max)-log10f(yRange.min)); //log
}

lazydog
Mar 22, 2008, 04:15 PM
If you want to check that x <= y then how about something like this:-


#define EPSILON ( 0.0001f )

float x, y ;


if ( ( x - y ) < EPSILON )
{

}


You would need to choose a suitable EPSILON though.

b e n

MrFusion
Mar 22, 2008, 04:28 PM
If you write a loop like

for (x = 0.0; x <= 1.5; x += 0.1) ...

you can never be sure how often it will be executed, because eventually x will have a value that is very close to 1.5, but it could be a tiny bit less, or a tiny bit more. Using double or long double instead of float won't help, x will be a lot closer to 1.5, but it will still be a tiny bit less or a tiny bit more. The solution is very simple: Use an integer variable.

int i;
double x;

for (i = 0; i <= 15; ++i) { x = i * 0.1; ... }

Nice idea.
Thanks gnasher and Lee.

MrFusion
Mar 22, 2008, 04:31 PM
If you want to check that x <= y then how about something like this:-


#define EPSILON ( 0.0001f )

float x, y ;


if ( ( x - y ) < EPSILON )
{

}


You would need to choose a suitable EPSILON though.

b e n

This also works, but since both 10E12 as 10E-9 is possible as value, choosing a suitable epsilon would be difficult.

MrFusion
Mar 22, 2008, 04:35 PM
Any help or advice for this idiot is welcome. :)

The idiot is grateful for all the replies. :)

lazydog
Mar 22, 2008, 04:37 PM
This also works, but since both 10E12 as 10E-9 is possible as value, choosing a suitable epsilon would be difficult.

I'm not sure what you mean. Are you saying your accumulated error could be as large as 1000000000000? :eek:

b e n

EDIT: Sorry I think i misunderstood you. Do you mean the range of values is 10E12 to 10E-9?

Nutter
Mar 22, 2008, 04:47 PM
Have you looked at NSDecimalNumber?

lazydog
Mar 22, 2008, 04:48 PM
Okay, I think this might work better for you then.



#define EPSILON ( 0.0001f )

float x, y ;


if ( ( ( x - y ) < EPSILON * fabsf( x ) ) && ( ( x - y ) < EPSILON * fabsf( y ) ) )
{

}


b e n