1
0

randomOutputsForTier.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. /**
  2. * """ Make up to `max_number` random output values, chosen using exponential
  3. distribution function. All parameters should be positive `int`s.
  4. None can be returned for expected types of failures, which will often occur
  5. when the input_amount is too small or too large, since it becomes uncommon
  6. to find a random assortment of values that satisfy the desired constraints.
  7. On success, this returns a list of length 1 to max_count, of non-negative
  8. integer values that sum up to exactly input_amount.
  9. The returned values will always exactly sum up to input_amount. This is done
  10. by renormalizing them, which means the actual effective `scale` will vary
  11. depending on random conditions.
  12. If `allow_extra_change` is passed (this is abnormal!) then this may return
  13. max_count+1 outputs; the last output will be the leftover change if all
  14. max_counts outputs were exhausted.
  15. """
  16. */
  17. export default (rng, input_amount, scale, offset, max_count, allow_extra_change=False) => {
  18. if input_amount < offset:
  19. return None
  20. lambd = 1./scale
  21. remaining = input_amount
  22. values = [] # list of fractional random values without offset
  23. for _ in range(max_count+1):
  24. val = rng.expovariate(lambd)
  25. # A ceil here makes sure rounding errors won't sometimes put us over the top.
  26. # Provided that scale is much larger than 1, the impact is negligible.
  27. remaining -= ceil(val) + offset
  28. if remaining < 0:
  29. break
  30. values.append(val)
  31. else:
  32. if allow_extra_change:
  33. result = [(round(v) + offset) for v in values[:-1]]
  34. result.append(input_amount - sum(result))
  35. return result
  36. # Fail because we would need too many outputs
  37. # (most likely, scale was too small)
  38. return None
  39. assert len(values) <= max_count
  40. if not values:
  41. # Our first try put us over the limit, so we have nothing to work with.
  42. # (most likely, scale was too large)
  43. return None
  44. desired_random_sum = input_amount - len(values) * offset
  45. assert desired_random_sum >= 0
  46. # Now we need to rescale and round the values so they fill up the desired.
  47. # input amount exactly. We perform rounding in cumulative space so that the
  48. # sum is exact, and the rounding is distributed fairly.
  49. cumsum = list(itertools.accumulate(values))
  50. rescale = desired_random_sum / cumsum[-1]
  51. normed_cumsum = [round(rescale * v) for v in cumsum]
  52. assert normed_cumsum[-1] == desired_random_sum
  53. differences = ((a - b) for a,b in zip(normed_cumsum, itertools.chain((0,),normed_cumsum)))
  54. result = [(offset + d) for d in differences]
  55. assert sum(result) == input_amount
  56. return result
  57. }