main.js (5759B)
1 /** 2 * @license Apache-2.0 3 * 4 * Copyright (c) 2018 The Stdlib Authors. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 'use strict'; 20 21 // MODULES // 22 23 var isPositiveInteger = require( '@stdlib/assert/is-positive-integer' ).isPrimitive; 24 var isnan = require( '@stdlib/math/base/assert/is-nan' ); 25 var isInfinite = require( '@stdlib/math/base/assert/is-infinite' ); 26 var frexp = require( '@stdlib/math/base/special/frexp' ); 27 var ldexp = require( '@stdlib/math/base/special/ldexp' ); 28 var Float64Array = require( '@stdlib/array/float64' ); 29 30 31 // FUNCTIONS // 32 33 /** 34 * Computes an updated product. 35 * 36 * @private 37 * @param {Array} workspace - workspace array 38 * @param {Object} acc - accumulated fractional and exponent parts 39 * @param {number} x - multiplicative factor 40 * @returns {number} product 41 */ 42 function product( workspace, acc, x ) { 43 // Split the incoming value into a normalized fraction and exponent: 44 frexp( workspace, x ); 45 46 // Update the accumulated fraction: 47 acc.frac *= workspace[ 0 ]; 48 49 // Update the accumulated exponent: 50 acc.exp += workspace[ 1 ]; 51 52 // Ensure fraction remains normalized to avoid overflow/underflow... 53 if ( acc.frac > -0.5 && acc.frac < 0.5 ) { 54 frexp( workspace, acc.frac ); 55 acc.frac = workspace[ 0 ]; 56 acc.exp += workspace[ 1 ]; 57 } 58 return ldexp( acc.frac, acc.exp ); 59 } 60 61 62 // MAIN // 63 64 /** 65 * Returns an accumulator function which incrementally computes a moving product. 66 * 67 * ## Method 68 * 69 * To avoid overflow/underflow, we store the fractional and exponent parts of intermediate results separately. By keeping a normalized fraction, we prevent underflow/overflow of the fraction. Underflow of the exponent is impossible, as IEEE 754 floating-point exponents are integer values. Overflow of the exponent is possible, but highly unlikely. In the worst case, an intermediate exponent is greater than the minimum safe integer, and adding the exponent of an incoming value does not change the intermediate result. While incorrect, such behavior does not lead to exponent overflow. 70 * 71 * While intermediate results are largely immune to overflow and not subject to underflow, this does not mean that returned results will never be zero or infinite. In fact, zero (underflow) and infinite (overflow) results may be transient (i.e., infinity followed by a finite number). 72 * 73 * 74 * ## References 75 * 76 * - Ueberhuber, Christoph W. 1997. _Numerical Computation 1: Methods, Software, and Analysis_. Springer-Verlag Berlin Heidelberg. doi:[10.1007/978-3-642-59118-1](https://doi.org/10.1007/978-3-642-59118-1). 77 * 78 * @param {PositiveInteger} W - window size 79 * @throws {TypeError} must provide a positive integer 80 * @returns {Function} accumulator function 81 * 82 * @example 83 * var accumulator = incrmprod( 3 ); 84 * 85 * var p = accumulator(); 86 * // returns null 87 * 88 * p = accumulator( 2.0 ); 89 * // returns 2.0 90 * 91 * p = accumulator( -5.0 ); 92 * // returns -10.0 93 * 94 * p = accumulator( 3.0 ); 95 * // returns -30.0 96 * 97 * p = accumulator( 5.0 ); 98 * // returns -75.0 99 * 100 * p = accumulator(); 101 * // returns -75.0 102 */ 103 function incrmprod( W ) { 104 var parts; 105 var prod; 106 var buf; 107 var acc; 108 var N; 109 var i; 110 if ( !isPositiveInteger( W ) ) { 111 throw new TypeError( 'invalid argument. Must provide a positive integer. Value: `' + W + '`.' ); 112 } 113 buf = new Float64Array( W ); 114 i = -1; 115 N = 0; 116 117 // Initialize a workspace for `frexp`: 118 parts = [ 0.0, 0 ]; 119 120 // Initial product is 1.0, which may be split into its fractional and exponent parts (0.5 x 2.0**1 = 1.0): 121 prod = 1.0; 122 acc = {}; 123 acc.frac = 0.5; 124 acc.exp = 1.0; 125 126 return accumulator; 127 128 /** 129 * If provided a value, the accumulator function returns an updated prodct. If not provided a value, the accumulator function returns the current prodct. 130 * 131 * @private 132 * @param {number} [x] - input value 133 * @returns {(number|null)} product or null 134 */ 135 function accumulator( x ) { 136 var k; 137 var v; 138 if ( arguments.length === 0 ) { 139 if ( N === 0 ) { 140 return null; 141 } 142 return prod; 143 } 144 // Update the index for managing the circular buffer: 145 i = (i+1) % W; 146 147 // Case: incoming value is NaN, the accumulated value is automatically NaN... 148 if ( isnan( x ) ) { 149 N = W; // explicitly set to avoid `N < W` branch 150 prod = NaN; 151 } 152 // Case: initial window... 153 else if ( N < W ) { 154 N += 1; 155 prod = product( parts, acc, x ); 156 } 157 // Case: outgoing value is a "special" value, and, thus, we need to compute the accumulated value... 158 else if ( 159 buf[ i ] === 0.0 || 160 isnan( buf[ i ] ) || 161 isInfinite( buf[ i ] ) 162 ) { 163 N = 1; 164 acc.frac = 0.5; 165 acc.exp = 1.0; 166 product( parts, acc, x ); 167 for ( k = 0; k < W; k++ ) { 168 if ( k !== i ) { 169 v = buf[ k ]; 170 if ( isnan( v ) ) { 171 N = W; // explicitly set to avoid `N < W` branch 172 prod = NaN; 173 break; // product is automatically NaN, so no need to continue 174 } 175 N += 1; 176 prod = product( parts, acc, v ); 177 } 178 } 179 } 180 // Case: neither the current accumulated value nor the incoming value are NaN, so we need to update the accumulated value... 181 else if ( isnan( prod ) === false ) { 182 v = x / buf[ i ]; 183 prod = product( parts, acc, v ); 184 } 185 // Case: the current accumulated value is NaN, so nothing to do until the buffer no longer contains NaN values... 186 buf[ i ] = x; 187 188 return prod; 189 } 190 } 191 192 193 // EXPORTS // 194 195 module.exports = incrmprod;