@hugov/metanorm
Version:
random number generator for specified confidence interval
122 lines (114 loc) • 3.88 kB
JavaScript
import icdf from 'norm-dist/icdf-voutier.js'
import parser from './parser.js'
/**
* Parser
* @param {TemplateStringsArray} strings - An array of string literals from the template.
* @param {...string} values - The interpolated values from the template.
* @returns {number => number} - random number generator
*/
export function parse(...args) {
const {points, options, risks} = parser(...args)
return metanorm(...points, options)
}
/**
* MetaNormal Distribution
* @param {...number} points - confidence interval low end at (1-conf)/2
* @param {Object} [options] - min, max, confidence interval
* @returns {number => number} - random number generator
*/
export default function metanorm(...args) {
const {points, options} = {
points:args,
options: typeof args[args.length-1] === 'object' ? args.pop() : {}
}
const {min, max, ci=0.8} = options,
low = points[0],
top = points[points.length-1]
for (let i=1; i<points.length; i++)
if (points[i-1] >= points[i]) throw Error(`out of order points: ${points[i-1]} >= ${points[i]}`)
if (max !== undefined && max <= top) throw Error(`max <= ${top}`)
if (min !== undefined && low <= min) throw Error(`${low} <= min`)
// 2bounds
if (min !== undefined && max !== undefined) {
const [a1,a2,a3,k] = params(ci, points.map(x => Math.log( (x-min)/(max-x) )))
if (a3 === 0) {
return z => {
const q = Math.exp( a1 + a2*z )
return q === Infinity ? max : ( min + max*q ) / ( 1 + q )
}
} else {
return z => {
if (z >= Number.MAX_VALUE) return max
if (z <= -Number.MAX_VALUE) return min
const q = Math.exp( a1 + z*(a2 + a3*z/(1+k*Math.abs(z)) ))
return q >= Number.MAX_VALUE ? max : ( min + max*q ) / ( 1 + q )
}
}
}
// min bound
if (min !== undefined) {
const [a1,a2,a3,k] = params(ci, points.map(x => Math.log( (x-min) )))
if (a3 === 0)
return z => min + Math.exp( a1 + a2*z )
else
return z => z>0
? (z===Infinity ? z : min + Math.exp( a1 + z*(a2 + a3*z/(1+k*z) )))
: (z===-Infinity? min : min + Math.exp( a1 + z*(a2 + a3*z/(1-k*z)) ))
}
// max bound
if (max !== undefined) {
const [a1,a2,a3,k] = params(ci, points.map(x => -Math.log( (max-x) )))
if (a3 === 0)
return z => max - Math.exp( -a1 - a2*z )
else
return z => z>0
? (z===Infinity ? max : max - Math.exp( -a1 - z*(a2 + a3*z/(1+k*z) )) )
: (z=== -Infinity ? z : max - Math.exp( -a1 - z*(a2 + a3*z/(1-k*z) )) )
}
// no bounds
const [a1,a2,a3,k] = params(ci, points)
if (a3 === 0)
return z => a1 + a2*z
else
return z => z+1===z ? z : a1 + z*(a2 + a3*z/(1+k*Math.abs(z)))
}
// x(z) = med + s*z + t*z*z/(1+|z|)
function params(ci, points) {
if (points.length === 0) return [0, 1, 0, 0] // default: m=0,s=1,t=0,k=0
if (points.length === 1) return [points[0], 1, 0, 0] // default: s=1,t=0,k=0
const l = points[0],
t = points[points.length-1],
zq = icdf( 0.5 + ci/2 )
if (points.length === 2) return [(t+l)/2, (t-l)/(2*zq), 0, 0]
const m = points[1],
α = 2*(m-l)/(t-l) - 1,
c = Math.abs(α) + Number.EPSILON, // safety against precision errors
k = c / (zq*(1-c))
return k > Number.MIN_VALUE
? [m, (t-l)/(2*zq), k*(t+l-2*m)*(1+k*zq)/(2*k*zq*zq), k]
: [(t+l)/2, (t-l)/(2*zq), 0, 0]
}
/**
* x = m + z*(a2 + (a3*k)*z/(1+|k*z|))
*
* t = m + a2*zq + a3*zq*zq/(1+k*zq)
* l = m - a2*zq + a3*zq*zq/(1+k*zq)
*
* (t-l)/2 = a2*z
* ==> a2 = (t-l)/2*zq
*
* (t+l)/2 = m + a3*k*zq*zq/(1+k*zq)
* a3 = (t+l-2m)(1+k*zq)/(2k*zq*zq)
* *** α = 2(m-l)/(h-l) - 1 -1 < α < 1 ***
* α = (2m-(h+l))/(h-l)
* ==> a3 = -(t-l)α(1+k*zq)/(2k*zq*zq)
*
* no sign change @ infinity: (a2 + a3*z/(1+|k*z|)) > 0
* (a2 + a3*z/(1+|k*z|)) > 0
* (1+|k*z|)/kz > - a3/a2
* 1/kz + |z|/z > α(1+k*zq)/k*zq
* 1/kz + |z|/z > α/kzq + α
* |z|/z - α > α/kzq - 1/kz
* k > (α/zq - 1/z) / (|z|/z - α)
* *** k > |α| / (zq (1-|α|)) ***
*/