diff --git a/apps/graph/graph/graph_controller.cpp b/apps/graph/graph/graph_controller.cpp index 998c2a20d..14232d717 100644 --- a/apps/graph/graph/graph_controller.cpp +++ b/apps/graph/graph/graph_controller.cpp @@ -39,58 +39,6 @@ bool GraphController::defaultRangeIsNormalized() const { return functionStore()->displaysNonCartesianFunctions(); } -void GraphController::interestingRanges(InteractiveCurveViewRange * range) const { - privateComputeRanges(true, range); -} - -Shared::InteractiveCurveViewRangeDelegate::Range GraphController::computeYRange(Shared::InteractiveCurveViewRange * interactiveCurveViewRange) { - InteractiveCurveViewRange tempRange = *interactiveCurveViewRange; - tempRange.setYAuto(false); - privateComputeRanges(false, &tempRange); - return Shared::InteractiveCurveViewRangeDelegate::Range{.min = tempRange.yMin(), .max = tempRange.yMax()}; -} - -void GraphController::privateComputeRanges(bool tuneXRange, InteractiveCurveViewRange * range) const { - Poincare::Context * context = textFieldDelegateApp()->localContext(); - float resultXMin = tuneXRange ? FLT_MAX : range->xMin(); - float resultXMax = tuneXRange ? -FLT_MAX : range->xMax(); - float resultYMin = FLT_MAX; - float resultYMax = -FLT_MAX; - assert(functionStore()->numberOfActiveFunctions() > 0); - int functionsCount = functionStore()->numberOfActiveFunctions(); - for (int i = 0; i < functionsCount; i++) { - ExpiringPointer f = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(i)); - f->rangeForDisplay(&resultXMin, &resultXMax, &resultYMin, &resultYMax, context, tuneXRange); - } - - range->setXMin(resultXMin); - range->setXMax(resultXMax); - range->setYMin(resultYMin); - range->setYMax(resultYMax); - /* We can only call this method once the X range has been fully computed. */ - yRangeForCursorFirstMove(range); -} - -void GraphController::yRangeForCursorFirstMove(InteractiveCurveViewRange * range) const { - Poincare::Context * context = textFieldDelegateApp()->localContext(); - assert(functionStore()->numberOfActiveFunctions() > 0); - int functionsCount = functionStore()->numberOfActiveFunctions(); - - float cursorStep = range->xGridUnit() / k_numberOfCursorStepsInGradUnit; - float yN, yP; - - for (int i = 0; i < functionsCount; i++) { - ExpiringPointer f = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(i)); - if (f->plotType() != ContinuousFunction::PlotType::Cartesian) { - continue; - } - yN = f->evaluateXYAtParameter(range->xCenter() - cursorStep, context).x2(); - yP = f->evaluateXYAtParameter(range->xCenter() + cursorStep, context).x2(); - range->setYMin(std::min(range->yMin(), std::min(yN, yP))); - range->setYMax(std::max(range->yMax(), std::max(yN, yP))); - } -} - void GraphController::selectFunctionWithCursor(int functionIndex) { FunctionGraphController::selectFunctionWithCursor(functionIndex); ExpiringPointer f = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(functionIndex)); diff --git a/apps/graph/graph/graph_controller.h b/apps/graph/graph/graph_controller.h index 89df7333a..347ceabc6 100644 --- a/apps/graph/graph/graph_controller.h +++ b/apps/graph/graph/graph_controller.h @@ -20,7 +20,6 @@ public: void viewWillAppear() override; bool displayDerivativeInBanner() const { return m_displayDerivativeInBanner; } void setDisplayDerivativeInBanner(bool displayDerivative) { m_displayDerivativeInBanner = displayDerivative; } - void interestingRanges(Shared::InteractiveCurveViewRange * range) const override; private: int estimatedBannerNumberOfLines() const override { return 1 + m_displayDerivativeInBanner; } void selectFunctionWithCursor(int functionIndex) override; @@ -37,9 +36,6 @@ private: void interestingFunctionRange(Shared::ExpiringPointer f, float tMin, float tMax, float step, float * xm, float * xM, float * ym, float * yM) const; bool shouldSetDefaultOnModelChange() const override; void jumpToLeftRightCurve(double t, int direction, int functionsCount, Ion::Storage::Record record) override; - Range computeYRange(Shared::InteractiveCurveViewRange * interactiveCurveViewRange) override; - void privateComputeRanges(bool tuneXRange, Shared::InteractiveCurveViewRange * range) const; - void yRangeForCursorFirstMove(Shared::InteractiveCurveViewRange * range) const; Shared::RoundCursorView m_cursorView; BannerView m_bannerView; diff --git a/apps/sequence/graph/curve_view_range.cpp b/apps/sequence/graph/curve_view_range.cpp index a4374164d..977c58dcc 100644 --- a/apps/sequence/graph/curve_view_range.cpp +++ b/apps/sequence/graph/curve_view_range.cpp @@ -75,15 +75,4 @@ void CurveViewRange::setTrigonometric() { MemoizedCurveViewRange::protectedSetYMin(-y, k_lowerMaxFloat, k_upperMaxFloat); } -void CurveViewRange::setDefault() { - if (m_delegate == nullptr) { - return; - } - m_yAuto = true; - float interestingXMin = m_delegate->interestingXMin(); - float interestingXRange = m_delegate->interestingXHalfRange(); - m_xRange.setMax(interestingXMin + interestingXRange, k_lowerMaxFloat, k_upperMaxFloat); - setXMin(interestingXMin - k_displayLeftMarginRatio * interestingXRange); -} - } diff --git a/apps/sequence/graph/curve_view_range.h b/apps/sequence/graph/curve_view_range.h index cb50fb502..83c4da170 100644 --- a/apps/sequence/graph/curve_view_range.h +++ b/apps/sequence/graph/curve_view_range.h @@ -11,7 +11,6 @@ public: void roundAbscissa() override; void normalize() override; void setTrigonometric() override; - void setDefault() override; private: constexpr static float k_displayLeftMarginRatio = 0.1f; }; diff --git a/apps/sequence/graph/graph_controller.cpp b/apps/sequence/graph/graph_controller.cpp index 303d4b001..6f98d3ef0 100644 --- a/apps/sequence/graph/graph_controller.cpp +++ b/apps/sequence/graph/graph_controller.cpp @@ -2,6 +2,8 @@ #include #include #include "../app.h" +#include +#include #include using namespace Shared; @@ -43,8 +45,7 @@ float GraphController::interestingXMin() const { return nmin; } -float GraphController::interestingXHalfRange() const { - float standardRange = Shared::FunctionGraphController::interestingXHalfRange(); +void GraphController::interestingRanges(InteractiveCurveViewRange * range) const { int nmin = INT_MAX; int nmax = 0; int nbOfActiveModels = functionStore()->numberOfActiveFunctions(); @@ -52,10 +53,13 @@ float GraphController::interestingXHalfRange() const { Sequence * s = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(i)); int firstInterestingIndex = s->initialRank(); nmin = std::min(nmin, firstInterestingIndex); - nmax = std::max(nmax, firstInterestingIndex + static_cast(standardRange)); + nmax = std::max(nmax, firstInterestingIndex + static_cast(k_defaultXHalfRange)); } - assert(nmax - nmin >= standardRange); - return nmax - nmin; + assert(nmax - nmin >= k_defaultXHalfRange); + + range->setXMin(nmin); + range->setYAuto(true); + range->setXMax(nmax); } bool GraphController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) { @@ -101,52 +105,4 @@ double GraphController::defaultCursorT(Ion::Storage::Record record) { return std::fmax(0.0, std::round(Shared::FunctionGraphController::defaultCursorT(record))); } -InteractiveCurveViewRangeDelegate::Range GraphController::computeYRange(InteractiveCurveViewRange * interactiveCurveViewRange) { - Poincare::Context * context = textFieldDelegateApp()->localContext(); - float min = FLT_MAX; - float max = -FLT_MAX; - float xMin = interactiveCurveViewRange->xMin(); - float xMax = interactiveCurveViewRange->xMax(); - assert(functionStore()->numberOfActiveFunctions() > 0); - for (int i = 0; i < functionStore()->numberOfActiveFunctions(); i++) { - ExpiringPointer f = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(i)); - /* Scan x-range from the middle to the extrema in order to get balanced - * y-range for even functions (y = 1/x). */ - double tMin = f->tMin(); - if (std::isnan(tMin)) { - tMin = xMin; - } else if (f->shouldClipTRangeToXRange()) { - tMin = std::max(tMin, xMin); - } - double tMax = f->tMax(); - if (std::isnan(tMax)) { - tMax = xMax; - } else if (f->shouldClipTRangeToXRange()) { - tMax = std::min(tMax, xMax); - } - /* In practice, a step smaller than a pixel's width is needed for sampling - * the values of a function. Otherwise some relevant extremal values may be - * missed. */ - float rangeStep = f->rangeStep(); - const float step = std::isnan(rangeStep) ? curveView()->pixelWidth() / 2.0f : rangeStep; - const int balancedBound = std::floor((tMax-tMin)/2/step); - for (int j = -balancedBound; j <= balancedBound ; j++) { - float t = (tMin+tMax)/2 + step * j; - Coordinate2D xy = f->evaluateXYAtParameter(t, context); - float x = xy.x1(); - if (!std::isnan(x) && !std::isinf(x) && x >= xMin && x <= xMax) { - float y = xy.x2(); - if (!std::isnan(y) && !std::isinf(y)) { - min = std::min(min, y); - max = std::max(max, y); - } - } - } - } - InteractiveCurveViewRangeDelegate::Range range; - range.min = min; - range.max = max; - return range; -} - } diff --git a/apps/sequence/graph/graph_controller.h b/apps/sequence/graph/graph_controller.h index 79440af91..de95bb647 100644 --- a/apps/sequence/graph/graph_controller.h +++ b/apps/sequence/graph/graph_controller.h @@ -20,14 +20,13 @@ public: TermSumController * termSumController() { return &m_termSumController; } // InteractiveCurveViewRangeDelegate float interestingXMin() const override; - float interestingXHalfRange() const override; + void interestingRanges(Shared::InteractiveCurveViewRange * range) const override; bool textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) override; private: Shared::XYBannerView * bannerView() override { return &m_bannerView; } bool handleEnter() override; bool moveCursorHorizontally(int direction, int scrollSpeed = 1) override; double defaultCursorT(Ion::Storage::Record record) override; - InteractiveCurveViewRangeDelegate::Range computeYRange(Shared::InteractiveCurveViewRange * interactiveCurveViewRange) override; CurveViewRange * interactiveCurveViewRange() override { return m_graphRange; } SequenceStore * functionStore() const override { return static_cast(Shared::FunctionGraphController::functionStore()); } GraphView * functionGraphView() override { return &m_view; } diff --git a/apps/sequence/sequence.h b/apps/sequence/sequence.h index d7f103890..850cffdb0 100644 --- a/apps/sequence/sequence.h +++ b/apps/sequence/sequence.h @@ -148,6 +148,7 @@ private: }; template T templatedApproximateAtAbscissa(T x, SequenceContext * sqctx) const; + void refinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Poincare::Context * context) const override { protectedRefinedYRangeForDisplay(xMin, xMax, yMin, yMax, context, false); } size_t metaDataSize() const override { return sizeof(RecordDataBuffer); } const Shared::ExpressionModel * model() const override { return &m_definition; } RecordDataBuffer * recordData() const; diff --git a/apps/shared/continuous_function.cpp b/apps/shared/continuous_function.cpp index 043e76343..0d316a5e1 100644 --- a/apps/shared/continuous_function.cpp +++ b/apps/shared/continuous_function.cpp @@ -263,7 +263,7 @@ void ContinuousFunction::setTMax(float tMax) { void ContinuousFunction::rangeForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Poincare::Context * context, bool tuneXRange) const { if (plotType() == PlotType::Cartesian) { - interestingXAndYRangesForDisplay(xMin, xMax, yMin, yMax, context, tuneXRange); + Function::rangeForDisplay(xMin, xMax, yMin, yMax, context, tuneXRange); } else { fullXYRange(xMin, xMax, yMin, yMax, context); } @@ -294,254 +294,6 @@ void ContinuousFunction::fullXYRange(float * xMin, float * xMax, float * yMin, f *yMax = resultYMax; } -static float evaluateAndRound(const ContinuousFunction * f, float x, Context * context, float precision = 1e-5) { - /* When evaluating sin(x)/x close to zero using the standard sine function, - * one can detect small varitions, while the cardinal sine is supposed to be - * locally monotonous. To smooth our such variations, we round the result of - * the evaluations. As we are not interested in precise results but only in - * ordering, this approximation is sufficient. */ - return precision * std::round(f->evaluateXYAtParameter(x, context).x2() / precision); -} - - -/* TODO : These three methods perform checks that will also be relevant for the - * equation solver. Remember to factorize this code when integrating the new - * solver. */ -static bool boundOfIntervalOfDefinitionIsReached(float y1, float y2) { - return std::isfinite(y1) && !std::isinf(y2) && std::isnan(y2); -} -static bool rootExistsOnInterval(float y1, float y2) { - return ((y1 < 0.f && y2 > 0.f) || (y1 > 0.f && y2 < 0.f)); -} -static bool extremumExistsOnInterval(float y1, float y2, float y3) { - return (y1 < y2 && y2 > y3) || (y1 > y2 && y2 < y3); -} - -/* This function checks whether an interval contains an extremum or an - * asymptote, by recursively computing the slopes. In case of an extremum, the - * slope should taper off toward the center. */ -static bool isExtremum(const ContinuousFunction * f, float x1, float x2, float x3, float y1, float y2, float y3, Context * context, int iterations = 3) { - if (iterations <= 0) { - return false; - } - float x[2] = {x1, x3}, y[2] = {y1, y3}; - float xm, ym; - for (int i = 0; i < 2; i++) { - xm = (x[i] + x2) / 2.f; - ym = evaluateAndRound(f, xm, context); - bool res = ((y[i] < ym) != (ym < y2)) ? isExtremum(f, x[i], xm, x2, y[i], ym, y2, context, iterations - 1) : std::fabs(ym - y[i]) >= std::fabs(y2 - ym); - if (!res) { - return false; - } - } - return true; -} - -enum class PointOfInterest : uint8_t { - None, - Bound, - Extremum, - Root -}; - -void ContinuousFunction::interestingXAndYRangesForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Context * context, bool tuneXRange) const { - assert(xMin && xMax && yMin && yMax); - assert(plotType() == PlotType::Cartesian); - - /* Constants of the algorithm. */ - constexpr float defaultMaxInterval = 2e5f; - constexpr float minDistance = 1e-2f; - constexpr float asymptoteThreshold = 2e-1f; - constexpr float stepFactor = 1.1f; - constexpr int maxNumberOfPoints = 3; - constexpr float breathingRoom = 0.3f; - constexpr float maxRatioBetweenPoints = 100.f; - - const bool hasIntervalOfDefinition = std::isfinite(tMin()) && std::isfinite(tMax()); - float center, maxDistance; - if (!tuneXRange) { - center = (*xMax + *xMin) / 2.f; - maxDistance = (*xMax - *xMin) / 2.f; - } else if (hasIntervalOfDefinition) { - center = (tMax() + tMin()) / 2.f; - maxDistance = (tMax() - tMin()) / 2.f; - } else { - center = 0.f; - maxDistance = defaultMaxInterval / 2.f; - } - - float resultX[2] = {FLT_MAX, - FLT_MAX}; - float resultYMin = FLT_MAX, resultYMax = - FLT_MAX; - float asymptote[2] = {FLT_MAX, - FLT_MAX}; - int numberOfPoints; - float xFallback, yFallback[2] = {NAN, NAN}; - float firstResult; - float dXOld, dXPrev, dXNext, yOld, yPrev, yNext; - - /* Look for a point of interest at the center. */ - const float a = center - minDistance - FLT_EPSILON, b = center + FLT_EPSILON, c = center + minDistance + FLT_EPSILON; - const float ya = evaluateAndRound(this, a, context), yb = evaluateAndRound(this, b, context), yc = evaluateAndRound(this, c, context); - if (boundOfIntervalOfDefinitionIsReached(ya, yc) || - boundOfIntervalOfDefinitionIsReached(yc, ya) || - rootExistsOnInterval(ya, yc) || - extremumExistsOnInterval(ya, yb, yc) || ya == yc) - { - resultX[0] = resultX[1] = center; - if (extremumExistsOnInterval(ya, yb, yc) && isExtremum(this, a, b, c, ya, yb, yc, context)) { - resultYMin = resultYMax = yb; - } - } - - /* We search for points of interest by exploring the function leftward from - * the center and then rightward, hence the two iterations. */ - for (int i = 0; i < 2; i++) { - /* Initialize the search parameters. */ - numberOfPoints = 0; - firstResult = NAN; - xFallback = NAN; - dXPrev = i == 0 ? - minDistance : minDistance; - dXNext = dXPrev * stepFactor; - yPrev = evaluateAndRound(this, center + dXPrev, context); - yNext = evaluateAndRound(this, center + dXNext, context); - - while(std::fabs(dXPrev) < maxDistance) { - /* Update the slider. */ - dXOld = dXPrev; - dXPrev = dXNext; - dXNext *= stepFactor; - yOld = yPrev; - yPrev = yNext; - yNext = evaluateAndRound(this, center + dXNext, context); - if (std::isinf(yNext)) { - continue; - } - /* Check for a change in the profile. */ - const PointOfInterest variation = boundOfIntervalOfDefinitionIsReached(yPrev, yNext) ? PointOfInterest::Bound : - rootExistsOnInterval(yPrev, yNext) ? PointOfInterest::Root : - extremumExistsOnInterval(yOld, yPrev, yNext) ? PointOfInterest::Extremum : - PointOfInterest::None; - switch (static_cast(variation)) { - /* The fall through is intentional, as we only want to update the Y - * range when an extremum is detected, but need to update the X range - * in all cases. */ - case static_cast(PointOfInterest::Extremum): - if (isExtremum(this, center + dXOld, center + dXPrev, center + dXNext, yOld, yPrev, yNext, context)) { - resultYMin = std::min(resultYMin, yPrev); - resultYMax = std::max(resultYMax, yPrev); - } - case static_cast(PointOfInterest::Bound): - /* We only count extrema / discontinuities for limiting the number - * of points. This prevents cos(x) and cos(x)+2 from having different - * profiles. */ - if (++numberOfPoints == maxNumberOfPoints) { - /* When too many points are encountered, we prepare their erasure by - * setting a restore point. */ - xFallback = dXNext + center; - yFallback[0] = resultYMin; - yFallback[1] = resultYMax; - } - case static_cast(PointOfInterest::Root): - asymptote[i] = i == 0 ? FLT_MAX : - FLT_MAX; - resultX[0] = std::min(resultX[0], center + (i == 0 ? dXNext : dXPrev)); - resultX[1] = std::max(resultX[1], center + (i == 1 ? dXNext : dXPrev)); - if (std::isnan(firstResult)) { - firstResult = dXNext; - } - break; - default: - const float slopeNext = (yNext - yPrev) / (dXNext - dXPrev), slopePrev = (yPrev - yOld) / (dXPrev - dXOld); - if ((std::fabs(slopeNext) < asymptoteThreshold) && (std::fabs(slopePrev) > asymptoteThreshold)) { - // Horizontal asymptote begins - asymptote[i] = (i == 0) ? std::min(asymptote[i], center + dXNext) : std::max(asymptote[i], center + dXNext); - } else if ((std::fabs(slopeNext) < asymptoteThreshold) && (std::fabs(slopePrev) > asymptoteThreshold)) { - // Horizontal asymptote invalidates : it might be an asymptote when - // going the other way. - asymptote[1 - i] = (i == 1) ? std::min(asymptote[1 - i], center + dXPrev) : std::max(asymptote[1 - i], center + dXPrev); - } - } - } - if (std::fabs(resultX[i]) > std::fabs(firstResult) * maxRatioBetweenPoints && !std::isnan(xFallback)) { - /* When there are too many points, cut them if their orders are too - * different. */ - resultX[i] = xFallback; - resultYMin = yFallback[0]; - resultYMax = yFallback[1]; - } - } - - if (tuneXRange) { - /* Cut after horizontal asymptotes. */ - resultX[0] = std::min(resultX[0], asymptote[0]); - resultX[1] = std::max(resultX[1], asymptote[1]); - if (resultX[0] >= resultX[1]) { - /* Fallback to default range. */ - resultX[0] = - Range1D::k_default; - resultX[1] = Range1D::k_default; - } else { - /* Add breathing room around points of interest. */ - float xRange = resultX[1] - resultX[0]; - resultX[0] -= breathingRoom * xRange; - resultX[1] += breathingRoom * xRange; - /* Round to the next integer. */ - resultX[0] = std::floor(resultX[0]); - resultX[1] = std::ceil(resultX[1]); - } - *xMin = std::min(resultX[0], *xMin); - *xMax = std::max(resultX[1], *xMax); - } - *yMin = std::min(resultYMin, *yMin); - *yMax = std::max(resultYMax, *yMax); - - refinedYRangeForDisplay(*xMin, *xMax, yMin, yMax, context); -} - -void ContinuousFunction::refinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Context * context) const { - /* This methods computes the Y range that will be displayed for the cartesian - * function, given an X range (xMin, xMax) and bounds yMin and yMax that must - * be inside the Y range.*/ - assert(plotType() == PlotType::Cartesian); - assert(yMin && yMax); - - constexpr int sampleSize = Ion::Display::Width / 4; - constexpr float boundNegligigbleThreshold = 0.2f; - - float sampleYMin = FLT_MAX, sampleYMax = -FLT_MAX; - const float step = (xMax - xMin) / (sampleSize - 1); - float x, y; - float sum = 0.f; - int pop = 0; - - for (int i = 1; i < sampleSize; i++) { - x = xMin + i * step; - y = privateEvaluateXYAtParameter(x, context).x2(); - sampleYMin = std::min(sampleYMin, y); - sampleYMax = std::max(sampleYMax, y); - if (std::isfinite(y) && std::fabs(y) > FLT_EPSILON) { - sum += std::log(std::fabs(y)); - pop++; - } - } - /* sum/pop is the log mean value of the function, which can be interpreted as - * its average order of magnitude. Then, bound is the value for the next - * order of magnitude and is used to cut the Y range. */ - float bound = (pop > 0) ? std::exp(sum / pop + 1.f) : FLT_MAX; - *yMin = std::min(*yMin, std::max(sampleYMin, -bound)); - *yMax = std::max(*yMax, std::min(sampleYMax, bound)); - if (*yMin == *yMax) { - float d = (*yMin == 0.f) ? 1.f : *yMin * 0.2f; - *yMin -= d; - *yMax += d; - } - /* Round out the smallest bound to 0 if it is negligible compare to the - * other one. This way, we can display the X axis for positive functions such - * as sqrt(x) even if we do not sample close to 0. */ - if (*yMin > 0.f && *yMin / *yMax < boundNegligigbleThreshold) { - *yMin = 0.f; - } else if (*yMax < 0.f && *yMax / *yMin < boundNegligigbleThreshold) { - *yMax = 0.f; - } -} - void * ContinuousFunction::Model::expressionAddress(const Ion::Storage::Record * record) const { return (char *)record->value().buffer+sizeof(RecordDataBuffer); } diff --git a/apps/shared/continuous_function.h b/apps/shared/continuous_function.h index 61674b834..7d1580c01 100644 --- a/apps/shared/continuous_function.h +++ b/apps/shared/continuous_function.h @@ -70,7 +70,7 @@ public: void setTMax(float tMax); float rangeStep() const override { return plotType() == PlotType::Cartesian ? NAN : (tMax() - tMin())/k_polarParamRangeSearchNumberOfPoints; } - void rangeForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Poincare::Context * context, bool tuneXRange = true) const; + void rangeForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Poincare::Context * context, bool tuneXRange = true) const override; // Extremum Poincare::Coordinate2D nextMinimumFrom(double start, double step, double max, Poincare::Context * context) const; @@ -91,10 +91,8 @@ private: Poincare::Coordinate2D nextPointOfInterestFrom(double start, double step, double max, Poincare::Context * context, ComputePointOfInterest compute) const; template Poincare::Coordinate2D privateEvaluateXYAtParameter(T t, Poincare::Context * context) const; - // Ranges void fullXYRange(float * xMin, float * xMax, float * yMin, float * yMax, Poincare::Context * context) const; - void interestingXAndYRangesForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Poincare::Context * context, bool tuneXRange = true) const; - void refinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Poincare::Context * context) const; + void refinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Poincare::Context * context) const override { protectedRefinedYRangeForDisplay(xMin, xMax, yMin, yMax, context, true); } /* RecordDataBuffer is the layout of the data buffer of Record * representing a ContinuousFunction. See comment on diff --git a/apps/shared/function.cpp b/apps/shared/function.cpp index 31b1e01e0..9491c3956 100644 --- a/apps/shared/function.cpp +++ b/apps/shared/function.cpp @@ -1,10 +1,14 @@ #include "function.h" +#include "range_1D.h" #include "poincare_helpers.h" #include "poincare/src/parsing/parser.h" +#include #include -#include -#include +#include #include +#include +#include +#include using namespace Poincare; @@ -74,4 +78,257 @@ Function::RecordDataBuffer * Function::recordData() const { return reinterpret_cast(const_cast(d.buffer)); } +// Ranges +static float evaluateAndRound(const Function * f, float x, Context * context, float precision = 1e-5) { + /* When evaluating sin(x)/x close to zero using the standard sine function, + * one can detect small varitions, while the cardinal sine is supposed to be + * locally monotonous. To smooth our such variations, we round the result of + * the evaluations. As we are not interested in precise results but only in + * ordering, this approximation is sufficient. */ + return precision * std::round(f->evaluateXYAtParameter(x, context).x2() / precision); +} + +/* TODO : These three methods perform checks that will also be relevant for the + * equation solver. Remember to factorize this code when integrating the new + * solver. */ +static bool boundOfIntervalOfDefinitionIsReached(float y1, float y2) { + return std::isfinite(y1) && !std::isinf(y2) && std::isnan(y2); +} +static bool rootExistsOnInterval(float y1, float y2) { + return ((y1 < 0.f && y2 > 0.f) || (y1 > 0.f && y2 < 0.f)); +} +static bool extremumExistsOnInterval(float y1, float y2, float y3) { + return (y1 < y2 && y2 > y3) || (y1 > y2 && y2 < y3); +} + +/* This function checks whether an interval contains an extremum or an + * asymptote, by recursively computing the slopes. In case of an extremum, the + * slope should taper off toward the center. */ +static bool isExtremum(const Function * f, float x1, float x2, float x3, float y1, float y2, float y3, Context * context, int iterations = 3) { + if (iterations <= 0) { + return false; + } + float x[2] = {x1, x3}, y[2] = {y1, y3}; + float xm, ym; + for (int i = 0; i < 2; i++) { + xm = (x[i] + x2) / 2.f; + ym = evaluateAndRound(f, xm, context); + bool res = ((y[i] < ym) != (ym < y2)) ? isExtremum(f, x[i], xm, x2, y[i], ym, y2, context, iterations - 1) : std::fabs(ym - y[i]) >= std::fabs(y2 - ym); + if (!res) { + return false; + } + } + return true; +} + +enum class PointOfInterest : uint8_t { + None, + Bound, + Extremum, + Root +}; + +void Function::rangeForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Context * context, bool tuneXRange) const { + assert(xMin && xMax && yMin && yMax); + + /* Constants of the algorithm. */ + constexpr float defaultMaxInterval = 2e5f; + constexpr float minDistance = 1e-2f; + constexpr float asymptoteThreshold = 2e-1f; + constexpr float stepFactor = 1.1f; + constexpr int maxNumberOfPoints = 3; + constexpr float breathingRoom = 0.3f; + constexpr float maxRatioBetweenPoints = 100.f; + + const bool hasIntervalOfDefinition = std::isfinite(tMin()) && std::isfinite(tMax()); + float center, maxDistance; + if (!tuneXRange) { + center = (*xMax + *xMin) / 2.f; + maxDistance = (*xMax - *xMin) / 2.f; + } else if (hasIntervalOfDefinition) { + center = (tMax() + tMin()) / 2.f; + maxDistance = (tMax() - tMin()) / 2.f; + } else { + center = 0.f; + maxDistance = defaultMaxInterval / 2.f; + } + + float resultX[2] = {FLT_MAX, - FLT_MAX}; + float resultYMin = FLT_MAX, resultYMax = - FLT_MAX; + float asymptote[2] = {FLT_MAX, - FLT_MAX}; + int numberOfPoints; + float xFallback, yFallback[2] = {NAN, NAN}; + float firstResult; + float dXOld, dXPrev, dXNext, yOld, yPrev, yNext; + + /* Look for a point of interest at the center. */ + const float a = center - minDistance - FLT_EPSILON, b = center + FLT_EPSILON, c = center + minDistance + FLT_EPSILON; + const float ya = evaluateAndRound(this, a, context), yb = evaluateAndRound(this, b, context), yc = evaluateAndRound(this, c, context); + if (boundOfIntervalOfDefinitionIsReached(ya, yc) || + boundOfIntervalOfDefinitionIsReached(yc, ya) || + rootExistsOnInterval(ya, yc) || + extremumExistsOnInterval(ya, yb, yc) || ya == yc) + { + resultX[0] = resultX[1] = center; + if (extremumExistsOnInterval(ya, yb, yc) && isExtremum(this, a, b, c, ya, yb, yc, context)) { + resultYMin = resultYMax = yb; + } + } + + /* We search for points of interest by exploring the function leftward from + * the center and then rightward, hence the two iterations. */ + for (int i = 0; i < 2; i++) { + /* Initialize the search parameters. */ + numberOfPoints = 0; + firstResult = NAN; + xFallback = NAN; + dXPrev = i == 0 ? - minDistance : minDistance; + dXNext = dXPrev * stepFactor; + yPrev = evaluateAndRound(this, center + dXPrev, context); + yNext = evaluateAndRound(this, center + dXNext, context); + + while(std::fabs(dXPrev) < maxDistance) { + /* Update the slider. */ + dXOld = dXPrev; + dXPrev = dXNext; + dXNext *= stepFactor; + yOld = yPrev; + yPrev = yNext; + yNext = evaluateAndRound(this, center + dXNext, context); + if (std::isinf(yNext)) { + continue; + } + /* Check for a change in the profile. */ + const PointOfInterest variation = boundOfIntervalOfDefinitionIsReached(yPrev, yNext) ? PointOfInterest::Bound : + rootExistsOnInterval(yPrev, yNext) ? PointOfInterest::Root : + extremumExistsOnInterval(yOld, yPrev, yNext) ? PointOfInterest::Extremum : + PointOfInterest::None; + switch (static_cast(variation)) { + /* The fallthrough is intentional, as we only want to update the Y + * range when an extremum is detected, but need to update the X range + * in all cases. */ + case static_cast(PointOfInterest::Extremum): + if (isExtremum(this, center + dXOld, center + dXPrev, center + dXNext, yOld, yPrev, yNext, context)) { + resultYMin = std::min(resultYMin, yPrev); + resultYMax = std::max(resultYMax, yPrev); + } + case static_cast(PointOfInterest::Bound): + /* We only count extrema / discontinuities for limiting the number + * of points. This prevents cos(x) and cos(x)+2 from having different + * profiles. */ + if (++numberOfPoints == maxNumberOfPoints) { + /* When too many points are encountered, we prepare their erasure by + * setting a restore point. */ + xFallback = dXNext + center; + yFallback[0] = resultYMin; + yFallback[1] = resultYMax; + } + case static_cast(PointOfInterest::Root): + asymptote[i] = i == 0 ? FLT_MAX : - FLT_MAX; + resultX[0] = std::min(resultX[0], center + (i == 0 ? dXNext : dXPrev)); + resultX[1] = std::max(resultX[1], center + (i == 1 ? dXNext : dXPrev)); + if (std::isnan(firstResult)) { + firstResult = dXNext; + } + break; + default: + const float slopeNext = (yNext - yPrev) / (dXNext - dXPrev), slopePrev = (yPrev - yOld) / (dXPrev - dXOld); + if ((std::fabs(slopeNext) < asymptoteThreshold) && (std::fabs(slopePrev) > asymptoteThreshold)) { + // Horizontal asymptote begins + asymptote[i] = (i == 0) ? std::min(asymptote[i], center + dXNext) : std::max(asymptote[i], center + dXNext); + } else if ((std::fabs(slopeNext) < asymptoteThreshold) && (std::fabs(slopePrev) > asymptoteThreshold)) { + // Horizontal asymptote invalidates : it might be an asymptote when + // going the other way. + asymptote[1 - i] = (i == 1) ? std::min(asymptote[1 - i], center + dXPrev) : std::max(asymptote[1 - i], center + dXPrev); + } + } + } + if (std::fabs(resultX[i]) > std::fabs(firstResult) * maxRatioBetweenPoints && !std::isnan(xFallback)) { + /* When there are too many points, cut them if their orders are too + * different. */ + resultX[i] = xFallback; + resultYMin = yFallback[0]; + resultYMax = yFallback[1]; + } + } + + if (tuneXRange) { + /* Cut after horizontal asymptotes. */ + resultX[0] = std::min(resultX[0], asymptote[0]); + resultX[1] = std::max(resultX[1], asymptote[1]); + if (resultX[0] >= resultX[1]) { + /* Fallback to default range. */ + resultX[0] = - Range1D::k_default; + resultX[1] = Range1D::k_default; + } else { + /* Add breathing room around points of interest. */ + float xRange = resultX[1] - resultX[0]; + resultX[0] -= breathingRoom * xRange; + resultX[1] += breathingRoom * xRange; + /* Round to the next integer. */ + resultX[0] = std::floor(resultX[0]); + resultX[1] = std::ceil(resultX[1]); + } + *xMin = std::min(resultX[0], *xMin); + *xMax = std::max(resultX[1], *xMax); + } + *yMin = std::min(resultYMin, *yMin); + *yMax = std::max(resultYMax, *yMax); + + refinedYRangeForDisplay(*xMin, *xMax, yMin, yMax, context); +} + +void Function::protectedRefinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Context * context, bool boundByMagnitude) const { + /* This methods computes the Y range that will be displayed for cartesian + * functions and sequences, given an X range (xMin, xMax) and bounds yMin and + * yMax that must be inside the Y range.*/ + assert(yMin && yMax); + + constexpr int sampleSize = Ion::Display::Width / 4; + constexpr float boundNegligigbleThreshold = 0.2f; + + float sampleYMin = FLT_MAX, sampleYMax = -FLT_MAX; + const float step = (xMax - xMin) / (sampleSize - 1); + float x, y; + float sum = 0.f; + int pop = 0; + + for (int i = 1; i < sampleSize; i++) { + x = xMin + i * step; + y = evaluateXYAtParameter(x, context).x2(); + if (!std::isfinite(y)) { + continue; + } + sampleYMin = std::min(sampleYMin, y); + sampleYMax = std::max(sampleYMax, y); + if (std::fabs(y) > FLT_EPSILON) { + sum += std::log(std::fabs(y)); + pop++; + } + } + /* sum/pop is the log mean value of the function, which can be interpreted as + * its average order of magnitude. Then, bound is the value for the next + * order of magnitude and is used to cut the Y range. */ + if (boundByMagnitude) { + float bound = (pop > 0) ? std::exp(sum / pop + 1.f) : FLT_MAX; + sampleYMin = std::max(sampleYMin, - bound); + sampleYMax = std::min(sampleYMax, bound); + } + *yMin = std::min(*yMin, sampleYMin); + *yMax = std::max(*yMax, sampleYMax); + if (*yMin == *yMax) { + float d = (*yMin == 0.f) ? 1.f : *yMin * 0.2f; + *yMin -= d; + *yMax += d; + } + /* Round out the smallest bound to 0 if it is negligible compare to the + * other one. This way, we can display the X axis for positive functions such + * as sqrt(x) even if we do not sample close to 0. */ + if (*yMin > 0.f && *yMin / *yMax < boundNegligigbleThreshold) { + *yMin = 0.f; + } else if (*yMax < 0.f && *yMax / *yMin < boundNegligigbleThreshold) { + *yMax = 0.f; + } +} + } diff --git a/apps/shared/function.h b/apps/shared/function.h index 7cfad9a11..c755a0675 100644 --- a/apps/shared/function.h +++ b/apps/shared/function.h @@ -53,6 +53,10 @@ public: virtual Poincare::Coordinate2D evaluateXYAtParameter(float t, Poincare::Context * context) const = 0; virtual Poincare::Coordinate2D evaluateXYAtParameter(double t, Poincare::Context * context) const = 0; virtual Poincare::Expression sumBetweenBounds(double start, double end, Poincare::Context * context) const = 0; + + // Range + virtual void rangeForDisplay(float * xMin, float * xMax, float * yMin, float * yMax, Poincare::Context * context, bool tuneXRange = true) const; + protected: /* RecordDataBuffer is the layout of the data buffer of Record * representing a Function. We want to avoid padding which would: @@ -88,7 +92,12 @@ protected: #endif bool m_active; }; + + void protectedRefinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Poincare::Context * context, bool boundByMagnitude) const; + private: + virtual void refinedYRangeForDisplay(float xMin, float xMax, float * yMin, float * yMax, Poincare::Context * context) const = 0; + RecordDataBuffer * recordData() const; }; diff --git a/apps/shared/function_graph_controller.cpp b/apps/shared/function_graph_controller.cpp index 949c92505..65a6653c0 100644 --- a/apps/shared/function_graph_controller.cpp +++ b/apps/shared/function_graph_controller.cpp @@ -152,4 +152,53 @@ int FunctionGraphController::numberOfCurves() const { return functionStore()->numberOfActiveFunctions(); } +void FunctionGraphController::interestingRanges(InteractiveCurveViewRange * range) const { + privateComputeRanges(true, range); +} + +Shared::InteractiveCurveViewRangeDelegate::Range FunctionGraphController::computeYRange(Shared::InteractiveCurveViewRange * interactiveCurveViewRange) { + InteractiveCurveViewRange tempRange = *interactiveCurveViewRange; + tempRange.setYAuto(false); + privateComputeRanges(false, &tempRange); + return Shared::InteractiveCurveViewRangeDelegate::Range{.min = tempRange.yMin(), .max = tempRange.yMax()}; +} + +void FunctionGraphController::privateComputeRanges(bool tuneXRange, InteractiveCurveViewRange * range) const { + Poincare::Context * context = textFieldDelegateApp()->localContext(); + float resultXMin = tuneXRange ? FLT_MAX : range->xMin(); + float resultXMax = tuneXRange ? -FLT_MAX : range->xMax(); + float resultYMin = FLT_MAX; + float resultYMax = -FLT_MAX; + assert(functionStore()->numberOfActiveFunctions() > 0); + int functionsCount = functionStore()->numberOfActiveFunctions(); + for (int i = 0; i < functionsCount; i++) { + ExpiringPointer f = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(i)); + f->rangeForDisplay(&resultXMin, &resultXMax, &resultYMin, &resultYMax, context, tuneXRange); + } + + range->setXMin(resultXMin); + range->setXMax(resultXMax); + range->setYMin(resultYMin); + range->setYMax(resultYMax); + /* We can only call this method once the X range has been fully computed. */ + yRangeForCursorFirstMove(range); +} + +void FunctionGraphController::yRangeForCursorFirstMove(InteractiveCurveViewRange * range) const { + Poincare::Context * context = textFieldDelegateApp()->localContext(); + assert(functionStore()->numberOfActiveFunctions() > 0); + int functionsCount = functionStore()->numberOfActiveFunctions(); + + float cursorStep = range->xGridUnit() / k_numberOfCursorStepsInGradUnit; + float yN, yP; + + for (int i = 0; i < functionsCount; i++) { + ExpiringPointer f = functionStore()->modelForRecord(functionStore()->activeRecordAtIndex(i)); + yN = f->evaluateXYAtParameter(range->xCenter() - cursorStep, context).x2(); + yP = f->evaluateXYAtParameter(range->xCenter() + cursorStep, context).x2(); + range->setYMin(std::min(range->yMin(), std::min(yN, yP))); + range->setYMax(std::max(range->yMax(), std::max(yN, yP))); + } +} + } diff --git a/apps/shared/function_graph_controller.h b/apps/shared/function_graph_controller.h index 245e1876e..72717664b 100644 --- a/apps/shared/function_graph_controller.h +++ b/apps/shared/function_graph_controller.h @@ -20,6 +20,8 @@ public: void didBecomeFirstResponder() override; void viewWillAppear() override; + void interestingRanges(Shared::InteractiveCurveViewRange * range) const override; + protected: float cursorTopMarginRatio() override { return 0.068f; } void reloadBannerView() override; @@ -40,6 +42,10 @@ protected: void initCursorParameters() override; CurveView * curveView() override; + Range computeYRange(Shared::InteractiveCurveViewRange * interactiveCurveViewRange) override; + void privateComputeRanges(bool tuneXRange, Shared::InteractiveCurveViewRange * range) const; + void yRangeForCursorFirstMove(Shared::InteractiveCurveViewRange * range) const; + private: virtual FunctionGraphView * functionGraphView() = 0; virtual FunctionCurveParameterController * curveParameterController() = 0; diff --git a/apps/shared/interactive_curve_view_range_delegate.h b/apps/shared/interactive_curve_view_range_delegate.h index 6da8921a2..e5c05a921 100644 --- a/apps/shared/interactive_curve_view_range_delegate.h +++ b/apps/shared/interactive_curve_view_range_delegate.h @@ -9,9 +9,9 @@ class InteractiveCurveViewRange; class InteractiveCurveViewRangeDelegate { public: + static constexpr float k_defaultXHalfRange = 10.0f; bool didChangeRange(InteractiveCurveViewRange * interactiveCurveViewRange); - virtual float interestingXMin() const { return -interestingXHalfRange(); } - virtual float interestingXHalfRange() const { return 10.0f; } + virtual float interestingXMin() const { return -k_defaultXHalfRange; } virtual bool defaultRangeIsNormalized() const { return false; } virtual void interestingRanges(InteractiveCurveViewRange * range) const { assert(false); } virtual float addMargin(float x, float range, bool isVertical, bool isMin) = 0;