squiggle/src/components/editor/distribution.js
2020-03-03 15:58:53 +03:00

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;