Python - Guides Float Value Randomness

I have been stuck for a couple of days now trying to discover my issue with handling guides and I think I found it.

Using the command “setHorizontalGuides” and “setVerticalGuides” they do not apply the correct values given into Krita so when I read them again they are different instead of being equal. so this causes an error.

The issue is that when you apply a new set of guides, randomly on certain values like number.0000000001 it will round as number.00000000013 and then on the next apply it will round into another random and then into another random.

The amount of times it does this is unpredicatable and impossible to stop.

I have tried to force the value into a integer so it would be just int(number) and also have tried to set the values to round(number,1) so it would not go on a train of zero decimals and it would be closer to the value it needs to be. However doing any of these do a infinite loop because on that given number it will always apply the value wrong instead of stabilizing on the probably nearest stable value that does not change after a couple of cycles.

Krita output


In this image you can see one of the cycles that messed up and needed 5 cycles to stabilize into correct values. The code only reads Krita. Sorts the values between themselves (no change to the values within the script) and considering there was a sorting event it applies the values back into Krita so they are organized. which causes the random cycle that can cause infinite loops on more complex changes. I moved with the mouse a guide within Krita’s canvas and this is my addon trying to coupe with that change, and it stops updating once there are no new values. The new values are underlined with red.

Variables
Cycle - Number of the plugin cycle to update
Krita - Guide list read from Krita
Previous - Scripts memory of previous cycle so it compares to the current cycle and decides how to operate (no operations are done in this example)
Sort - Sorting operation that alters the order of the values.
Next - The value that should be read by Krita on the next Cycle.

Python code check
1 - create a new document size 1000x1000
2 - Open Scripter and run this Python code

import krita
ad = Krita.instance().activeDocument()
list = [222.00000000000001, 344.00000000000002, 472.00000000000003, 515.0000000000001, 583.0000000000002, 609.0000000000003]
ad.setHorizontalGuides(list)
guides = ad.horizontalGuides()
print(guides)

Output

[222.00000000000003, 344.00000000000006, 472.0000000000001, 515.0000000000001, 583.0000000000002, 609.0000000000003]

As you can see the first 3 values in the list changed and the last 3 values in the list did not change.
I should add that this list of values was created by moving guides on the canvas.

Observations:
I understand why float is being used for the Guides however their value usage of it seems very inconsistent this due to the fact that their location is in between pixels by force of the system so instead of 222.00000000000003 why is it not 222.00000000000000 or 222.0 alone on all values? This is what puzzles me the most, as this in-between pixel snapping should round the values to 222.0.

Snapping

Hi

You’re discovering how computers really works with floating point numbers :slight_smile:

This is relative how float type are stored in memory.
I’m too lazy to explain it in details :sweat_smile:
You can take a look here, chapter 5. The IEEE-754 floating-point standard if you don’t want to read everything

In Python there’s a dedicated module to work with decimals value on which C/C++ float precision error is avoided:
https://docs.python.org/3/library/decimal.html

Not sure you really need to work with it, except if you’re really bored by the precision.
(But if you really want to use it, keep in mind that Krita will continue to work with float values so problem will still occurs on Krita side…)

Grum999

I know about that but does not floating point imprecision affect all values inside the computer since ever?
then how can I get a guide list value like these? these values are “ignoring” it for the user already. I find that a bit strange to be just that.

read guides placed on canvas then input same again with python:
[99.99999637900498, 190.99999999999997, 393.00000000000006, 544.0, 602.0]
output:
[99.999996379005, 190.99999999999997, 393.00000000000006, 544.0, 602.0]

there are values that seem not affected by it for the most part, but then you move them a bit and they mess up heavily and create the infinite loop bla bla bla.

Just float impercision? I don’t know. in Pigmento I had no decimal treatment as there was a separation from the values the user uses and the values pigmento uses and the check was the same.

previous != now

And it would update only once unless you used non RGB values. seems too good to be true just float decimal values error.

Also I have been setting float values to a bunch of other properties and this is the only one plagued by float imprecision seems to dramatic to be true. I will do the check if it is that but if it is seems a issue none the less.

So, open a bug.
But on my side I’m sure that just an error precision, nothing else.

Note: guide are stored in a qreal, so might be a double instead of a float number; error precision is not exactly the same :slight_smile:

24years working in IT, and practically 39years of coding in miscellaneous langages…
I saw this kind of problem so many times, that when I saw a new one, I don’t try to look around about origin of problem :slight_smile:

Grum999

1 Like

Small example of precision error in Python<–>C

from ctypes import *

lst=[1, 0.1, 0.01, 99.99999637900498, 190.99999999999997, 393.00000000000006, 544.0, 602.0]

for f in lst:
    ctf=float(c_float(f).value)
    ctd=float(c_double(f).value)
    ctld=float(c_longdouble(f).value)
    
    print('float', f, 'float(c_float)', ctf, 'float(c_double)', ctd, 'float(c_longdouble)', ctld)

Result:

float 1 float(c_float) 1.0 float(c_double) 1.0 float(c_longdouble) 1.0
float 0.1 float(c_float) 0.10000000149011612 float(c_double) 0.1 float(c_longdouble) 0.1
float 0.01 float(c_float) 0.009999999776482582 float(c_double) 0.01 float(c_longdouble) 0.01
float 99.99999637900498 float(c_float) 100.0 float(c_double) 99.99999637900498 float(c_longdouble) 99.99999637900498
float 190.99999999999997 float(c_float) 191.0 float(c_double) 190.99999999999997 float(c_longdouble) 190.99999999999997
float 393.00000000000006 float(c_float) 393.0 float(c_double) 393.00000000000006 float(c_longdouble) 393.00000000000006
float 544.0 float(c_float) 544.0 float(c_double) 544.0 float(c_longdouble) 544.0
float 602.0 float(c_float) 602.0 float(c_double) 602.0 float(c_longdouble) 602.0

The ctypes library allows you to work directly with C types

As you can see, you have precision error when Python float is translated to C float.
(seems to not have precision error to double because Python float might be a internally stored as a double - but I didn’t check)…

In our case, we have multiple layers:

  • Python
  • SIP (bridge between Python and C)
  • C
  • Krita

I don’t know of SIP manage conversion between python and c libraries (maybe it’s transparent)
In Krita, maybe there’s some conversion, I don’t know: API currently return a QList<qreal>; according to Qt documentation, qreal is a double by default, but can be a float according to compilation option

So maybe if inside Krita there’s different used types (float, double), you can accumulate precision error
Maybe in Krita you have some conversion to different units, or some rounding rules that finally produce the error you got. I don’t know.

But for me what you have is typically the result of a precision error… :man_shrugging:

Grum999

1 Like

That’s one of the reasons that you never compare floats together… always abs(f1, f2) < epsilon, where epsilon says what precision you want to keep it at.

3 Likes

Well I followed your advice about float precision error and all seems to not loop anymore, so it is working for the time being, I was not able to crash it yet by changing values.

I read C++ only very lightly so I don’t understand everything you wrote there just partially.

I ended up using the round again instead of decimal because it needs a lot more lines and I have no need to then change it back to float giving the same results with less work so to speak. After all I am destroying the original read either way by correcting it.

for i in range(0, len(gui_hor)):
    gui_hor[i] = round(gui_hor[i], 5)

But this is essentially wrong. Math is not a potato but it seems here it really is a potato. :cry:

Luckily here is just virtual world but in the real world this is not something you try and keep I have seen so many engineers slacking off because of this. Working with a error creates more error with each cycle luckily guides force the snapping due to their nature and ignore all that.

The odd thing about this all is that I have been comparing floats since day one with Python and me and no error ever occurred this must be my 15th property I have used read and write with Krita and floats (the others are booleans so they don’t count) and guides is the first to throw a tantrum at me because of it. Strangely it should have been the one less annoying about it. I don’t know all the C++ variable types nor it’s limitations but it feels overall inconsistent to me to see this now on the guides.