diff --git a/Readme.md b/Readme.md index b5cbad6..31ec4b2 100644 --- a/Readme.md +++ b/Readme.md @@ -4,7 +4,7 @@ # Unirand A JavaScript module for generating seeded random distributions and its statistical analysis. -Implemented in pure JavaScript with no dependencies, designed to work in Node.js and fully asynchronous, tested *with 780+ tests*. +Implemented in pure JavaScript with no dependencies, designed to work in Node.js and fully asynchronous, tested *with 800+ tests*. #### [Supported distributions](./core/methods/) @@ -25,6 +25,7 @@ Implemented in pure JavaScript with no dependencies, designed to work in Node.js | Erlang distribution | `k` - integer, `k` > 0, `mu` - float value, `mu` > 0 | `unirand.erlang(k, mu).random()` | | Exponential distribution | `lambda` - float value, `lambda` > 0 | `unirand.exponential(lambda).random()` | | Extreme (Gumbel-type) Value distribution | `mu` - any value, `sigma` - float number, `sigma` > 0 | `unirand.extremevalue(mu, sigma).random()` | +| Fatigue life distribution | `alpha` > 0, `beta` > 0 | `unirand.fatigue(alpha, beta).random()` | | Gamma distribution | `alpha` - float value, `alpha` > 0, `beta` - integer, `beta` > 0 | `unirand.gamma(alpha, beta).random()` | | Geometric distribution | `p` - float value, 0 <= `p` <= 1 | `unirand.geometric(p).random()` | | Irwin-Hall distribution | `n` - integer, `n` > 0 | `unirand.irwinhall(n).random()` | diff --git a/core/methods/Readme.md b/core/methods/Readme.md new file mode 100644 index 0000000..6880f18 --- /dev/null +++ b/core/methods/Readme.md @@ -0,0 +1,32 @@ +| Name | Parameters | Usage | +| --- | --- | --- | +| Uniform distribution | `min` - any value, `max` - any value, `min` < `max` | `unirand.uniform(min, max).random()` | +| Normal (Gaussian) distribution | `mu` - any value, `sigma` > 0 | `unirand.normal(mu, sigma).random()` | +| Bates distribution | `n` - integer, `n` >= 1, `a` - any value, `b` - any value, `b` > `a` | `unirand.bates(n, a, b).random()` | +| Bernoulli distribution | `p` - float number, 0 <= `p` <= 1 | `unirand.bernoulli(p).random()` | +| Beta distribution | `alpha` - integer, `alpha` > 0, `beta` > integer, `beta` > 0 | `unirand.beta(alpha, beta).random()` | +| BetaPrime distribution | `alpha` - integer, `alpha` > 0, `beta` > integer, `beta` > 0 | `unirand.betaprime(alpha, beta).random()` | +| Binomial distribution | `n` - integer, `n` > 0, `p` - float number, 0 <= `p` <= 1 | `unirand.binomial(n, p).random()` | +| Cauchy (Lorenz) distribution | `x` - any value, `gamma` > 0 | `unirand.cauchy(x, gamma).random()` | +| Chi distribution | `k` - integer, `k` > 0 | `unirand.chi(k).random()` | +| Chi Square distribution | `k` - integer, `k` > 0 | `unirand.chisquare(k).random()` | +| Compertz distribution | `nu` > 0 - float value, `b` > 0 - float value | `unirand.compertz(nu, b).random()` | +| Delaporte distribution | `alpha` > 0 - float value, `beta` > 0 - float value, `lambda` > 0 - float value | `unirand.delaporte(alpha, beta, lambda).random()` | +| Erlang distribution | `k` - integer, `k` > 0, `mu` - float value, `mu` > 0 | `unirand.erlang(k, mu).random()` | +| Exponential distribution | `lambda` - float value, `lambda` > 0 | `unirand.exponential(lambda).random()` | +| Extreme (Gumbel-type) Value distribution | `mu` - any value, `sigma` - float number, `sigma` > 0 | `unirand.extremevalue(mu, sigma).random()` | +| Fatigue life distribution | `alpha` > 0, `beta` > 0 | `unirand.fatigue(alpha, beta).random()` | +| Gamma distribution | `alpha` - float value, `alpha` > 0, `beta` - integer, `beta` > 0 | `unirand.gamma(alpha, beta).random()` | +| Geometric distribution | `p` - float value, 0 <= `p` <= 1 | `unirand.geometric(p).random()` | +| Irwin-Hall distribution | `n` - integer, `n` > 0 | `unirand.irwinhall(n).random()` | +| Laplace distribution | `mu` - any value, `b` - float value, `b` > 0 | `unirand.laplace(mu, b).random()` | +| Logistic distribution | `mu` - any value, `s` - float value, `s` > 0 | `unirand.logistic(mu, s).random()` | +| Lognormal distribution | `mu` - any value, `sigma` - float value, `sigma` > 0 | `unirand.lognormal(mu, sigma).random()` | +| Negative Binomial distribution | `r` - integer, `r` > 0, `p` - float value, 0 <= `p` <= 1 | `unirand.negativebinomial(r, p).random()` | +| Pareto distribution | `xm` - float value, `xm` > 0, `alpha` - float value, `alpha` > 0 | `unirand.pareto(xm, alpha).random()` | +| Poisson distribution | `lambda` - integer, `lambda` > 0 | `unirand.poisson(lambda).random()` | +| Rayleigh distribution | `sigma` - float value, `sigma` > 0 | `unirand.rayleigh(sigma).random()` | +| Student's t-distribution | `v` - integer, `v` > 0 | `unirand.student(v).random()` | +| Triangular distribution | `a`, `b`, `c` - any number, `b` > `a`, `a` <= `c` <= `b` | `unirand.triangular(a, b, c).random()` | +| Weibull distribution | `k` - float value, `k` > 0, `lambda` - float value, `lambda` > 0 | `unirand.weibull(k, lambda).random()` | +| Zipf distribution | `alpha` - float value, `alpha` >= 0, `shape` - integer, `shape` > 1 | `unirand.zipf(alpha, shape).random()` | diff --git a/core/methods/fatigue.js b/core/methods/fatigue.js new file mode 100644 index 0000000..c6f35bd --- /dev/null +++ b/core/methods/fatigue.js @@ -0,0 +1,160 @@ +// @flow +/** + * Fatigue life Distribution, also known as Birnbaum–Saunders distribution + * Continuous distribution + * https://en.wikipedia.org/wiki/Birnbaum-Saunders_distribution + * @param alpha: number - shape parameter, alpha > 0 + * @param beta: number - scale parameter, beta > 0 + * @returns Fatigue Distributed value + * Created by Alexey S. Kiselev + */ + +import type { MethodError, RandomArray } from '../types'; +import type { IDistribution } from '../interfaces'; + +const Normal = require('./normal'); + +class Fatigue implements IDistribution { + alpha: number; + beta: number; + normal: Normal; + + constructor(alpha: number, beta: number): void { + this.alpha = Number(alpha); + this.beta = Number(beta); + this.normal = new Normal(0, 1); + } + + _random(norm: number): number { + return this.beta * Math.pow(this.alpha * norm + Math.sqrt(Math.pow(this.alpha * norm, 2) + 4), 2) / 4; + } + + /** + * Generates a random number + * @returns {number} a Fatigue distributed number + */ + random(): number { + return this._random(this.normal.random()); + } + + /** + * Generates next seeded random number + * @returns {number} a Fatigue distributed number + */ + next(): number { + return this._random(this.normal.next()); + } + + /** + * Generates Fatigue distributed numbers + * @param n: number - Number of elements in resulting array, n > 0 + * @returns Array - Fatigue distributed numbers + */ + distribution(n: number): RandomArray { + let fatigueArray: RandomArray = [], + random: RandomArray = this.normal.distribution(n); + for (let i: number = 0; i < n; i += 1){ + fatigueArray[i] = this._random(random[i]); + } + return fatigueArray; + } + + /** + * Error handling + * @returns {boolean} + */ + isError(): MethodError { + if (!this.alpha || !this.beta) { + return {error: 'Fatigue distribution: you should point parameters "alpha" and "beta" as numerical values'}; + } + if (this.alpha <= 0 || this.beta <= 0) { + return {error: 'Fatigue distribution: parameters "alpha" and "beta" must be a positive numbers'}; + } + + return { error: false }; + } + + /** + * Refresh method + * @param newAlpha: number - new parameter "alpha" + * @param newBeta: number - new parameter "beta" + * This method does not return values + */ + refresh(newAlpha: number, newBeta: number): void { + this.alpha = Number(newAlpha); + this.beta = Number(newBeta); + } + + /** + * Class .toString method + * @returns {string} + */ + toString(): string { + let info = [ + 'Fatigue Distribution', + `Usage: unirand.fatigue(${this.alpha}, ${this.beta}).random()` + ]; + return info.join('\n'); + } + + /** + * Mean value + * Information only + * For calculating real mean value use analyzer + */ + get mean(): number { + return this.beta * (Math.pow(this.alpha, 2) + 2) /2; + } + + /** + * Median value + * Information only + * For calculating real mean value use analyzer + */ + get median(): number { + return this.beta; + } + + /** + * Variance value + * Information only + * For calculating real variance value use analyzer + */ + get variance(): number { + return Math.pow(this.alpha * this.beta, 2) * (5 * Math.pow(this.alpha, 2) + 4) / 4; + } + + /** + * Skewness value + * Information only + * For calculating real skewness value use analyzer + */ + get skewness(): number { + return 4 * this.alpha * (11 * Math.pow(this.alpha, 2) + 6) / Math.pow(5 * this.alpha * this.alpha + 4, 1.5); + } + + /** + * Kurtosis value + * Information only + * For calculating real kurtosis value use analyzer + */ + get kurtosis(): number { + return 3 + Math.pow(this.alpha, 2) * (558 * this.alpha * this.alpha + 240) / Math.pow(5 * this.alpha * this.alpha + 4, 2); + } + + /** + * All parameters of distribution in one object + * Information only + */ + get parameters(): {} { + return { + mean: this.mean, + median: this.median, + variance: this.variance, + skewness: this.skewness, + kurtosis: this.kurtosis + }; + } +} + +module.exports = Fatigue; diff --git a/core/methods/index.js b/core/methods/index.js index bbdcfd4..5795a29 100644 --- a/core/methods/index.js +++ b/core/methods/index.js @@ -11,6 +11,7 @@ const delaporte = require('./delaporte'); const erlang = require('./erlang'); const exponential = require('./exponential'); const extremevalue = require('./extremevalue'); +const fatigue = require('./fatigue'); const gamma = require('./gamma'); const geometric = require('./geometric'); const irwinhall = require('./irwinhall'); @@ -42,6 +43,7 @@ module.exports = { erlang, exponential, extremevalue, + fatigue, gamma, geometric, irwinhall, diff --git a/package.json b/package.json index d05b30a..b5696c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unirand", - "version": "2.8.2", + "version": "2.8.4", "description": "Random numbers and Distributions generation", "main": "./lib/index.js", "scripts": { @@ -45,9 +45,6 @@ ], "author": "Alexey S. Kiselev ", "license": "ISC", - "engines": { - "node": "^8.12" - }, "devDependencies": { "babel-core": "^6.26.0", "babel-eslint": "^8.1.1", diff --git a/test/distributions.seed.test.js b/test/distributions.seed.test.js index 9d52d98..d17dab4 100644 --- a/test/distributions.seed.test.js +++ b/test/distributions.seed.test.js @@ -362,6 +362,37 @@ describe('Random distributions with seed', () => { done(); }); }); + describe('Fatigue distribution (alpha = 1, beta = 2)', () => { + const Fatigue = require('../lib/methods/fatigue'); + it('should return same value each time', () => { + const fatigue = new Fatigue(1, 2); + prng.seed('first fatigue seed test'); + const fatigueFirst = fatigue.random(); + for(let i = 0; i < 1000; i += 1) { + expect(fatigue.random()).to.be.closeTo(fatigueFirst, 0.000001); + } + prng.seed('second fatigue seed test'); + const fatigueSecond = fatigue.random(); + for(let i = 0; i < 1000; i += 1) { + expect(fatigue.random()).to.be.closeTo(fatigueSecond, 0.000001); + } + }); + it('should return same distribution each time', function(done) { + this.timeout(480000); + const fatigue = new Fatigue(1, 2); + prng.seed('first fatigue seed test'); + const fatigueFirst = fatigue.distribution(10000); + for(let i = 0; i < 10; i += 1) { + compareDistributions(fatigue.distribution(10000), fatigueFirst); + } + prng.seed('second fatigue seed test'); + const fatigueSecond = fatigue.distribution(10000); + for(let i = 0; i < 10; i += 1) { + compareDistributions(fatigue.distribution(10000), fatigueSecond); + } + done(); + }); + }); describe('Erlang distribution (k = 2, mu = 2)', () => { const Erlang = require('../lib/methods/erlang'); it('should return same value each time', () => { diff --git a/test/distributions.test.js b/test/distributions.test.js index 087d0f6..9ed1c2b 100644 --- a/test/distributions.test.js +++ b/test/distributions.test.js @@ -673,7 +673,8 @@ describe('Random distributions without seed', () => { expect(beta.mean).to.be.a('number'); beta.mean.should.equal(0.25); }); - it('should generate a numerical value for alpha=1 or beta=1', () => { + it('should generate a numerical value for alpha=1 or beta=1', function(done) { + this.timeout(480000); const beta = new Beta(1, 1); prng.seed(); for(let i = 0; i < 10000; i += 1){ @@ -689,6 +690,7 @@ describe('Random distributions without seed', () => { for(let i = 0; i < 10000; i += 1){ expect(beta3.random()).to.be.a('number'); } + done(); }); it('should return different values each time', () => { let beta = new Beta(1, 2), @@ -1594,6 +1596,207 @@ describe('Random distributions without seed', () => { }); }); + // Fatigue distribution + describe('Fatigue distribution', () => { + beforeEach(() => { + prng.seed(); + }); + before(() => { + prng.seed(); + }); + let Fatigue = require('../lib/methods/fatigue'), + Common = require('../lib/analyzer/common'), + Percentile = require('../lib/analyzer/percentiles'); + it('requires three numerical arguments with alpha > 0 and beta > 0', () => { + let zeroParams = () => { + let fatigue = new Fatigue(); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + zeroParams.should.throw(Error); + + let oneParam = () => { + let fatigue = new Fatigue(1); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + oneParam.should.throw(Error); + + let twoParams = () => { + let fatigue = new Fatigue(1, 2); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + twoParams.should.not.throw(Error); + + let badParams = () => { + let fatigue = new Fatigue('a', 1); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + badParams.should.throw(Error); + + let badParams2 = () => { + let fatigue = new Fatigue(1, 'b'); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + badParams2.should.throw(Error); + + let badParamsLess0 = () => { + let fatigue = new Fatigue(-1, 1); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + badParamsLess0.should.throw(Error); + + let badParamsLess02 = () => { + let fatigue = new Fatigue(1, -1); + if(fatigue.isError().error) + throw new Error(fatigue.isError().error); + }; + badParamsLess02.should.throw(Error); + }); + it('should has methods: .random, .distribution, .refresh, .isError', () => { + let fatigue = new Fatigue(1, 2); + expect(fatigue).to.have.property('random'); + expect(fatigue).to.respondsTo('random'); + expect(fatigue).to.have.property('distribution'); + expect(fatigue).to.respondsTo('distribution'); + expect(fatigue).to.have.property('refresh'); + expect(fatigue).to.respondsTo('refresh'); + expect(fatigue).to.have.property('isError'); + expect(fatigue).to.respondsTo('isError'); + }); + it('should have values for initial alpha = 1 and beta = 2 equals to alpha = 2 and beta = 3 after .refresh(2, 3) method',() => { + let fatigue = new Fatigue(1, 2); + fatigue.alpha.should.equal(1); + fatigue.beta.should.equal(2); + fatigue.refresh(2, 3); + fatigue.alpha.should.equal(2); + fatigue.beta.should.equal(3); + }); + it('should generate an array with random values with length of 500', () => { + let fatigue = new Fatigue(1, 2), + randomArray = fatigue.distribution(500), + countDiffs = 0, + last, + delta = 0.01; + // Check all values + randomArray.map(rand => { + if(last && Math.abs(rand - last) > delta){ + countDiffs += 1; + } + last = rand; + }); + expect(randomArray).to.be.an('array'); + expect(randomArray).to.have.lengthOf(500); + expect(countDiffs).to.be.at.least(300); + }); + describe('With real generated data (alpha = 1, beta = 2)', () => { + beforeEach(() => { + prng.seed(); + }); + before(() => { + prng.seed(); + }); + let fatigue = new Fatigue(1, 2), + distribution, + analyzer, + percentiler, + min = [], + max = [], + mean = [], + median = [], + variance = [], + skewness = [], + kurtosis = []; + + prng.seed(); + for(let i = 0; i < 20; i += 1) { + distribution = fatigue.distribution(300000); + analyzer = Common.getInstance(distribution); + percentiler = Percentile.getInstance(distribution); + min.push(analyzer.min); + max.push(analyzer.max); + mean.push(analyzer.mean); + median.push(percentiler.median); + variance.push(analyzer.variance); + skewness.push(analyzer.skewness); + kurtosis.push(analyzer.kurtosis); + } + + it('should has min value close to 0', () => { + expect(analyzer.min).to.be.a('number'); + expect(meanValue(min)).to.be.closeTo(0, 0.1); + }); + it('should has max value at least 8', () => { + expect(analyzer.max).to.be.a('number'); + expect(meanValue(max)).to.be.at.least(8); + }); + it('should has correct mean value', () => { + expect(analyzer.mean).to.be.a('number'); + expect(meanValue(mean)).to.be.closeTo(fatigue.mean, 0.02); + }); + it('should has correct median value', () => { + expect(percentiler.median).to.be.a('number'); + expect(meanValue(median)).to.be.closeTo(fatigue.median, 0.02); + }); + it('should has correct variance value', () => { + expect(analyzer.variance).to.be.a('number'); + expect(meanValue(variance)).to.be.closeTo(fatigue.variance, 0.03); + }); + it('should has correct skewness value', () => { + expect(analyzer.skewness).to.be.a('number'); + expect(meanValue(skewness)).to.be.closeTo(fatigue.skewness, 0.05); + }); + it('should has correct kurtosis value', () => { + expect(analyzer.kurtosis).to.be.a('number'); + expect(meanValue(kurtosis)).to.be.closeTo(fatigue.kurtosis, 0.1); + }); + it('should has pdf array with 1000 elements and sum of them close to 1', () => { + let analyzer = Common.getInstance(distribution, { + pdf: 1000 + }), + sum = 0; + expect(analyzer.pdf.probabilities).to.be.an('array'); + expect(analyzer.pdf.probabilities[0]).to.be.a('number'); + expect(analyzer.pdf.values).to.be.an('array'); + expect(analyzer.pdf.values[0]).to.be.a('number'); + expect(analyzer.pdf.probabilities.length).to.be.equal(1000); + expect(analyzer.pdf.values.length).to.be.equal(1000); + expect(analyzer.pdf.values.length).to.be.equal(analyzer.pdf.probabilities.length); + for(let el of analyzer.pdf.probabilities) { + sum += el; + } + expect(sum).to.be.closeTo(1, 0.005); + }); + it('should has pdf value close to zero on corners', () => { + let analyzer = Common.getInstance(distribution, { + pdf: 1000 + }); + expect(analyzer.pdf.probabilities).to.be.an('array'); + expect(analyzer.pdf.probabilities[0]).to.be.a('number'); + expect(analyzer.pdf.probabilities[0]).to.be.closeTo(0, 0.02); + expect(analyzer.pdf.probabilities[999]).to.be.closeTo(0, 0.02); + }); + it('should has cdf array with 1000 elements and last element close to 1', () => { + let analyzer = Common.getInstance(distribution, { + pdf: 1000 + }); + expect(analyzer.cdf.probabilities).to.be.an('array'); + expect(analyzer.cdf.probabilities[0]).to.be.a('number'); + expect(analyzer.cdf.values).to.be.an('array'); + expect(analyzer.cdf.values[0]).to.be.a('number'); + expect(analyzer.cdf.probabilities.length).to.be.equal(1000); + expect(analyzer.cdf.values.length).to.be.equal(1000); + expect(analyzer.cdf.values.length).to.be.equal(analyzer.pdf.probabilities.length); + expect(analyzer.cdf.probabilities[0]).to.be.closeTo(0, 0.02); + expect(analyzer.cdf.probabilities[999]).to.be.closeTo(1, 0.02); + }); + }); + }); + // Cauchy distribution describe('Cauchy distribution', () => { beforeEach(() => {