From 89c50509d8408e5d6884a264a4aa393327aef2a4 Mon Sep 17 00:00:00 2001 From: Gabriel Ozouf Date: Mon, 4 Jan 2021 14:41:32 +0100 Subject: [PATCH] [shared] InteractiveCurveViewRange::isOrthonormal The old method isOrthonormal has been split into two : - shouldBeNormalized tests whether the range is close enough to a normal range - isOrthonormal tests whether the range is strictly orthonormal, but takes into account imprecisions of floating-point arithmetic --- apps/regression/store.cpp | 2 +- apps/shared/interactive_curve_view_range.cpp | 44 ++++++++++++++++---- apps/shared/interactive_curve_view_range.h | 10 +++-- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/apps/regression/store.cpp b/apps/regression/store.cpp index f2615acea..e081924f5 100644 --- a/apps/regression/store.cpp +++ b/apps/regression/store.cpp @@ -163,7 +163,7 @@ void Store::setDefault() { m_xRange.setMax(xMax); m_yRange.setMin(yMin); m_yRange.setMax(yMax); - bool revertToOrthonormal = isOrthonormal(k_orthonormalTolerance); + bool revertToOrthonormal = shouldBeNormalized(); float range = xMax - xMin; setXMin(xMin - k_displayHorizontalMarginRatio * range); diff --git a/apps/shared/interactive_curve_view_range.cpp b/apps/shared/interactive_curve_view_range.cpp index 0f314b0c2..a45bad573 100644 --- a/apps/shared/interactive_curve_view_range.cpp +++ b/apps/shared/interactive_curve_view_range.cpp @@ -157,8 +157,13 @@ void InteractiveCurveViewRange::normalize(bool forceChangeY) { m_yRange.setMin(newYMin, k_lowerMaxFloat, k_upperMaxFloat); MemoizedCurveViewRange::protectedSetYMax(newYMax, k_lowerMaxFloat, k_upperMaxFloat); - /* The range should be close to orthonormal, unless it has been clipped because the maximum bounds have been reached. */ - assert(isOrthonormal() || xMin() <= - k_lowerMaxFloat || xMax() >= k_lowerMaxFloat || yMin() <= - k_lowerMaxFloat || yMax() >= k_lowerMaxFloat); + /* The range should be close to orthonormal, unless : + * - it has been clipped because the maximum bounds have been reached. + * - the the bounds are too close and of too large a magnitude, leading to + * a drastic loss of significance. */ + assert(isOrthonormal() + || xMin() <= - k_lowerMaxFloat || xMax() >= k_lowerMaxFloat || yMin() <= - k_lowerMaxFloat || yMax() >= k_lowerMaxFloat + || normalizationSignificantBits() <= 0); setZoomNormalize(isOrthonormal()); } @@ -193,7 +198,7 @@ void InteractiveCurveViewRange::setDefault() { m_yRange.setMin(roundLimit(m_delegate->addMargin(yMin(), yRange, true , true), yRange, true), k_lowerMaxFloat, k_upperMaxFloat); MemoizedCurveViewRange::protectedSetYMax(roundLimit(m_delegate->addMargin(yMax(), yRange, true , false), yRange, false), k_lowerMaxFloat, k_upperMaxFloat); - if (m_delegate->defaultRangeIsNormalized() || isOrthonormal(k_orthonormalTolerance)) { + if (m_delegate->defaultRangeIsNormalized() || shouldBeNormalized()) { /* Normalize the axes, so that a polar circle is displayed as a circle. * If we are displaying cartesian functions with a default range, we want * the X bounds untouched. */ @@ -276,8 +281,27 @@ void InteractiveCurveViewRange::panToMakePointVisible(float x, float y, float to setZoomNormalize(isOrthonormal()); } -bool InteractiveCurveViewRange::isOrthonormal(float tolerance) const { - if (tolerance == 0.f) { +bool InteractiveCurveViewRange::shouldBeNormalized() const { + float ratio = (yMax() - yMin()) / (xMax() - xMin()); + return ratio >= NormalYXRatio() / k_orthonormalTolerance && ratio <= NormalYXRatio() * k_orthonormalTolerance; +} + +bool InteractiveCurveViewRange::isOrthonormal() const { + float significantBits = normalizationSignificantBits(); + if (significantBits <= 0) { + return false; + } + float ratio = (yMax() - yMin()) / (xMax() - xMin()); + /* The last N (= 23 - significantBits) bits of "ratio" mantissa have become + * insignificant. "tolerance" is the difference between ratio with those N + * bits set to 1, and ratio with those N bits set to 0 ; i.e. a measure of + * the interval in which numbers are indistinguishable from ratio with this + * level of precision. */ + float tolerance = std::pow(2.f, IEEE754::exponent(ratio) - significantBits); + return ratio - tolerance <= NormalYXRatio() && ratio + tolerance >= NormalYXRatio(); +} + +int InteractiveCurveViewRange::normalizationSignificantBits() const { float xr = std::fabs(xMin()) > std::fabs(xMax()) ? xMax() / xMin() : xMin() / xMax(); float yr = std::fabs(yMin()) > std::fabs(yMax()) ? yMax() / yMin() : yMin() / yMax(); /* The subtraction x - y induces a loss of significance of -log2(1-x/y) @@ -285,9 +309,11 @@ bool InteractiveCurveViewRange::isOrthonormal(float tolerance) const { * the ratio of the normalized range will deviate from the Normal ratio. We * add an extra two lost bits to account for loss of precision from other * sources. */ - tolerance = std::pow(2.f, - std::log2(std::min(1.f - xr, 1.f - yr)) - 23.f + 2.f); - } - float ratio = (yMax() - yMin()) / (xMax() - xMin()); - return ratio <= NormalYXRatio() + tolerance && ratio >= NormalYXRatio() - tolerance; + float loss = std::log2(std::min(1.f - xr, 1.f - yr)); + if (loss > 0.f) { + loss = 0.f; + } + return std::floor(loss + 23.f - 2.f); } + } diff --git a/apps/shared/interactive_curve_view_range.h b/apps/shared/interactive_curve_view_range.h index f7b860090..d56f43bbc 100644 --- a/apps/shared/interactive_curve_view_range.h +++ b/apps/shared/interactive_curve_view_range.h @@ -24,9 +24,9 @@ public: } static constexpr float NormalYXRatio() { return NormalizedYHalfRange(1.f) / NormalizedXHalfRange(1.f); } - /* If the tolerance is null, isOrthonormal will adapt the tolerance to take - * the loss of significance when changing the ratio into account. */ - bool isOrthonormal(float tolerance = 0.f) const; + /* The method isOrthonormal takes the loss of significance when changing the + * ratio into account. */ + bool isOrthonormal() const; void setDelegate(InteractiveCurveViewRangeDelegate * delegate); uint32_t rangeChecksum() override; @@ -62,7 +62,7 @@ protected: constexpr static float k_lowerMaxFloat = 9E+7f; constexpr static float k_maxRatioPositionRange = 1E5f; /* The tolerance is chosen to normalize sqrt(x) */ - constexpr static float k_orthonormalTolerance = 0.24f; + constexpr static float k_orthonormalTolerance = 1.78f; static float clipped(float x, bool isMax) { return Range1D::clipped(x, isMax, k_lowerMaxFloat, k_upperMaxFloat); } /* In normalized settings, we put each axis so that 1cm = 2 units. For now, * the screen has size 43.2mm * 57.6mm. @@ -78,11 +78,13 @@ protected: * 2 * 1 unit -> 10.0mm * So normalizedYHalfRange = 43.2mm * 170/240 * 1 unit / 10.0mm */ constexpr static float NormalizedYHalfRange(float unit) { return 3.06f * unit; } + bool shouldBeNormalized() const; virtual bool hasDefaultRange() const { return (xMin() == std::round(xMin())) && (xMax() == std::round(xMax())); } InteractiveCurveViewRangeDelegate * m_delegate; private: float offscreenYAxis() const override { return m_offscreenYAxis; } + int normalizationSignificantBits() const; float m_offscreenYAxis; bool m_zoomAuto;