Skip to content

Commit

Permalink
Cleanup temporal mask, see #300
Browse files Browse the repository at this point in the history
  • Loading branch information
samreid committed Jan 6, 2019
1 parent 0713b90 commit e72c92a
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 49 deletions.
85 changes: 36 additions & 49 deletions js/common/model/Scene.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2018, University of Colorado Boulder
// Copyright 2018-2019, University of Colorado Boulder

/**
* The scene determines the medium and wave generator types, coordinate frames, relative scale, etc. For a description
Expand All @@ -23,6 +23,7 @@ define( require => {
const Range = require( 'DOT/Range' );
const Rectangle = require( 'DOT/Rectangle' );
const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
const TemporalMask = require( 'WAVE_INTERFERENCE/common/model/TemporalMask' );
const Util = require( 'DOT/Util' );
const Validator = require( 'AXON/Validator' );
const Vector2 = require( 'DOT/Vector2' );
Expand Down Expand Up @@ -371,8 +372,13 @@ define( require => {
} );
}

// TODO: memory leak, will need to be pruned
this.entries = [];
// @private
this.temporalMask1 = new TemporalMask();

// @private
this.temporalMask2 = new TemporalMask();

// @private
this.stepIndex = 0;
}

Expand All @@ -390,6 +396,11 @@ define( require => {
const isContinuous = ( this.disturbanceTypeProperty.get() === DisturbanceTypeEnum.CONTINUOUS );
const continuous1 = isContinuous && this.continuousWave1OscillatingProperty.get();
const continuous2 = isContinuous && this.continuousWave2OscillatingProperty.get();

// Used to compute whether a delta appears in either mask
let temporalMask1Empty = true;
let temporalMask2Empty = true;

if ( continuous1 || continuous2 || this.pulseFiringProperty.get() ) {

// The simulation is designed to start with a downward wave, corresponding to water splashing in
Expand All @@ -416,25 +427,22 @@ define( require => {
const j = latticeCenterJ + distanceFromCenter;
lattice.setCurrentValue( WaveInterferenceConstants.POINT_SOURCE_HORIZONTAL_COORDINATE, j, waveValue );
this.oscillator1Property.value = waveValue;

this.entries.push( {
stepIndex: this.stepIndex,
sourcePosition: new Vector2( WaveInterferenceConstants.POINT_SOURCE_HORIZONTAL_COORDINATE, j )
} );
this.temporalMask1.set( true, this.stepIndex, j );
temporalMask1Empty = false;
}

// Secondary source (note if there is only one source, this sets the same value as above)
if ( this.continuousWave2OscillatingProperty.get() ) {
const j = latticeCenterJ - distanceFromCenter;
lattice.setCurrentValue( WaveInterferenceConstants.POINT_SOURCE_HORIZONTAL_COORDINATE, j, waveValue );
this.oscillator2Property.value = waveValue;

this.entries.push( {
stepIndex: this.stepIndex,
sourcePosition: new Vector2( WaveInterferenceConstants.POINT_SOURCE_HORIZONTAL_COORDINATE, j )
} );
this.temporalMask2.set( true, this.stepIndex, j );
temporalMask2Empty = false;
}
}

temporalMask1Empty && this.temporalMask1.set( false, this.stepIndex, 0 );
temporalMask2Empty && this.temporalMask2.set( false, this.stepIndex, 0 );
}

/**
Expand Down Expand Up @@ -581,7 +589,11 @@ define( require => {
// Scene-specific physics updates
this.step( dt );

this.applyMask();
// Apply temporal masking, but only for point sources. Plane waves already clear the wave area when changing
// parameters
if ( this.waveSpatialType === WaveSpatialTypeEnum.POINT ) {
this.applyTemporalMask();
}

// Notify listeners about changes
this.lattice.changedEmitter.emit();
Expand All @@ -594,56 +606,31 @@ define( require => {
* we can apply a masking function across the wave area, zeroing out any cell that could note have been generated
* from the source disturbance. This filters out spurious noise and restores "black" for the light scene.
*
* TODO: Letting the wave sit for 3 minutes on iPad air causes it to oversaturate.
* TODO: Letting the wave sit for 3 minutes on iPad air causes it to oversaturate. after several minutes, 2 sources.
* @private
*/
applyMask() {

// I expected the wave speed on the lattice to be 1 or sqrt(2)/2, and was surprised to see that this value
// worked much better empirically. This is a speed in lattice cells per time step, which is the same
// for each scene
const theoreticalRate = Math.sqrt( 2 ) / 3;
const MASKING_TOLERANCE = 1.1;
applyTemporalMask() {

// zero out values that are outside of the mask
for ( let i = 0; i < this.lattice.width; i++ ) {
for ( let j = 0; j < this.lattice.height; j++ ) {

// Brute force search to see if the current cell corresponds to the distance = rate * time of *any* of the
// source disturbances. It is likely that a data structure could simplify the effort here, which may be
// necessary, because this can sometimes drop performance to 5fps on iPad Air.
let matchesMask = false;
for ( let k = 0; k < this.entries.length; k++ ) {
const entry = this.entries[ k ];
const distanceFromCellToSource = entry.sourcePosition.distanceXY( i, j );

const time = this.stepIndex - entry.stepIndex;
const theoreticalDistance = theoreticalRate * time;
if ( Math.abs( theoreticalDistance - distanceFromCellToSource ) <= MASKING_TOLERANCE ) {
matchesMask = true;
break;
}
}
const cameFrom1 = this.temporalMask1.matches( i, j, this.stepIndex );
const cameFrom2 = this.temporalMask2.matches( i, j, this.stepIndex );

if ( !matchesMask ) {
// Math.random()<0.0001 && console.log(cameFrom1,cameFrom2);
if ( !cameFrom1 && !cameFrom2 ) {
this.lattice.setCurrentValue( i, j, 0 );
this.lattice.setLastValue( i, j, 0 );
this.lattice.visitedMatrix.set( i, j, 0 );
}
}
}

// Prune entries. Elements that are too far out of range are eliminated. At the time of writing, this
// caps out around 608 entries with both sources on.
for ( let k = 0; k < this.entries.length; k++ ) {
const entry = this.entries[ k ];

const time = this.stepIndex - entry.stepIndex;
if ( time > Math.sqrt( 2 ) * this.lattice.width / theoreticalRate ) { // d = vt, t=d/v
this.entries.splice( k, 1 );
k--;
}
}
// Prune entries. Elements that are too far out of range are eliminated. Use the diagonal of the lattice for the
// max distance
this.temporalMask1.prune( Math.sqrt( 2 ) * this.lattice.width, this.stepIndex );
this.temporalMask2.prune( Math.sqrt( 2 ) * this.lattice.width, this.stepIndex );
}

/**
Expand Down
113 changes: 113 additions & 0 deletions js/common/model/TemporalMask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2019, University of Colorado Boulder

/**
* Records on and off times of a single source, so that we can determine whether it could have contributed to the value
* on the lattice at a later time.
*
* @author Sam Reid (PhET Interactive Simulations)
*/
define( require => {
'use strict';

// modules
const waveInterference = require( 'WAVE_INTERFERENCE/waveInterference' );
const WaveInterferenceConstants = require( 'WAVE_INTERFERENCE/common/WaveInterferenceConstants' );

// constants
// I expected the wave speed on the lattice to be 1 or sqrt(2)/2, and was surprised to see that this value
// worked much better empirically. This is a speed in lattice cells per time step, which is the same
// for each scene
const waveSpeed = Math.sqrt( 2 ) / 3;
const I = WaveInterferenceConstants.POINT_SOURCE_HORIZONTAL_COORDINATE;

class TemporalMask {

/**
*/
constructor() {

// @private - record of {on, time,j} of changes in wave disturbance sources.
this.deltas = [];
}

/**
* Set the current state of the model. If this differs from the prior state type (in position or whether it is on)
* a delta is generated.
* @param {boolean} on - true if the source is on, false if the source is off
* @param {number} time - integer number of times the wave has been stepped on the lattice
* @param {number} j - vertical lattice coordinate
* @public
*/
set( on, time, j ) {
const lastDelta = this.deltas.length > 0 ? this.deltas[ this.deltas.length - 1 ] : null;
if ( this.deltas.length === 0 || lastDelta.on !== on || lastDelta.j !== j ) {

// record a delta
this.deltas.push( {
on: on,
time: time,
j: j
} );
}
}

/**
* Determines if the wave source was turned on at a time that contributed to the cell value
* @param {number} i - horizontal coordinate on the lattice
* @param {number} j - vertical coordinate on the lattice
* @param {number} time - integer number of times the wave has been stepped on the lattice
* @returns {boolean}
* @public
*/
matches( i, j, time ) {

// search back through time to see if the source contributed to the value at (i,j) at the current time
for ( let k = 0; k < this.deltas.length; k++ ) {
const delta = this.deltas[ k ];
if ( delta.on ) {

const di = I - i;
const dj = delta.j - j;
const distance = Math.sqrt( di * di + dj * dj );

// Find out when this delta is in effect
const startTime = delta.time;
const endTime = this.deltas[ k + 1 ] ? this.deltas[ k + 1 ].time : time;

const theoreticalTime = time - distance / waveSpeed;

// if theoreticalDistance matches any time in this range, then we have a winner
if ( theoreticalTime >= startTime && theoreticalTime <= endTime ) {

// Return as early as possible to improve performance
return true;
}
}
}

return false;
}

/**
* Remove delta values that are so old they can no longer impact the model, to avoid memory leaks.
* @param {number} maxDistance - the furthest a point can be from a source
* @param {number} time - integer number of times the wave has been stepped on the lattice
* @public
*/
prune( maxDistance, time ) {
for ( let k = 0; k < this.deltas.length; k++ ) {
const delta = this.deltas[ k ];

const time = this.time - delta.time;

// max time is across the diagonal of the lattice
if ( time > maxDistance / waveSpeed ) { // d = vt, t=d/v
this.deltas.splice( k, 1 );
k--;
}
}
}
}

return waveInterference.register( 'TemporalMask', TemporalMask );
} );

0 comments on commit e72c92a

Please sign in to comment.