Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expression: use a precision that ensures no floating point issue #9163

Merged
merged 1 commit into from Apr 5, 2023

Conversation

0penBrain
Copy link
Contributor

https://forum.freecad.org/viewtopic.php?t=77287

I should say I didn't really understand current code.
std::numeric_limits<double>::digits10 is exactly the maximum precision to ensure no floating point rounding issue.
So setting it to + 1 is just making a step to the world of problems. 馃槃

@github-actions github-actions bot added the Core Issue or PR touches core sections (App, Gui, Base) of FreeCAD label Apr 2, 2023
@0penBrain 0penBrain requested a review from wwmayer April 2, 2023 19:09
@freecadci
Copy link

pipeline status for feature branch PR_9163. Pipeline 826746123 was triggered at 98b0d75. All CI branches and pipelines.

@wwmayer
Copy link
Contributor

wwmayer commented Apr 3, 2023

I don't think it's that easy. Have a look at https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10
when writing a number.

with this test function

#include <limits>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <cmath>

int main()
{
    {
        std::ostringstream str;
        str << std::setprecision(std::numeric_limits<double>::digits10 + 1);
        str << 8.2 << '\n';
        double next = std::nextafter(8.2, 9.0);
        str << next << '\n';
        str << std::fixed << std::numeric_limits<double>::epsilon() << '\n';
        std::cout << "\nUse " << std::numeric_limits<double>::digits10 + 1 << " digits\n";
        std::cout << str.str();
    }
    {
        std::ostringstream str;
        str << std::setprecision(std::numeric_limits<double>::digits10);
        str << 8.2 << '\n';
        double next = std::nextafter(8.2, 9.0);
        str << next << '\n';
        str << std::fixed << std::numeric_limits<double>::epsilon() << '\n';
        std::cout << "\nUse " << std::numeric_limits<double>::digits10 << " digits\n";
        std::cout << str.str();
    }
}

the output is:

Use 16 digits
8.199999999999999
8.200000000000001
0.0000000000000002

Use 15 digits
8.2
8.2
0.000000000000000

@0penBrain
Copy link
Contributor Author

I don't think it's that easy.

Hmm. Sorry I don't see. For me all values on top are wrong, and all on bottom are correct. So it seems to confirm the fix. What did I miss?

@wwmayer
Copy link
Contributor

wwmayer commented Apr 4, 2023

What did I miss?

The value of double next = std::nextafter(8.2, 9.0); is guaranteed to be different to 8.2. When writing with digits10 the text output is the same, i.e. when reading it in we have a loss of accuracy.
Also the value of std::numeric_limits<double>::epsilon() is guaranteed to be higher than the written 0.0

The documentation of max_digits10 says:

Unlike most mathematical operations, the conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double): it is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact. It may take over a hundred decimal digits to represent the precise value of a float in decimal notation.

So, this means when serializing a double as string then by using max_digits10 (which is even higher than digits10 + 1) it's guaranteed that the double converted from the string is equal. With digits10 we lose precision.

Look at this test code now:

#include <limits>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <cmath>

int main()
{
    {
        double value1 = std::asin(1);
        std::ostringstream str;
        str << std::setprecision(std::numeric_limits<double>::max_digits10);
        str << value1;
        double value2 = std::stod(str.str());
        std::cout << "double -> text -> double: " << (value1 == value2 ? "equal\n" : "not equal\n");
    }
    {
        double value1 = std::asin(1);
        std::ostringstream str;
        str << std::setprecision(std::numeric_limits<double>::digits10);
        str << value1;
        double value2 = std::stod(str.str());
        std::cout << "double -> text -> double: " << (value1 == value2 ? "equal\n" : "not equal\n");
    }
}

The output is:

double -> text -> double: equal
double -> text -> double: not equal

So, the question is whether the change could cause possible regressions elsewhere.

@0penBrain
Copy link
Contributor Author

Well, all that you wrote is perfectly correct and I agree with it. But IMO this is looking at the problem from the wrong side. 馃槈

The value of double next = std::nextafter(8.2, 9.0); is guaranteed to be different to 8.2. When writing with digits10 the text output is the same, i.e. when reading it in we have a loss of accuracy.

Which is excellent. We have an inner precision better than the one we display. This is precisely what we want.
Oppositely, your previous posts proved that using max_digits10 +1 can't guarantee that all inputs aren't actually possible as 8.2 and its nextafter are separated by more than 1 less-significant digit.

Also the value of std::numeric_limits<double>::epsilon() is guaranteed to be higher than the written 0.0

Again, I have no doubt that it internally is. And having displayed as 0.0 is excellent as this also proves that our display precision is compatible with internal precision.

The documentation of max_digits10 says:

Unlike most mathematical operations, the conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double): it is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact. It may take over a hundred decimal digits to represent the precise value of a float in decimal notation.

True. But basically we do the opposite : string -> value -> string. And in this case this documentation doesn't apply. It even is the "opposite", we have issues using a more than max_digits10 value to string conversion. Which is normal.

So, this means when serializing a double as string then by using max_digits10 (which is even higher than digits10 + 1) it's guaranteed that the double converted from the string is equal. With digits10 we lose precision.

But we need to lose precision in our case. 馃槈 This is a tradeoff to ensure we correctly display the user input.

So, the question is whether the change could cause possible regressions elsewhere.

I quickly checked and toString() calls seem to be only used for presentation so it shouldn't create any regression.
And anyway if it would, that would be an error made somewhere else in the code to use this IMO.
And well, admittedly it solves the bug seen on the forum. 馃槈

@wwmayer
Copy link
Contributor

wwmayer commented Apr 5, 2023

I have looked at the history of the file and at the very beginning there was no precision set. Later digits10 + 1 was set, then digits10 + 2 that caused some problems. The change was reverted until it was again set to digts10 + 2. This of course, caused again some problems so that it was reverted.

However, I never found an explanation why digits10 +1 or +2 was used.

True. But basically we do the opposite : string -> value -> string.

Not only. When saving it to a file then we have value -> string -> value. Doing some tests I couldn't find a case where the restored value has caused problems.

I quickly checked and toString() calls seem to be only used for presentation so it shouldn't create any regression.

Then you missed the saving of a project. The call stack is:

  • NumberExpression::_toString
  • Expression::toString
  • Expression::toString (overloaded version)
  • Cell::getStringContent
  • Cell::save
  • Cell::save (overloaded version)
  • PropertySheet::Save
  • ...

But as said I couldn't find a problem I think it's fine to merge it.

@wwmayer wwmayer merged commit 96bf293 into FreeCAD:master Apr 5, 2023
7 checks passed
@0penBrain
Copy link
Contributor Author

Not only. When saving it to a file then we have value -> string -> value.

Yes, but this is actually part of a longer chain : string (user input) -> value (expression) -> string (document) -> value (expression) -> string (display). 馃槈

Then you missed the saving of a project.

Yes I did. 馃槃

@wwmayer
Copy link
Contributor

wwmayer commented Apr 6, 2023

Yes, but this is actually part of a longer chain : string (user input) -> value (expression) -> string (document) -> value (expression) -> string (display).

Not necessarily. I have created a test project with a spreadsheet. After the first saving I just opened the file and directly saved it again without having opened the spreadsheet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Core Issue or PR touches core sections (App, Gui, Base) of FreeCAD Expressions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants