The problem
We want to select 3 coordinates within a specific range.
However, once we select X
and Y
, Z
is fixed.

We cannot have full degrees of liberty, for instance if we picked X = 0.72
and Y = 0.24
, then Z
cannot exist within the desired range.
If we pick X
and Y
randomly and compute Z
, in many cases Z
will be incorrect:
size = 10_000
X = np.random.uniform(0.71, 0.72, size=size)
Y = np.random.uniform(0.23, 0.24, size=size)
Z = 1-(X+Y)

The question is: how to solve this?
oversampling
This is usually not a recommended approach, but we can use trial and error, here we need to sample a bit more than twice to hope having enough valid points (sampling on a square, keeping half of the points):
target = 100_000
factor = 2.2 # the exact value depends on the target
# there is always a risk that we miss a few points
size = int(target*factor)
X = np.random.uniform(0.71, 0.72, size=size)
Y = np.random.uniform(0.23, 0.24, size=size)
Z = 1-(X+Y)
idx = np.arange(size)[(Z>=0.05) & (Z<=0.06)][:target]
X = X[idx]
Y = Y[idx]
Z = Z[idx]
assert np.allclose(X+Y+Z, 1)

using math
What we are looking for is equivalent to generating uniform points on a triangle.
You can use this formula:

size = 10_000
limits = [(0.71, 0.72), (0.23, 0.24), (0.05, 0.06)]
MIN, MAX = np.array(limits).T
corners = np.repeat(MIN[None], 3, axis=0)
np.fill_diagonal(corners, MAX)
# or setting the corners manually:
# corners = np.array([[0.72, 0.23, 0.05],
# [0.71, 0.24, 0.05],
# [0.71, 0.23, 0.06]])
r1 = np.sqrt(np.random.random(size=size))
r2 = np.random.random(size=size)
X, Y, Z = (corners[:,None] * np.array([(1-r1), r1*(1-r2), r2*r1])[...,None]
).sum(axis=0).T
assert np.allclose(X+Y+Z, 1)
Alternative using einsum
for the last step:
X, Y, Z = np.einsum('ik,ij->kj', corners, np.array([(1-r1), r1*(1-r2), r2*r1]))

(The black dots are the corners of the triangle)
Note that for this approach to work the sum of the minimum boundaries of two coordinates + the maximum boundary of the third coordinate must be equal to 1 (Xmax+Ymin+Zmin == Xmin+Ymax+Zmin == Xmin+Ymin+Zmax == 1
). If this is not the case, ignore the MAX and use corners = (1-np.identity(3))*MIN ; np.fill_diagonal(corners, 1-corners.sum(axis=1))
.