Skip to content

Commit

Permalink
Replace base curve by originalCurve (see #275)
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Veillette <[email protected]>
  • Loading branch information
veillette committed Mar 14, 2023
1 parent f8eb09d commit 9c2c617
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 75 deletions.
44 changes: 20 additions & 24 deletions js/common/model/DerivativeCurve.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// Copyright 2020-2023, University of Colorado Boulder

/**
* DerivativeCurve is a Curve subclass for a curve that represents the derivative of a 'base' curve. It is used
* as both the first derivative and second derivative of the original curve.
* DerivativeCurve is a Curve subclass for a curve that represents the derivative of a Curve. It is used
* to evaluate the first derivative of the original curve.
*
* DerivativeCurves' main responsibility is to observe when the 'base' Curve changes and differentiates it and update
* DerivativeCurves' main responsibility is to observe when the original Curve changes and differentiates it and update
* the Points of the derivative. Derivatives are computed by considering the slope of the secant lines from both sides
* of every point. For a general background on differentiation, see
* https://en.wikipedia.org/wiki/Derivative#Rigorous_definition.
*
*
* Like Curve, DerivativeCurve is created at the start and persists for the lifetime of the simulation. Links
* are left as-is and DerivativeCurves are never disposed.
*
Expand All @@ -23,58 +22,55 @@ import Curve from './Curve.js';

export default class DerivativeCurve extends Curve {

// Reference to the 'base' Curve that was passed-in.
private readonly baseCurve: Curve;
// Reference to the originalCurve that was passed-in.
private readonly originalCurve: Curve;

/**
* @param baseCurve - the curve to differentiate to get the values of this DerivativeCurve
* @param originalCurve - the curve to differentiate to get the values for this DerivativeCurve
* @param tandem
*/
public constructor( baseCurve: Curve, tandem: Tandem ) {
public constructor( originalCurve: Curve, tandem: Tandem ) {

super( {

// CurveOptions
xRange: baseCurve.xRange,
numberOfPoints: baseCurve.numberOfPoints,
xRange: originalCurve.xRange,
numberOfPoints: originalCurve.numberOfPoints,
tandem: tandem
} );

this.baseCurve = baseCurve;
this.originalCurve = originalCurve;

// Observes when the 'base' Curve changes and update this curve to represent the derivative of the 'base' Curve.
// Observes when the originalCurve changes and update this curve to represent the derivative.
// Listener is never removed since DerivativeCurves are never disposed.
baseCurve.curveChangedEmitter.addListener( this.updateDerivative.bind( this ) );
originalCurve.curveChangedEmitter.addListener( this.updateDerivative.bind( this ) );

// Makes the initial call to updateDerivative() to match the 'base' Curve upon initialization.
// Makes the initial call to updateDerivative() to match the originalCurve upon initialization.
this.updateDerivative();
}

/**
* Updates the y-values of the DerivativeCurve to represent the derivative of the 'base' Curve.
* Updates the y-values of the DerivativeCurve to represent the derivative of the originalCurve.
*
* The derivative is approximated as the slope of the secant line between each adjacent Point.
* Our version considers both the slope of the secant lines from the left and right side of every point. See
* https://en.wikipedia.org/wiki/Numerical_differentiation
*
* Since the 'Calculus Grapher' sim has second derivatives, the 'base' curve could have cusps and/or non-finite
* points. The algorithm for computing derivatives works by iterating through each Point of the 'base' Curve.
*
* TODO https://github.com/phetsims/calculus-grapher/issues/110 add documentation
*/
private updateDerivative(): void {

const basePoints = this.baseCurve.points;
const originalPoints = this.originalCurve.points;

const length = basePoints.length;
const length = originalPoints.length;

let leftSlope: number | null;
let rightSlope: number | null;

for ( let index = 0; index < length; index++ ) {
const previousPoint = index > 0 ? basePoints[ index - 1 ] : null;
const point = basePoints[ index ];
const nextPoint = index < length - 1 ? basePoints[ index + 1 ] : null;
const previousPoint = index > 0 ? originalPoints[ index - 1 ] : null;
const point = originalPoints[ index ];
const nextPoint = index < length - 1 ? originalPoints[ index + 1 ] : null;


if ( previousPoint === null || point.isCusp && previousPoint.isCusp || ( point.isDiscontinuous && previousPoint.isDiscontinuous ) ) {
Expand All @@ -92,7 +88,7 @@ export default class DerivativeCurve extends Curve {
}

if ( typeof leftSlope === 'number' && typeof rightSlope === 'number' ) {
// If both the left and right adjacent Points of the Point of the 'base' curve exist, the derivative is
// If both the left and right adjacent Points of the Point of the originalCurve exist, the derivative is
// the average of the slopes if they are approximately equal. Otherwise, the derivative doesn't exist.
this.points[ index ].y = ( leftSlope + rightSlope ) / 2;
}
Expand Down
44 changes: 19 additions & 25 deletions js/common/model/IntegralCurve.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,65 @@
// Copyright 2020-2023, University of Colorado Boulder

/**
* IntegralCurve is a Curve subclass for the curve that represents the integral of the TransformedCurve.
* The TransformedCurve is referenced as the 'base' Curve of the IntegralCurve.
*
* IntegralCurve's main responsibility is to observe when the 'base' Curve changes and integrate it and update the
* IntegralCurve is a Curve subclass for the curve that represents the integral of originalCurve.
* IntegralCurve's main responsibility is to observe when the Curve changes and integrate it and update the
* Points of the Integral. For a general background on integration, see https://en.wikipedia.org/wiki/Integral. Our
* version uses a trapezoidal Riemann sum to approximate integrals. See https://en.wikipedia.org/wiki/Trapezoidal_rule
* for background. Since the 'base' Curve exists at all Points, the Integral is also finite at all points.
* for background. Since the originalCurve exists at all Points, the Integral is also finite at all points.
*
* Like Curve, IntegralCurve is created at the start and persists for the lifetime of the simulation. Links are left
* as-is and IntegralCurves are never disposed.
*
* @author Brandon Li
* @author Martin Veillette
*/

import Tandem from '../../../../tandem/js/Tandem.js';
import calculusGrapher from '../../calculusGrapher.js';
import Curve from './Curve.js';

export default class IntegralCurve extends Curve {

// Reference to the 'base' Curve that was passed-in.
private readonly baseCurve: Curve;
// Reference to the originalCurve that was passed-in.
private readonly originalCurve: Curve;

/**
* @param baseCurve - the curve to integrate to get the values of this IntegralCurve.
* @param originalCurve - the curve to integrate to get the values of this IntegralCurve.
* @param tandem
*/
public constructor( baseCurve: Curve, tandem: Tandem ) {
public constructor( originalCurve: Curve, tandem: Tandem ) {

super( {

// CurveOptions
xRange: baseCurve.xRange,
numberOfPoints: baseCurve.numberOfPoints,
xRange: originalCurve.xRange,
numberOfPoints: originalCurve.numberOfPoints,
tandem: tandem
} );

this.baseCurve = baseCurve;
this.originalCurve = originalCurve;

// Observes when the 'base' Curve changes and update this curve to represent the integral of the 'base' Curve.
// Observes when the originalCurve changes and update this curve to represent the integral of the originalCurve.
// Listener is never removed since IntegralCurves are never disposed.
baseCurve.curveChangedEmitter.addListener( this.updateIntegral.bind( this ) );
originalCurve.curveChangedEmitter.addListener( this.updateIntegral.bind( this ) );

// Makes the initial call to update the integral to match the 'base' Curve upon initialization.
// Makes the initial call to update the integral to match the originalCurve upon initialization.
this.updateIntegral();
}

/**
* Updates the y-values of the IntegralCurve to represent the integral of the 'base' Curve.
* Updates the y-values of the IntegralCurve to represent the integral of the originalCurve.
*
* The integral is approximated by performing a Riemann Sum. A left Riemann Sum is used
* to determine the area of a series of rectangles to approximate the area under a curve. The left Riemann Sum
* to determine the area from a series of rectangles to approximate the area under a curve. The left Riemann Sum
* uses the left side of the function for the height of the rectangle summing up all
* trapezoidal areas. See https://en.wikipedia.org/wiki/Riemann_sum for more details.
*
* The IntegralCurve exists at all points since TransformedCurve is finite at all points, so we don't need to consider
* non-differentiable or non-finite points of the 'base' curve.
*/
private updateIntegral(): void {

// Loop through each pair of adjacent Points of the base Curve.
for ( let index = 1; index < this.baseCurve.points.length; index++ ) {
const point = this.baseCurve.points[ index ];
const previousPoint = this.baseCurve.points[ index - 1 ];
// Loop through each pair of adjacent points of the original curve.
for ( let index = 1; index < this.originalCurve.points.length; index++ ) {
const point = this.originalCurve.points[ index ];
const previousPoint = this.originalCurve.points[ index - 1 ];

assert && assert( point.isFinite && previousPoint.isFinite );

Expand Down
52 changes: 26 additions & 26 deletions js/common/model/SecondDerivativeCurve.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright 2023, University of Colorado Boulder

/**
* SecondDerivative is a Curve subclass for a curve that represents the second derivative of a 'base' curve.
* SecondDerivative is a Curve subclass for a curve that represents the second derivative of a Curve.
* It is used to evaluate the second derivative of the originalCurve.
*
* SecondDerivatives' main responsibility is to observe when the 'base' Curve changes and differentiates it and update
* SecondDerivatives' main responsibility is to observe when the originalCurve changes and differentiates it and update
* the Points of the second derivative.
*
* Like Curve, SecondDerivative is created at the start and persists for the lifetime of the simulation. Links
Expand All @@ -18,70 +19,69 @@ import Curve from './Curve.js';

export default class SecondDerivativeCurve extends Curve {

// Reference to the 'base' Curve that was passed-in.
private readonly baseCurve: Curve;
// Reference to the originalCurve that was passed-in.
private readonly originalCurve: Curve;

/**
* @param baseCurve - the curve to differentiate to get the values of this SecondDerivative
* @param originalCurve - the curve to differentiate to get the values of this SecondDerivative
* @param tandem
*/
public constructor( baseCurve: Curve, tandem: Tandem ) {
public constructor( originalCurve: Curve, tandem: Tandem ) {

super( {

// CurveOptions
xRange: baseCurve.xRange,
numberOfPoints: baseCurve.numberOfPoints,
xRange: originalCurve.xRange,
numberOfPoints: originalCurve.numberOfPoints,
tandem: tandem
} );

this.baseCurve = baseCurve;
this.originalCurve = originalCurve;

// Observes when the 'base' Curve changes and update this curve to represent the second derivative of the 'base' Curve.
// Observes when the originalCurve changes and update this curve to represent the second derivative of the originalCurve.
// Listener is never removed since SecondDerivative is never disposed.
baseCurve.curveChangedEmitter.addListener( this.updateSecondDerivative.bind( this ) );
originalCurve.curveChangedEmitter.addListener( this.updateSecondDerivative.bind( this ) );

// Makes the initial call to updateSecondDerivative() to match the 'base' Curve upon initialization.
// Makes the initial call to updateSecondDerivative() to match the originalCurve upon initialization.
this.updateSecondDerivative();
}

/**
* Updates the y-values of the SecondDerivative to represent the derivative of the 'base' Curve.
* Updates the y-values of the SecondDerivative to represent the derivative of the originalCurve.
*
* To update the second derivative, we (1) assume that the points are smooth,
* and evaluate the second derivative using the standard finite difference algorithm.
* For points that are not smooth, we correct for the wrong assumption, by assigning
* their second derivative to a smooth point directly next to it.
*
*/
private updateSecondDerivative(): void {

// Convenience variables
const basePoints = this.baseCurve.points;
const length = basePoints.length;
const originalPoints = this.originalCurve.points;
const length = originalPoints.length;

for ( let index = 0; index < length; index++ ) {

// Is the base point smooth?
const isBasePointSmooth = basePoints[ index ].pointType === 'smooth';
// Is the original point smooth?
const isOriginalPointSmooth = originalPoints[ index ].pointType === 'smooth';

// The point type is the same as the base point, unless the base point is not smooth, in which case it must be discontinuous
this.points[ index ].pointType = isBasePointSmooth ? 'smooth' : 'discontinuous';
// The point type is the same as the original point, unless the original point is not smooth, in which case it must be discontinuous
this.points[ index ].pointType = isOriginalPointSmooth ? 'smooth' : 'discontinuous';

// We exclude the first and last point. They will be dealt with later
if ( index !== 0 && index !== length - 1 ) {
const previousPoint = basePoints[ index - 1 ];
const point = basePoints[ index ];
const nextPoint = basePoints[ index + 1 ];
const previousPoint = originalPoints[ index - 1 ];
const point = originalPoints[ index ];
const nextPoint = originalPoints[ index + 1 ];

// Determine the second derivative using the naive assumption that all points are smooth. We will handle exceptions later
// Determine the second derivative using the naive assumption that all original points are smooth. We will handle exceptions later
this.points[ index ].y = ( point.getSlope( nextPoint ) - point.getSlope( previousPoint ) ) / ( 2 * this.deltaX );
}
}

// Handle the y value of the first and last point
this.points[ 0 ].y = ( basePoints[ 1 ].pointType === 'smooth' ) ? this.points[ 1 ].y : 0;
this.points[ length - 1 ].y = ( basePoints[ length - 2 ].pointType === 'smooth' ) ? this.points[ length - 2 ].y : 0;
this.points[ 0 ].y = ( originalPoints[ 1 ].pointType === 'smooth' ) ? this.points[ 1 ].y : 0;
this.points[ length - 1 ].y = ( originalPoints[ length - 2 ].pointType === 'smooth' ) ? this.points[ length - 2 ].y : 0;


// Reiterate over points but this time taking into account the point type
Expand Down

0 comments on commit 9c2c617

Please sign in to comment.