246 lines
5.5 KiB
JavaScript
246 lines
5.5 KiB
JavaScript
const _math = require("mathjs");
|
|
const math = _math.create(_math.all);
|
|
const jStat = require("jstat");
|
|
|
|
/**
|
|
* This module defines an abstract BinnedDistribution class, which
|
|
* should be implemented for each distribution. You need to decide
|
|
* how to bin the distribution (use _adabin unless there's a nicer
|
|
* way for your distr) and how to choose the distribution's support.
|
|
*/
|
|
|
|
math.import({
|
|
normal: jStat.normal,
|
|
beta: jStat.beta,
|
|
lognormal: jStat.lognormal,
|
|
uniform: jStat.uniform
|
|
});
|
|
|
|
class BaseDistributionBinned {
|
|
/**
|
|
* @param args
|
|
*/
|
|
constructor(args) {
|
|
this._set_props();
|
|
this.max_bin_size = 0.5;
|
|
this.min_bin_size = 0;
|
|
this.increment = 0.001;
|
|
this.desired_delta = 0.01;
|
|
this.start_bin_size = 0.01;
|
|
|
|
[this.params, this.pdf_func, this.sample] = this.get_params_and_pdf_func(
|
|
args
|
|
);
|
|
|
|
[this.start_point, this.end_point] = this.get_bounds();
|
|
[this.pdf_vals, this.divider_pts] = this.bin();
|
|
}
|
|
|
|
/**
|
|
* this is hacky but class properties aren't always supported
|
|
* @private
|
|
*/
|
|
_set_props() {
|
|
throw new Error("NotImplementedError");
|
|
}
|
|
|
|
/**
|
|
* @returns {(number[]|[*])[]}
|
|
* @private
|
|
*/
|
|
_adabin() {
|
|
let point = this.start_point;
|
|
let vals = [this.pdf_func(point)];
|
|
let divider_pts = [point];
|
|
let support = this.end_point - this.start_point;
|
|
let bin_size = this.start_bin_size * support;
|
|
|
|
while (point < this.end_point) {
|
|
let val = this.pdf_func(point + bin_size);
|
|
if (Math.abs(val - vals[vals.length - 1]) > this.desired_delta) {
|
|
while (
|
|
(Math.abs(val - vals[vals.length - 1]) > this.desired_delta) &
|
|
(bin_size - this.increment * support > this.min_bin_size)
|
|
) {
|
|
bin_size -= this.increment;
|
|
val = this.pdf_func(point + bin_size);
|
|
}
|
|
} else if (Math.abs(val - vals[vals.length - 1]) < this.desired_delta) {
|
|
while (
|
|
(Math.abs(val - vals[vals.length - 1]) < this.desired_delta) &
|
|
(bin_size < this.max_bin_size)
|
|
) {
|
|
bin_size += this.increment;
|
|
val = this.pdf_func(point + bin_size);
|
|
}
|
|
}
|
|
point += bin_size;
|
|
vals.push(val);
|
|
divider_pts.push(point);
|
|
}
|
|
vals = vals.map((_, idx) => vals[idx] / 2 + vals[idx + 1] / 2);
|
|
vals = vals.slice(0, -1);
|
|
return [vals, divider_pts];
|
|
}
|
|
|
|
bin() {
|
|
throw new Error("NotImplementedError");
|
|
}
|
|
|
|
get_bounds() {
|
|
throw new Error("NotImplementedError");
|
|
}
|
|
|
|
/**
|
|
* @param args
|
|
* @returns {(any|(function(*=): *))[]}
|
|
*/
|
|
get_params_and_pdf_func(args) {
|
|
let args_str = args.toString() + ")";
|
|
let substr = this.name + ".pdf(x, " + args_str;
|
|
let compiled = math.compile(substr);
|
|
|
|
function pdf_func(x) {
|
|
return compiled.evaluate({ x: x });
|
|
}
|
|
|
|
let mc_compiled = math.compile(this.name + ".sample(" + args_str);
|
|
let kv_pairs = this.param_names.map((val, idx) => [val, args[idx]]);
|
|
let params = Object.fromEntries(new Map(kv_pairs));
|
|
return [params, pdf_func, mc_compiled.evaluate];
|
|
}
|
|
}
|
|
|
|
class NormalDistributionBinned extends BaseDistributionBinned {
|
|
/**
|
|
* @private
|
|
*/
|
|
_set_props() {
|
|
this.name = "normal";
|
|
this.param_names = ["mean", "std"];
|
|
}
|
|
|
|
/**
|
|
* @returns {(number|*)[]}
|
|
*/
|
|
get_bounds() {
|
|
return [
|
|
this.params.mean - 4 * this.params.std,
|
|
this.params.mean + 4 * this.params.std
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @returns {[[*], [*]]}
|
|
*/
|
|
bin() {
|
|
return this._adabin(this.params.std);
|
|
}
|
|
}
|
|
|
|
class UniformDistributionBinned extends BaseDistributionBinned {
|
|
/**
|
|
* @private
|
|
*/
|
|
_set_props() {
|
|
this.name = "uniform";
|
|
this.param_names = ["start_point", "end_point"];
|
|
this.num_bins = 200;
|
|
}
|
|
|
|
/**
|
|
* @returns {*[]}
|
|
*/
|
|
get_bounds() {
|
|
return [this.params.start_point, this.params.end_point];
|
|
}
|
|
|
|
/**
|
|
* @returns {(*[])[]}
|
|
*/
|
|
bin() {
|
|
let divider_pts = evenly_spaced_grid(
|
|
this.params.start_point,
|
|
this.params.end_point,
|
|
this.num_bins
|
|
);
|
|
let vals = divider_pts.map(x =>
|
|
this.pdf_func(this.params.start_point / 2 + this.params.end_point / 2)
|
|
);
|
|
vals = vals.slice(0, -1);
|
|
return [vals, divider_pts];
|
|
}
|
|
}
|
|
|
|
class LogNormalDistributionBinned extends BaseDistributionBinned {
|
|
/**
|
|
* @private
|
|
*/
|
|
_set_props() {
|
|
this.name = "lognormal";
|
|
this.param_names = ["normal_mean", "normal_std"];
|
|
this.n_bounds_samples = 1000;
|
|
this.n_largest_bound_sample = 10;
|
|
}
|
|
|
|
/**
|
|
* @param samples
|
|
* @param n
|
|
* @returns {any}
|
|
* @private
|
|
*/
|
|
_nth_largest(samples, n) {
|
|
var largest_buffer = Array(n).fill(-Infinity);
|
|
for (const sample of samples) {
|
|
if (sample > largest_buffer[n - 1]) {
|
|
var i = n;
|
|
while ((i > 0) & (sample > largest_buffer[i - 1])) {
|
|
i -= 1;
|
|
}
|
|
largest_buffer[i] = sample;
|
|
}
|
|
}
|
|
return largest_buffer[n - 1];
|
|
}
|
|
|
|
/**
|
|
* @returns {(*|any)[]}
|
|
*/
|
|
get_bounds() {
|
|
let samples = Array(this.n_bounds_samples)
|
|
.fill(0)
|
|
.map(() => this.sample());
|
|
return [
|
|
math.min(samples),
|
|
this._nth_largest(samples, this.n_largest_bound_sample)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @returns {[[*], [*]]}
|
|
*/
|
|
bin() {
|
|
return this._adabin();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param start
|
|
* @param stop
|
|
* @param numel
|
|
* @returns {*[]}
|
|
*/
|
|
function evenly_spaced_grid(start, stop, numel) {
|
|
return Array(numel)
|
|
.fill(0)
|
|
.map((_, idx) => start + (idx / numel) * (stop - start));
|
|
}
|
|
|
|
const distrs = {
|
|
normal: NormalDistributionBinned,
|
|
lognormal: LogNormalDistributionBinned,
|
|
uniform: UniformDistributionBinned
|
|
};
|
|
|
|
exports.distrs = distrs;
|