ds-algo-study
Version:
Just experimenting with publishing a package
175 lines (168 loc) • 7.38 kB
JavaScript
var debug = require( 'debug' )( 'dtw' );
var validate = require( './validate' );
var matrix = require( './matrix' );
var comparison = require( './comparison' );
function validateOptions( options ) {
if ( typeof options !== 'object' ) {
throw new TypeError( 'Invalid options type: expected an object' );
} else if ( typeof options.distanceMetric !== 'string' && typeof options.distanceFunction !== 'function' ) {
throw new TypeError( 'Invalid distance types: expected a string distance type or a distance function' );
} else if ( typeof options.distanceMetric === 'string' && typeof options.distanceFunction === 'function' ) {
throw new Error( 'Invalid parameters: provide either a distance metric or function but not both' );
}
if ( typeof options.distanceMetric === 'string' ) {
var normalizedDistanceMetric = options.distanceMetric.toLowerCase();
if ( normalizedDistanceMetric !== 'manhattan' && normalizedDistanceMetric !== 'euclidean' &&
normalizedDistanceMetric !== 'squaredeuclidean' ) {
throw new Error( 'Invalid parameter value: Unknown distance metric \'' + options.distanceMetric + '\'' );
}
}
}
function retrieveDistanceFunction( distanceMetric ) {
var normalizedDistanceMetric = distanceMetric.toLowerCase();
var distanceFunction = null;
if ( normalizedDistanceMetric === 'manhattan' ) {
distanceFunction = require( './distanceFunctions/manhattan' ).distance;
} else if ( normalizedDistanceMetric === 'euclidean' ) {
distanceFunction = require( './distanceFunctions/euclidean' ).distance;
} else if ( normalizedDistanceMetric === 'squaredeuclidean' ) {
distanceFunction = require( './distanceFunctions/squaredEuclidean' ).distance;
}
return distanceFunction;
}
var DTW = function ( options ) {
var state = {
distanceCostMatrix: null
};
if ( typeof options === 'undefined' ) {
state.distance = require( './distanceFunctions/squaredEuclidean' ).distance;
} else {
validateOptions( options );
if ( typeof options.distanceMetric === 'string' ) {
state.distance = retrieveDistanceFunction( options.distanceMetric );
} else if ( typeof options.distanceFunction === 'function' ) {
state.distance = options.distanceFunction;
}
}
this.compute = function ( firstSequence, secondSequence, window ) {
var cost = Number.POSITIVE_INFINITY;
if ( typeof window === 'undefined' ) {
cost = computeOptimalPath( firstSequence, secondSequence, state );
} else if ( typeof window === 'number' ) {
cost = computeOptimalPathWithWindow( firstSequence, secondSequence, window, state );
} else {
throw new TypeError( 'Invalid window parameter type: expected a number' );
}
return cost;
};
this.path = function () {
var path = null;
if ( state.distanceCostMatrix instanceof Array ) {
path = retrieveOptimalPath( state );
}
return path;
};
};
function validateComputeParameters( s, t ) {
validate.sequence( s, 'firstSequence' );
validate.sequence( t, 'secondSequence' );
}
function computeOptimalPath( s, t, state ) {
debug( '> computeOptimalPath' );
validateComputeParameters( s, t );
var start = new Date().getTime();
state.m = s.length;
state.n = t.length;
var distanceCostMatrix = matrix.create( state.m, state.n, Number.POSITIVE_INFINITY );
distanceCostMatrix[ 0 ][ 0 ] = state.distance( s[ 0 ], t[ 0 ] );
for ( var rowIndex = 1; rowIndex < state.m; rowIndex++ ) {
var cost = state.distance( s[ rowIndex ], t[ 0 ] );
distanceCostMatrix[ rowIndex ][ 0 ] = cost + distanceCostMatrix[ rowIndex - 1 ][ 0 ];
}
for ( var columnIndex = 1; columnIndex < state.n; columnIndex++ ) {
var cost = state.distance( s[ 0 ], t[ columnIndex ] );
distanceCostMatrix[ 0 ][ columnIndex ] = cost + distanceCostMatrix[ 0 ][ columnIndex - 1 ];
}
for ( var rowIndex = 1; rowIndex < state.m; rowIndex++ ) {
for ( var columnIndex = 1; columnIndex < state.n; columnIndex++ ) {
var cost = state.distance( s[ rowIndex ], t[ columnIndex ] );
distanceCostMatrix[ rowIndex ][ columnIndex ] =
cost + Math.min(
distanceCostMatrix[ rowIndex - 1 ][ columnIndex ],
distanceCostMatrix[ rowIndex ][ columnIndex - 1 ],
distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ] );
}
}
var end = new Date().getTime();
var time = end - start;
debug( '< computeOptimalPath (' + time + ' ms)' );
state.distanceCostMatrix = distanceCostMatrix;
state.similarity = distanceCostMatrix[ state.m - 1 ][ state.n - 1 ];
return state.similarity;
}
function computeOptimalPathWithWindow( s, t, w, state ) {
debug( '> computeOptimalPathWithWindow' );
validateComputeParameters( s, t );
var start = new Date().getTime();
state.m = s.length;
state.n = t.length;
var window = Math.max( w, Math.abs( s.length - t.length ) );
var distanceCostMatrix = matrix.create( state.m + 1, state.n + 1, Number.POSITIVE_INFINITY );
distanceCostMatrix[ 0 ][ 0 ] = 0;
for ( var rowIndex = 1; rowIndex <= state.m; rowIndex++ ) {
for ( var columnIndex = Math.max( 1, rowIndex - window ); columnIndex <= Math.min( state.n, rowIndex + window ); columnIndex++ ) {
var cost = state.distance( s[ rowIndex - 1 ], t[ columnIndex - 1 ] );
distanceCostMatrix[ rowIndex ][ columnIndex ] =
cost + Math.min(
distanceCostMatrix[ rowIndex - 1 ][ columnIndex ],
distanceCostMatrix[ rowIndex ][ columnIndex - 1 ],
distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ] );
}
}
var end = new Date().getTime();
var time = end - start;
debug( '< computeOptimalPathWithWindow (' + time + ' ms)' );
distanceCostMatrix.shift();
distanceCostMatrix = distanceCostMatrix.map( function ( row ) {
return row.slice( 1, row.length );
} );
state.distanceCostMatrix = distanceCostMatrix;
state.similarity = distanceCostMatrix[ state.m - 1 ][ state.n - 1 ];
return state.similarity;
}
function retrieveOptimalPath( state ) {
debug( '> retrieveOptimalPath' );
var start = new Date().getTime();
var rowIndex = state.m - 1;
var columnIndex = state.n - 1;
var path = [
[ rowIndex, columnIndex ]
];
var epsilon = 1e-14;
while ( ( rowIndex > 0 ) || ( columnIndex > 0 ) ) {
if ( ( rowIndex > 0 ) && ( columnIndex > 0 ) ) {
var min = Math.min(
state.distanceCostMatrix[ rowIndex - 1 ][ columnIndex ],
state.distanceCostMatrix[ rowIndex ][ columnIndex - 1 ],
state.distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ] );
if ( comparison.nearlyEqual( min, state.distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ], epsilon ) ) {
rowIndex--;
columnIndex--;
} else if ( comparison.nearlyEqual( min, state.distanceCostMatrix[ rowIndex - 1 ][ columnIndex ], epsilon ) ) {
rowIndex--;
} else if ( comparison.nearlyEqual( min, state.distanceCostMatrix[ rowIndex ][ columnIndex - 1 ], epsilon ) ) {
columnIndex--;
}
} else if ( ( rowIndex > 0 ) && ( columnIndex === 0 ) ) {
rowIndex--;
} else if ( ( rowIndex === 0 ) && ( columnIndex > 0 ) ) {
columnIndex--;
}
path.push( [ rowIndex, columnIndex ] );
}
var end = new Date().getTime();
var time = end - start;
debug( '< retrieveOptimalPath (' + time + ' ms)' );
return path.reverse();
}
module.exports = DTW;