You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
8.1 KiB
301 lines
8.1 KiB
def iup_segment(coords, rc1, rd1, rc2, rd2):
|
|
# rc1 = reference coord 1
|
|
# rd1 = reference delta 1
|
|
out_arrays = [None, None]
|
|
for j in 0,1:
|
|
out_arrays[j] = out = []
|
|
x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j]
|
|
|
|
|
|
if x1 == x2:
|
|
n = len(coords)
|
|
if d1 == d2:
|
|
out.extend([d1]*n)
|
|
else:
|
|
out.extend([0]*n)
|
|
continue
|
|
|
|
if x1 > x2:
|
|
x1, x2 = x2, x1
|
|
d1, d2 = d2, d1
|
|
|
|
# x1 < x2
|
|
scale = (d2 - d1) / (x2 - x1)
|
|
for pair in coords:
|
|
x = pair[j]
|
|
|
|
if x <= x1:
|
|
d = d1
|
|
elif x >= x2:
|
|
d = d2
|
|
else:
|
|
# Interpolate
|
|
d = d1 + (x - x1) * scale
|
|
|
|
out.append(d)
|
|
|
|
return zip(*out_arrays)
|
|
|
|
def iup_contour(delta, coords):
|
|
assert len(delta) == len(coords)
|
|
if None not in delta:
|
|
return delta
|
|
|
|
n = len(delta)
|
|
# indices of points with explicit deltas
|
|
indices = [i for i,v in enumerate(delta) if v is not None]
|
|
if not indices:
|
|
# All deltas are None. Return 0,0 for all.
|
|
return [(0,0)]*n
|
|
|
|
out = []
|
|
it = iter(indices)
|
|
start = next(it)
|
|
if start != 0:
|
|
# Initial segment that wraps around
|
|
i1, i2, ri1, ri2 = 0, start, start, indices[-1]
|
|
out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
|
|
out.append(delta[start])
|
|
for end in it:
|
|
if end - start > 1:
|
|
i1, i2, ri1, ri2 = start+1, end, start, end
|
|
out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
|
|
out.append(delta[end])
|
|
start = end
|
|
if start != n-1:
|
|
# Final segment that wraps around
|
|
i1, i2, ri1, ri2 = start+1, n, start, indices[0]
|
|
out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
|
|
|
|
assert len(delta) == len(out), (len(delta), len(out))
|
|
return out
|
|
|
|
def iup_delta(delta, coords, ends):
|
|
assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4
|
|
n = len(coords)
|
|
ends = ends + [n-4, n-3, n-2, n-1]
|
|
out = []
|
|
start = 0
|
|
for end in ends:
|
|
end += 1
|
|
contour = iup_contour(delta[start:end], coords[start:end])
|
|
out.extend(contour)
|
|
start = end
|
|
|
|
return out
|
|
|
|
# Optimizer
|
|
|
|
def can_iup_in_between(deltas, coords, i, j, tolerance):
|
|
assert j - i >= 2
|
|
interp = list(iup_segment(coords[i+1:j], coords[i], deltas[i], coords[j], deltas[j]))
|
|
deltas = deltas[i+1:j]
|
|
|
|
assert len(deltas) == len(interp)
|
|
|
|
return all(abs(complex(x-p, y-q)) <= tolerance for (x,y),(p,q) in zip(deltas, interp))
|
|
|
|
def _iup_contour_bound_forced_set(delta, coords, tolerance=0):
|
|
"""The forced set is a conservative set of points on the contour that must be encoded
|
|
explicitly (ie. cannot be interpolated). Calculating this set allows for significantly
|
|
speeding up the dynamic-programming, as well as resolve circularity in DP.
|
|
|
|
The set is precise; that is, if an index is in the returned set, then there is no way
|
|
that IUP can generate delta for that point, given coords and delta.
|
|
"""
|
|
assert len(delta) == len(coords)
|
|
|
|
forced = set()
|
|
# Track "last" and "next" points on the contour as we sweep.
|
|
nd, nc = delta[0], coords[0]
|
|
ld, lc = delta[-1], coords[-1]
|
|
for i in range(len(delta)-1, -1, -1):
|
|
d, c = ld, lc
|
|
ld, lc = delta[i-1], coords[i-1]
|
|
|
|
for j in (0,1): # For X and for Y
|
|
cj = c[j]
|
|
dj = d[j]
|
|
lcj = lc[j]
|
|
ldj = ld[j]
|
|
ncj = nc[j]
|
|
ndj = nd[j]
|
|
|
|
if lcj <= ncj:
|
|
c1, c2 = lcj, ncj
|
|
d1, d2 = ldj, ndj
|
|
else:
|
|
c1, c2 = ncj, lcj
|
|
d1, d2 = ndj, ldj
|
|
|
|
# If coordinate for current point is between coordinate of adjacent
|
|
# points on the two sides, but the delta for current point is NOT
|
|
# between delta for those adjacent points (considering tolerance
|
|
# allowance), then there is no way that current point can be IUP-ed.
|
|
# Mark it forced.
|
|
force = False
|
|
if c1 <= cj <= c2:
|
|
if not (min(d1,d2)-tolerance <= dj <= max(d1,d2)+tolerance):
|
|
force = True
|
|
else: # cj < c1 or c2 < cj
|
|
if c1 == c2:
|
|
if d1 == d2:
|
|
if abs(dj - d1) > tolerance:
|
|
force = True
|
|
else:
|
|
if abs(dj) > tolerance:
|
|
# Disabled the following because the "d1 == d2" does
|
|
# check does not take tolerance into consideration...
|
|
pass # force = True
|
|
elif d1 != d2:
|
|
if cj < c1:
|
|
if dj != d1 and ((dj-tolerance < d1) != (d1 < d2)):
|
|
force = True
|
|
else: # c2 < cj
|
|
if d2 != dj and ((d2 < dj+tolerance) != (d1 < d2)):
|
|
force = True
|
|
|
|
if force:
|
|
forced.add(i)
|
|
break
|
|
|
|
nd, nc = d, c
|
|
|
|
return forced
|
|
|
|
def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=None):
|
|
"""Straightforward Dynamic-Programming. For each index i, find least-costly encoding of
|
|
points 0 to i where i is explicitly encoded. We find this by considering all previous
|
|
explicit points j and check whether interpolation can fill points between j and i.
|
|
|
|
Note that solution always encodes last point explicitly. Higher-level is responsible
|
|
for removing that restriction.
|
|
|
|
As major speedup, we stop looking further whenever we see a "forced" point."""
|
|
|
|
n = len(delta)
|
|
if lookback is None:
|
|
lookback = n
|
|
costs = {-1:0}
|
|
chain = {-1:None}
|
|
for i in range(0, n):
|
|
best_cost = costs[i-1] + 1
|
|
|
|
costs[i] = best_cost
|
|
chain[i] = i - 1
|
|
|
|
if i - 1 in forced:
|
|
continue
|
|
|
|
for j in range(i-2, max(i-lookback, -2), -1):
|
|
|
|
cost = costs[j] + 1
|
|
|
|
if cost < best_cost and can_iup_in_between(delta, coords, j, i, tolerance):
|
|
costs[i] = best_cost = cost
|
|
chain[i] = j
|
|
|
|
if j in forced:
|
|
break
|
|
|
|
return chain, costs
|
|
|
|
def _rot_list(l, k):
|
|
"""Rotate list by k items forward. Ie. item at position 0 will be
|
|
at position k in returned list. Negative k is allowed."""
|
|
n = len(l)
|
|
k %= n
|
|
if not k: return l
|
|
return l[n-k:] + l[:n-k]
|
|
|
|
def _rot_set(s, k, n):
|
|
k %= n
|
|
if not k: return s
|
|
return {(v + k) % n for v in s}
|
|
|
|
def iup_contour_optimize(delta, coords, tolerance=0.):
|
|
n = len(delta)
|
|
|
|
# Get the easy cases out of the way:
|
|
|
|
# If all are within tolerance distance of 0, encode nothing:
|
|
if all(abs(complex(*p)) <= tolerance for p in delta):
|
|
return [None] * n
|
|
|
|
# If there's exactly one point, return it:
|
|
if n == 1:
|
|
return delta
|
|
|
|
# If all deltas are exactly the same, return just one (the first one):
|
|
d0 = delta[0]
|
|
if all(d0 == d for d in delta):
|
|
return [d0] + [None] * (n-1)
|
|
|
|
# Else, solve the general problem using Dynamic Programming.
|
|
|
|
forced = _iup_contour_bound_forced_set(delta, coords, tolerance)
|
|
# The _iup_contour_optimize_dp() routine returns the optimal encoding
|
|
# solution given the constraint that the last point is always encoded.
|
|
# To remove this constraint, we use two different methods, depending on
|
|
# whether forced set is non-empty or not:
|
|
|
|
if forced:
|
|
# Forced set is non-empty: rotate the contour start point
|
|
# such that the last point in the list is a forced point.
|
|
k = (n-1) - max(forced)
|
|
assert k >= 0
|
|
|
|
delta = _rot_list(delta, k)
|
|
coords = _rot_list(coords, k)
|
|
forced = _rot_set(forced, k, n)
|
|
|
|
chain, costs = _iup_contour_optimize_dp(delta, coords, forced, tolerance)
|
|
|
|
# Assemble solution.
|
|
solution = set()
|
|
i = n - 1
|
|
while i is not None:
|
|
solution.add(i)
|
|
i = chain[i]
|
|
assert forced <= solution, (forced, solution)
|
|
delta = [delta[i] if i in solution else None for i in range(n)]
|
|
|
|
delta = _rot_list(delta, -k)
|
|
else:
|
|
# Repeat the contour an extra time, solve the 2*n case, then look for solutions of the
|
|
# circular n-length problem in the solution for 2*n linear case. I cannot prove that
|
|
# this always produces the optimal solution...
|
|
chain, costs = _iup_contour_optimize_dp(delta+delta, coords+coords, forced, tolerance, n)
|
|
best_sol, best_cost = None, n+1
|
|
|
|
for start in range(n-1, 2*n-1):
|
|
# Assemble solution.
|
|
solution = set()
|
|
i = start
|
|
while i > start - n:
|
|
solution.add(i % n)
|
|
i = chain[i]
|
|
if i == start - n:
|
|
cost = costs[start] - costs[start - n]
|
|
if cost <= best_cost:
|
|
best_sol, best_cost = solution, cost
|
|
|
|
delta = [delta[i] if i in best_sol else None for i in range(n)]
|
|
|
|
|
|
return delta
|
|
|
|
def iup_delta_optimize(delta, coords, ends, tolerance=0.):
|
|
assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4
|
|
n = len(coords)
|
|
ends = ends + [n-4, n-3, n-2, n-1]
|
|
out = []
|
|
start = 0
|
|
for end in ends:
|
|
contour = iup_contour_optimize(delta[start:end+1], coords[start:end+1], tolerance)
|
|
assert len(contour) == end - start + 1
|
|
out.extend(contour)
|
|
start = end+1
|
|
|
|
return out
|