mujoco_worldgen/util/placement.py (172 lines of code) (raw):
import numpy as np
import scipy.optimize
def place_boxes(random_state, boxes, width, height, placement_margin=0.0):
'''
Tries to randomly place rectangular boxes between (0,0) and (width, height)
boxes - list of dictionaries, each with keys 'size' and 'placement_xy'
'size' must be (x,y)
'placement_xy' is either None or (x_frac, y_frac) between 0 and 1
(0, 0) is left lower corner, (.5, .5) is the center, etc
width, height - dimensions of the outer bound to place boxes in
placement_margin - minimum distance (orthogonal) between placed boxes
Returns list of box positions (x,y) from left lower corner, or None if
no solution found.
'''
width = float(width)
height = float(height)
assert width > 0 and height > 0, "invalid width, height"
area = 0
sizes = [box["size"] for box in boxes]
for s in sizes:
if s[0] > width - 2 * placement_margin or s[0] <= 0.0 or \
s[1] > height - 2 * placement_margin or s[1] <= 0.0:
return None
area += (s[0] + placement_margin // 2) * (s[1] + placement_margin // 2)
if area > 0.6 * (width - placement_margin) * (height - placement_margin) and len(sizes) > 1:
return None
if area > width * height:
return None
a_edge, b_edge = _get_edge_constraints(sizes, width, height, placement_margin)
for _ in range(10):
def get_matrices(xy):
a_eq = []
b_eq = []
for idx, box in enumerate(boxes):
if box["placement_xy"] is not None:
xy[idx] = box["placement_xy"][0] * (width - box["size"][0])
xy[idx + len(sizes)] = box["placement_xy"][1] * \
(height - box["size"][1])
for i in [idx, idx + len(sizes)]:
a = np.zeros(2 * len(sizes))
a[i] = 1.0
a_eq.append(a)
b_eq.append(xy[i])
a_pairwise, b_pairwise, violated = _get_pairwise_constraints(xy, boxes, placement_margin)
for idx, v in enumerate(violated):
if not v:
for i in [idx, idx + len(sizes)]:
a = np.zeros(2 * len(sizes))
a[i] = 1.0
a_eq.append(a)
b_eq.append(xy[i])
if len(a_eq) > 0:
a_eq = np.stack(a_eq)
b_eq = np.stack(b_eq)
else:
a_eq = None
b_eq = None
return a_eq, b_eq, a_pairwise, b_pairwise, violated
best_xy = None
best_violated = [True for _ in range(100)]
for _ in range(10):
xy = _get_random_xy(random_state, boxes, width, height, placement_margin)
a_eq, b_eq, a_pairwise, b_pairwise, violated = get_matrices(xy)
if sum(violated) < sum(best_violated):
best_xy = xy
best_violated = violated
xy = best_xy
a_eq, b_eq, a_pairwise, b_pairwise, violated = get_matrices(xy)
# If it's not violated, than further optimization is not needed.
if sum(violated) > -1e-4:
return [(a, b) for a, b in zip(xy[:len(sizes)], xy[len(sizes):])]
if len(a_pairwise) == 0:
a = a_edge
b = b_edge
else:
a = np.concatenate([a_edge, np.stack(a_pairwise)], 0)
b = np.concatenate([b_edge, np.array(b_pairwise)], 0)
random = random_state.uniform(-1, 1, 2 * len(sizes))
sol = scipy.optimize.linprog(random, A_ub=-a, b_ub=-b, A_eq=a_eq, b_eq=b_eq)
if sol.success:
return _further_randomize(random_state, boxes, a, b, sol.x)
return None
# Constrainsts ensuring that boxes are within 0-width, 0-height.
def _get_edge_constraints(sizes, width, height, placement_margin):
# a xy >= b
a_edge = []
b_edge = []
for idx, s in enumerate(sizes):
# x_idx >= 0
a = np.zeros(2 * len(sizes))
a[idx] = 1.0
a_edge.append(a)
b_edge.append(placement_margin)
# x_idx <= width - s_idx[0]
# -x_idx >= s_idx[0] - width
a = np.zeros(2 * len(sizes))
a[idx] = -1.0
a_edge.append(a)
b_edge.append(s[0] - width + placement_margin)
# y_idx >= 0
a = np.zeros(2 * len(sizes))
a[idx + len(sizes)] = 1.0
a_edge.append(a)
b_edge.append(placement_margin)
# y_idx <= height - s_idx[1]
# -y_idx >= s_idx[0] - height
a = np.zeros(2 * len(sizes))
a[idx + len(sizes)] = -1.0
a_edge.append(a)
b_edge.append(s[1] - height + placement_margin)
a_edge = np.stack(a_edge)
b_edge = np.array(b_edge)
return a_edge, b_edge
# simplex algorithm locates all objects next to edges.
# We compute slack of the solution and we move objects
# within the slack randomly.
def _further_randomize(random_state, boxes, a, b, xy):
# Determines how much positions can be shifted.
slack = np.zeros((2, a.shape[1]))
slack[0, :] = -np.inf
slack[1, :] = np.inf
# a * xy - b > sol_slack
# if a[i][j] == 1 then xy[j] can be as small as -slack / np.sum(abs(a[i]))
# if a[i][j] == -1 then xy[j] can be as big as slack / np.sum(abs(a[i]))
# np.sum(abs(a[i]))
sol_slack = np.matmul(a, xy) - b
for i in range(a.shape[0]):
row = np.sum(np.abs(a[i]))
for j in range(a.shape[1]):
if np.abs(a[i][j] - 1.) < 1e-4:
slack[0][j] = min(
max(sol_slack[i], -slack[0][j]) / row, 0)
elif np.abs(a[i][j] + 1.) < 1e-4:
slack[1][j] = max(
min(sol_slack[i], slack[1][j]) / row, 0)
assert((slack[0][:] <= slack[1][:]).all())
dim = xy.shape[0] // 2
for i in range(xy.shape[0]):
if boxes[i % dim]["placement_xy"] is None:
xy[i] += random_state.uniform(slack[0][i], slack[1][i])
return [(a, b) for a, b in zip(xy[:dim], xy[dim:])]
# Generates random initial xy position of boxes. attempts to place
# them far from each other (with respect to each coordinate).
def _get_random_xy(random_state, boxes, width, height, placement_margin):
xy = np.zeros(len(boxes) * 2)
for t in range(3):
if t > 0:
dist = np.abs(np.expand_dims(xy, 1) - np.expand_dims(xy, 0))
dist /= max(width, height)
for i in range(dist.shape[0]):
dist[i, i] = 1.0
for i, box in enumerate(boxes):
if t == 0 or np.min(dist[i, :]) < 0.1:
xy[i] = random_state.uniform(placement_margin,
width - box["size"][0] - placement_margin)
if t == 0 or np.min(dist[i + len(boxes), :]) < 0.1:
xy[i + len(boxes)] = random_state.uniform(placement_margin,
height - box["size"][1] - placement_margin)
return xy
# determines all constrains between pairs of boxes.
def _get_pairwise_constraints(xy, boxes, placement_margin):
a_pairwise = []
b_pairwise = []
violated = [0.0 for _ in range(len(boxes))]
sizes = [box["size"] for box in boxes]
for idx0, s0 in enumerate(sizes):
for idx1, s1 in enumerate(sizes):
if idx1 <= idx0:
continue
a_small = []
b_small = []
for axis in [0, 1]:
# x0 >= x1 + s1[0] or y0 >= y1 + s1[1]
a = np.zeros(2 * len(boxes))
a[idx0 + axis * len(boxes)] = 1.0
a[idx1 + axis * len(boxes)] = -1.0
a_small.append(a)
b_small.append(s1[axis] + placement_margin)
# x1 >= x0 + s0[0] or y1 >= y0 + s0[1]
a = np.zeros(2 * len(boxes))
a[idx0 + axis * len(boxes)] = -1.0
a[idx1 + axis * len(boxes)] = 1.0
a_small.append(a)
b_small.append(s0[axis] + placement_margin)
a_small = np.stack(a_small)
b_small = np.array(b_small)
ret = np.matmul(a_small, xy) - b_small
idx = np.argmax(ret)
a_pairwise.append(a_small[idx])
b_pairwise.append(b_small[idx])
if ret[idx] <= -1e-4:
violated[idx0] += ret[idx]
violated[idx1] += ret[idx]
return a_pairwise, b_pairwise, violated