mirror of
https://github.com/UpsilonNumworks/Upsilon.git
synced 2026-01-18 16:27:34 +01:00
[apps/function] Factorize zoom in Function class
The new zoom implemented for ContinuousFunction is now factorized inside Function to benefit the Sequence class. The same things is done to code added to Graph::GraphController, which is moved into FunctionGraphController. This removes the reimplementation of several methods, most notably computeYRange, as the implementation for function is general enough to work on sequences. Change-Id: I9b8211354064f46c3fa3dde3191dcb39d627a1d2
This commit is contained in:
committed by
Émilie Feral
parent
33f9bb50a3
commit
a6db9688cd
@@ -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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user