def lattice_width(vertices): if not vertices: return ZZ(-1) miny = min(y for x, y in vertices) maxy = max(y for x, y in vertices) # Interval of Y values lattice_width = maxy - miny # Try to do better than lattice_width: we propose an affine # transformation matrix # [ a b 0 ] # [ d e f ] # [ 0 0 1 ] # with the goal of minimizing the interval of Y values. compute_width = MixedIntegerLinearProgram(maximization=False, solver="PPL") t = compute_width.new_variable(integer=True) lw = compute_width.new_variable(integer=True)[0] compute_width.set_objective(lw) # We want d >= 1 to ensure that the X-axis gets mapped to some # line which is not parallel to the original X-axis. If we keep the # X-axis parallel, then we get a lattice width equal to lattice_width # which does not give new information. If d >= 1, we are also # guaranteed that we can make the transformation matrix invertible. compute_width.set_min(t[0], 1) for x, y in vertices: compute_width.add_constraint(0 <= t[0] * x + t[1] * y + t[2] <= lw) return min(compute_width.solve(), lattice_width)
def normalize_vertices(vertices): if not vertices: return vertices lw = lattice_width(vertices) # Find all values (d,e,f) of transformation matrices # [ a b 0 ] # [ d e f ] # [ 0 0 1 ] # realizing the lattice width. compute_width = MixedIntegerLinearProgram(maximization=False, solver="PPL") t = compute_width.new_variable(integer=True) # There are always 2 choices of sign for (d,e) but we want to pick # only one. So we assume d >= 0. We still need to take care of the # case d == 0 and e < 0 later. compute_width.set_min(t[0], 0) for x, y in vertices: compute_width.add_constraint(0 <= t[0] * x + t[1] * y + t[2] <= lw) # Get list of (a,b,d,e,f) transformation matrices if lw: transformations = [] for d, e, f in compute_width.polyhedron().integral_points(): # Take care of sign of (d,e) when d == 0 and skip # trivial solution (d,e) == (0,0). if d == 0 and e <= 0: continue g, a, b = e.xgcd(-d) # a e - b d = 1 assert g == 1 # Two cases: det=1 and det=-1 transformations += [(a,b,d,e,f), (-a,-b,d,e,f)] else: # Special case lattice width zero: the "compute_width" # polyhedron is a vector line and we want the point on that # line with content 1. d, e, f = compute_width.polyhedron().lines()[0] g = d.gcd(e) d, e, f = d/g, e/g, f/g g, a, b = e.xgcd(-d) # a e - b d = 1 assert g == 1 transformations = [(a,b,d,e,f)] best_llg = None candidates = [] # Contains lists of vertices with optimal lattice length for a, b, d, e, f in transformations: newvertices = [(a * x + b * y, d * x + e * y + f) for x, y in vertices] newvertices.sort(key=lambda xy: (xy[1], xy[0])) # Assert that the range of Y values is the interval [0,lw] assert newvertices[0][1] == 0 assert newvertices[-1][1] == lw # Now move the first vertex to the origin and shear such # that all points have positive X-coordinate. # Note that this shearing keeps the vertices sorted. originx = newvertices[0][0] # Now shear by a variable amount. We only need those candidates # with minimal lattice length. Remark that lattice length is a # convex function of the shear distance. if lw: maxshear = max((originx - x) / y for x, y in newvertices if y).ceil() newvertices = [(x - originx + maxshear * y, y) for x, y in newvertices] else: # All points have Y-coordinate 0: shearing does nothing newvertices = [(x - originx, y) for x, y in newvertices] newllg = max(x for x, y in newvertices) if best_llg is None or newllg < best_llg: # We improved the best length: delete all # previous candidates candidates = [] best_llg = newllg if newllg == best_llg: candidates.append(newvertices) if not lw: continue # Now continue shearing in both directions as long as we do not # increase the lattice length. for s in [-1, 1]: shearvertices = newvertices while True: originx = min(x + s*y for x, y in shearvertices) shearvertices = [(x + s*y - originx, y) for x, y in shearvertices] llg = max(x for x, y in shearvertices) if llg > newllg: break if llg < best_llg: # We improved the best length: delete all # previous candidates candidates = [] best_llg = llg if llg == best_llg: candidates.append(shearvertices) candidates180 = [] for vertices in candidates: # Also consider the polygon rotated over 180 degrees. We need # this because we considered only 1 sign for (d,e) in the # transformation matrix. vertices180 = [(best_llg-x, lw-y) for x, y in vertices[::-1]] candidates180.append(vertices180) candidates += candidates180 # Now find the best candidate def vertices_key(v): k = [] for x, y in v: k += [y, x] return k return min(candidates, key=vertices_key)