phetsims / calculus-grapher

"Calculus Grapher" is an educational simulation in HTML5, by PhET Interactive Simulations.
GNU General Public License v3.0
4 stars 4 forks source link

Smooth function should handle piecewise function #84

Closed veillette closed 1 year ago

veillette commented 1 year ago

We need to generalized the smooth functionality

The triangle function

Calculus Grapher screenshot (4)

The triangle function after pressing the smooth button once.

Calculus Grapher screenshot (5)

The smooth function does a "Box smoothing", such that piece wise linear function become piecewise quadratic. Although the function f(x) looks smooth, the derivative and second derivative are still box-like.

We could generalize the smooth function to use a Gaussian kernel to smooth the function. This would ensure that all the derivatives would be smooth, even after one press of the smooth button.

veillette commented 1 year ago

I replaced the moving box approach for the smoothing function to a gaussian kernel.

In many ways it is a very similar implementation to the previous smooth method, but it not takes into account variable weights.

As a result of the modification, some of the nomenclature has changed. Since we are using a gaussian function, it is commonly accepted to refer to its "width" using the term standard deviation. As a result, the query parameter associated with the smoothing was renamed to smoothingStandardDeviation.

/**
   * Smooths the curve. Called when the user presses the 'smooth' button.
   *
   * This method uses a weighted-average algorithm for 'smoothing' a curve, using a gaussian kernel
   * see https://en.wikipedia.org/wiki/Kernel_smoother
   */
  public smooth(): void {

    // Save the current values of our Points for the next undoToLastSave call. Note that the current y-values are the
    // same as the previous y-values for all Points in the OriginalCurve.
    this.saveCurrentPoints();

    // gaussian kernel that will be used in the convolution of our curve
    const gaussianFunction = ( x: number ) => Math.exp( -1 / 2 * ( x / STANDARD_DEVIATION ) ** 2 ) /
                                              ( STANDARD_DEVIATION * Math.sqrt( 2 * Math.PI ) );

    // Loop through each Point and set the Point's new y-value.
    this.points.forEach( point => {

      // Flag that tracks the sum of the weighted y-values of all Points
      let weightedY = 0;
      let totalWeight = 0;

      // we want to use the kernel over a number of standard deviations
      // beyond 3 standard deviations, the kernel has very small weight, less than 1%.
      const numberOfStandardDeviations = 3;

      // Loop through each point on BOTH sides of the window, adding the y-value to our total.
      for ( let dx = -numberOfStandardDeviations * STANDARD_DEVIATION;
            dx < numberOfStandardDeviations * STANDARD_DEVIATION;
            dx += 1 / this.pointsPerCoordinate ) {

        // weight of the point
        const weight = gaussianFunction( dx );

        totalWeight += weight;

        // Add the Point's lastSavedY, which was the Point's y-value before the smooth() method was called.
        weightedY += this.getClosestPointAt( point.x + dx ).lastSavedY * weight;
      }

      // Set the Point's new y-value to the weighted average.
      point.y = weightedY / totalWeight;
    } );

    // Signal that this Curve has changed.
    this.curveChangedEmitter.emit();
  }
veillette commented 1 year ago

Here a screenshot after one smooth event:

ALthough f(x) looks similar as above, f' and f'' are clearly not made of piecewise function anymore. Calculus Grapher screenshot (6)

pixelzoom commented 1 year ago

That looks really nice!

veillette commented 1 year ago

This behaves well. Closing