require 'distribution' # source: https://github.com/sciruby/distribution # todo: remove distribution dependency by developing a normal distribution generator # - Reference: https://stackoverflow.com/a/6178290/4352306 module Eco module Data module Random class Distribution MAX_DECIMALS = 10 BALANCE = ["uniform", "normal", "half-normal", "forged"] attr_accessor :decimals attr_reader :calls attr_reader :range, :min, :max, :balance, :mean, :sigma attr_reader :weights, :weights_distribution def initialize(init = {}) @calls = 0 @weight_positions = 0 @decimals = init.fetch('decimals', MAX_DECIMALS) self.range = init.fetch('range', 0..1) self.weights = init.fetch('weights', nil) self.balance = init.fetch('balance', nil) @normal = ::Distribution::Normal.rng(@mean, @sigma) @random = ::Random.new end def balance=(value) @balance = value @balance = BALANCE[0] if !BALANCE.include?(@balance) case @balance when 'forged' if @weights&.instance_of?(Array) top = @weights.length - 1 self.range = (0..top) end end end def range=(value) @range = value @min = @range.first; @max = @range.last @mean = @max / 2 @sigma = (@max - @mean) / 3.5 end def weights=(value) @weights = nil; @weights_distribution = nil; @weight_positions = 0 @weights = value if value && value.instance_of?(Array) if @weights total = @weights.inject(0, :+) @weight_positions = total * 20 @weights_distribution = Array.new(@weight_positions) i = 0; j = 0 while i < @weights.length positions = (@weights[i] * @weight_positions / total).round positions.times do @weights_distribution[j] = i j += 1 end i += 1 end if (missing = @weight_positions - j) >= 0 (missing + 1).times { @weights_distribution[j] = 0 j += 1 } end end end def generate @calls += 1 case @balance when 'uniform' num = generate_uniform when 'normal' num = generate_normal when 'half-normal' num = generate_half_normal when 'forged' num = generate_forged else num = generate_uniform end num = generate if num && !@range.member?(num.round) return num && num.round(@decimals) end private def generate_uniform @random.rand(@range) end def generate_normal @normal.call end def generate_half_normal num = generate_normal num = normal_mirror(num, @mean) if num.round < @mean num = generate_half_normal if num.round == @mean num -= @mean num = num - 1 if num.round > 0 # move distribution to left (mirroring doubles density except at mean) return num end def generate_forged #raise "You need to assign weights to be able to use the forge balance!" if !@weights_distribution&.instance_of?(Array) if @weights_distribution&.instance_of?(Array) top = @weight_positions - 1 pos = @random.rand(0..top) return weights_distribution[pos] end return generate_normal # should raise error at the beginning of the function end def normal_mirror (num, mean) return mean + (mean - num) if num.round < mean return mean - (num - mean) if num.round > mean return num end end end end end