104 строки
3.8 KiB
JavaScript
104 строки
3.8 KiB
JavaScript
/**
|
||
* @license
|
||
* Copyright 2017 Google LLC
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
// The exact double values for the max and min scores possible in each range.
|
||
const MIN_PASSING_SCORE = 0.90000000000000002220446049250313080847263336181640625;
|
||
const MAX_AVERAGE_SCORE = 0.899999999999999911182158029987476766109466552734375;
|
||
const MIN_AVERAGE_SCORE = 0.5;
|
||
const MAX_FAILING_SCORE = 0.499999999999999944488848768742172978818416595458984375;
|
||
|
||
/**
|
||
* Approximates the Gauss error function, the probability that a random variable
|
||
* from the standard normal distribution lies within [-x, x]. Moved from
|
||
* traceviewer.b.math.erf, based on Abramowitz and Stegun, formula 7.1.26.
|
||
* @param {number} x
|
||
* @return {number}
|
||
*/
|
||
function erf(x) {
|
||
// erf(-x) = -erf(x);
|
||
const sign = Math.sign(x);
|
||
x = Math.abs(x);
|
||
|
||
const a1 = 0.254829592;
|
||
const a2 = -0.284496736;
|
||
const a3 = 1.421413741;
|
||
const a4 = -1.453152027;
|
||
const a5 = 1.061405429;
|
||
const p = 0.3275911;
|
||
const t = 1 / (1 + p * x);
|
||
const y = t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5))));
|
||
return sign * (1 - y * Math.exp(-x * x));
|
||
}
|
||
|
||
/**
|
||
* Returns the score (1 - percentile) of `value` in a log-normal distribution
|
||
* specified by the `median` value, at which the score will be 0.5, and a 10th
|
||
* percentile value, at which the score will be 0.9. The score represents the
|
||
* amount of the distribution greater than `value`. All values should be in the
|
||
* same units (e.g. milliseconds). See
|
||
* https://www.desmos.com/calculator/o98tbeyt1t
|
||
* for an interactive view of the relationship between these parameters and the
|
||
* typical parameterization (location and shape) of the log-normal distribution.
|
||
* @param {{median: number, p10: number}} parameters
|
||
* @param {number} value
|
||
* @return {number}
|
||
*/
|
||
function getLogNormalScore({median, p10}, value) {
|
||
// Required for the log-normal distribution.
|
||
if (median <= 0) throw new Error('median must be greater than zero');
|
||
if (p10 <= 0) throw new Error('p10 must be greater than zero');
|
||
// Not strictly required, but if p10 > median, it flips around and becomes the p90 point.
|
||
if (p10 >= median) throw new Error('p10 must be less than the median');
|
||
|
||
// Non-positive values aren't in the distribution, so always 1.
|
||
if (value <= 0) return 1;
|
||
|
||
// Closest double to `erfc-1(1/5)`.
|
||
const INVERSE_ERFC_ONE_FIFTH = 0.9061938024368232;
|
||
|
||
// Shape (σ) is `|log(p10/median) / (sqrt(2)*erfc^-1(1/5))|` and
|
||
// standardizedX is `1/2 erfc(log(value/median) / (sqrt(2)*σ))`, so simplify a bit.
|
||
const xRatio = Math.max(Number.MIN_VALUE, value / median); // value and median are > 0, so is ratio.
|
||
const xLogRatio = Math.log(xRatio);
|
||
const p10Ratio = Math.max(Number.MIN_VALUE, p10 / median); // p10 and median are > 0, so is ratio.
|
||
const p10LogRatio = -Math.log(p10Ratio); // negate to keep σ positive.
|
||
const standardizedX = xLogRatio * INVERSE_ERFC_ONE_FIFTH / p10LogRatio;
|
||
const complementaryPercentile = (1 - erf(standardizedX)) / 2;
|
||
|
||
// Clamp to avoid floating-point out-of-bounds issues and keep score in expected range.
|
||
let score;
|
||
if (value <= p10) {
|
||
// Passing. Clamp to [0.9, 1].
|
||
score = Math.max(MIN_PASSING_SCORE, Math.min(1, complementaryPercentile));
|
||
} else if (value <= median) {
|
||
// Average. Clamp to [0.5, 0.9).
|
||
score = Math.max(MIN_AVERAGE_SCORE, Math.min(MAX_AVERAGE_SCORE, complementaryPercentile));
|
||
} else {
|
||
// Failing. Clamp to [0, 0.5).
|
||
score = Math.max(0, Math.min(MAX_FAILING_SCORE, complementaryPercentile));
|
||
}
|
||
return score;
|
||
}
|
||
|
||
/**
|
||
* Interpolates the y value at a point x on the line defined by (x0, y0) and (x1, y1)
|
||
* @param {number} x0
|
||
* @param {number} y0
|
||
* @param {number} x1
|
||
* @param {number} y1
|
||
* @param {number} x
|
||
* @return {number}
|
||
*/
|
||
function linearInterpolation(x0, y0, x1, y1, x) {
|
||
const slope = (y1 - y0) / (x1 - x0);
|
||
return y0 + (x - x0) * slope;
|
||
}
|
||
|
||
export {
|
||
linearInterpolation,
|
||
getLogNormalScore,
|
||
};
|