Here is a Python solution to your question. This one has the following advantages:
- It does not use rejection sampling.
- It chooses uniformly at random from among all combinations that meet the requirements.
It's a modification I made based on an algorithm by John McClane, which he posted as an answer to another question. I describe the algorithm in another answer.
import random # Or secrets
def _getSolTableForRanges(ranges, adjsum):
n = len(ranges)
t = [[0 for i in range(adjsum + 1)] for j in range(n + 1)]
t[0][0] = 1
for i in range(1, n + 1):
for j in range(0, adjsum + 1):
krange = ranges[i-1][1] - ranges[i-1][0]
jm = max(j - krange, 0)
v = 0
for k in range(jm, j + 1):
v += t[i - 1][k]
t[i][j] = v
return t
def intsInRangesWithSum(numSamples, ranges, total):
""" Generates one or more combinations of
'len(ranges)' numbers each, where each
combination's numbers sum to 'total', and each number
has its own valid range. 'ranges' is a list of valid ranges
for each number; the first item in each range is the minimum
value and the second is the maximum value. For example,
'ranges' can be [[1,4],[3,5],[2,6]], which says that the first
number must be in the interval [1, 4], the second in [3, 5],
and the third in [2, 6].
The combinations are chosen uniformly at random.
Neither the integers in the 'ranges' list nor
'total' may be negative. Returns an empty
list if 'numSamples' is zero.
This is a modification I made to an algorithm that
was contributed in a _Stack Overflow_
answer (`questions/61393463`) by John McClane.
Raises an error if there is no solution for the given
parameters. """
mintotal = sum([x[0] for x in ranges])
maxtotal = sum([x[1] for x in ranges])
adjsum = total - mintotal
print([total,adjsum])
# Min, max, sum negative
if total<0: raise ValueError
for r in ranges:
if r[0]<0 or r[1]<0: raise ValueError
# No solution
if mintotal > total or maxtotal < total:
raise ValueError
if numSamples == 0:
return []
# One solution
if maxtotal == total:
return [[x[1] for x in ranges] for i in range(numSamples)]
if mintotal == total:
return [[x[0] for x in ranges] for i in range(numSamples)]
samples = [None for i in range(numSamples)]
numPerSample = len(ranges)
table = _getSolTableForRanges(ranges, adjsum)
for sample in range(numSamples):
s = adjsum
ret = [0 for i in range(numPerSample)]
for ib in range(numPerSample):
i = numPerSample - 1 - ib
# Or secrets.randbelow(table[i + 1][s])
v = random.randint(0, table[i + 1][s] - 1)
r = ranges[i][0]
v -= table[i][s]
while v >= 0:
s -= 1
r += 1
v -= table[i][s]
ret[i] = r
samples[sample] = ret
return samples
Example:
weights=intsInRangesWithSum(
# One sample
1,
# Ranges for each random number
[[50, 70], [5, 30], [5, 30], [5, 50], [3, 15]],
# Sum of the numbers
100)
# Divide by 100 to get weights that sum to 1
weights=[x/100.0 for x in weights[0]]