UNPKG

ds-algo-study

Version:

Just experimenting with publishing a package

229 lines (199 loc) 8.95 kB
# DTW.js ```js 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; } /** * Create a DTWOptions object * @class DTWOptions * @member {string} distanceMetric The distance metric to use: `'manhattan' | 'euclidean' | 'squaredEuclidean'`. * @member {function} distanceFunction The distance function to use. The function should accept two numeric arguments and return the numeric distance. e.g. function (a, b) { return a + b; } */ /** * Create a DTW object * @class DTW */ /** * Initializes a new instance of the `DTW`. If no options are provided the squared euclidean distance function is used. * @function DTW * @param {DTWOptions} [options] The options to initialize the dynamic time warping instance with. */ /** * Computes the optimal match between two provided sequences. * @method compute * @param {number[]} firstSequence The first sequence. * @param {number[]} secondSequence The second sequence. * @param {number} [window] The window parameter (for the locality constraint) to use. * @returns {number} The similarity between the provided temporal sequences. */ /** * Retrieves the optimal match between two provided sequences. * @method path * @returns {number[]} The array containing the optimal path points. */ 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 ], // Insertion distanceCostMatrix[ rowIndex ][ columnIndex - 1 ], // Deletion distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ] ); // Match } } 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 ], // Insertion distanceCostMatrix[ rowIndex ][ columnIndex - 1 ], // Deletion distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ] ); // Match } } 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 ], // Insertion state.distanceCostMatrix[ rowIndex ][ columnIndex - 1 ], // Deletion state.distanceCostMatrix[ rowIndex - 1 ][ columnIndex - 1 ] ); // Match 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; ```