1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 |
- /**
- * """ Make up to `max_number` random output values, chosen using exponential
- distribution function. All parameters should be positive `int`s.
- None can be returned for expected types of failures, which will often occur
- when the input_amount is too small or too large, since it becomes uncommon
- to find a random assortment of values that satisfy the desired constraints.
- On success, this returns a list of length 1 to max_count, of non-negative
- integer values that sum up to exactly input_amount.
- The returned values will always exactly sum up to input_amount. This is done
- by renormalizing them, which means the actual effective `scale` will vary
- depending on random conditions.
- If `allow_extra_change` is passed (this is abnormal!) then this may return
- max_count+1 outputs; the last output will be the leftover change if all
- max_counts outputs were exhausted.
- """
- */
- export default (rng, input_amount, scale, offset, max_count, allow_extra_change=False) => {
- if input_amount < offset:
- return None
- lambd = 1./scale
- remaining = input_amount
- values = [] # list of fractional random values without offset
- for _ in range(max_count+1):
- val = rng.expovariate(lambd)
- # A ceil here makes sure rounding errors won't sometimes put us over the top.
- # Provided that scale is much larger than 1, the impact is negligible.
- remaining -= ceil(val) + offset
- if remaining < 0:
- break
- values.append(val)
- else:
- if allow_extra_change:
- result = [(round(v) + offset) for v in values[:-1]]
- result.append(input_amount - sum(result))
- return result
- # Fail because we would need too many outputs
- # (most likely, scale was too small)
- return None
- assert len(values) <= max_count
- if not values:
- # Our first try put us over the limit, so we have nothing to work with.
- # (most likely, scale was too large)
- return None
- desired_random_sum = input_amount - len(values) * offset
- assert desired_random_sum >= 0
- # Now we need to rescale and round the values so they fill up the desired.
- # input amount exactly. We perform rounding in cumulative space so that the
- # sum is exact, and the rounding is distributed fairly.
- cumsum = list(itertools.accumulate(values))
- rescale = desired_random_sum / cumsum[-1]
- normed_cumsum = [round(rescale * v) for v in cumsum]
- assert normed_cumsum[-1] == desired_random_sum
- differences = ((a - b) for a,b in zip(normed_cumsum, itertools.chain((0,),normed_cumsum)))
- result = [(offset + d) for d in differences]
- assert sum(result) == input_amount
- return result
- }
|