1*e1fe3e4aSElliott Hughestry: 2*e1fe3e4aSElliott Hughes import cython 3*e1fe3e4aSElliott Hughes 4*e1fe3e4aSElliott Hughes COMPILED = cython.compiled 5*e1fe3e4aSElliott Hughesexcept (AttributeError, ImportError): 6*e1fe3e4aSElliott Hughes # if cython not installed, use mock module with no-op decorators and types 7*e1fe3e4aSElliott Hughes from fontTools.misc import cython 8*e1fe3e4aSElliott Hughes 9*e1fe3e4aSElliott Hughes COMPILED = False 10*e1fe3e4aSElliott Hughes 11*e1fe3e4aSElliott Hughesfrom typing import ( 12*e1fe3e4aSElliott Hughes Sequence, 13*e1fe3e4aSElliott Hughes Tuple, 14*e1fe3e4aSElliott Hughes Union, 15*e1fe3e4aSElliott Hughes) 16*e1fe3e4aSElliott Hughesfrom numbers import Integral, Real 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughes 19*e1fe3e4aSElliott Hughes_Point = Tuple[Real, Real] 20*e1fe3e4aSElliott Hughes_Delta = Tuple[Real, Real] 21*e1fe3e4aSElliott Hughes_PointSegment = Sequence[_Point] 22*e1fe3e4aSElliott Hughes_DeltaSegment = Sequence[_Delta] 23*e1fe3e4aSElliott Hughes_DeltaOrNone = Union[_Delta, None] 24*e1fe3e4aSElliott Hughes_DeltaOrNoneSegment = Sequence[_DeltaOrNone] 25*e1fe3e4aSElliott Hughes_Endpoints = Sequence[Integral] 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughes 28*e1fe3e4aSElliott HughesMAX_LOOKBACK = 8 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes 31*e1fe3e4aSElliott Hughes@cython.cfunc 32*e1fe3e4aSElliott Hughes@cython.locals( 33*e1fe3e4aSElliott Hughes j=cython.int, 34*e1fe3e4aSElliott Hughes n=cython.int, 35*e1fe3e4aSElliott Hughes x1=cython.double, 36*e1fe3e4aSElliott Hughes x2=cython.double, 37*e1fe3e4aSElliott Hughes d1=cython.double, 38*e1fe3e4aSElliott Hughes d2=cython.double, 39*e1fe3e4aSElliott Hughes scale=cython.double, 40*e1fe3e4aSElliott Hughes x=cython.double, 41*e1fe3e4aSElliott Hughes d=cython.double, 42*e1fe3e4aSElliott Hughes) 43*e1fe3e4aSElliott Hughesdef iup_segment( 44*e1fe3e4aSElliott Hughes coords: _PointSegment, rc1: _Point, rd1: _Delta, rc2: _Point, rd2: _Delta 45*e1fe3e4aSElliott Hughes): # -> _DeltaSegment: 46*e1fe3e4aSElliott Hughes """Given two reference coordinates `rc1` & `rc2` and their respective 47*e1fe3e4aSElliott Hughes delta vectors `rd1` & `rd2`, returns interpolated deltas for the set of 48*e1fe3e4aSElliott Hughes coordinates `coords`.""" 49*e1fe3e4aSElliott Hughes 50*e1fe3e4aSElliott Hughes # rc1 = reference coord 1 51*e1fe3e4aSElliott Hughes # rd1 = reference delta 1 52*e1fe3e4aSElliott Hughes out_arrays = [None, None] 53*e1fe3e4aSElliott Hughes for j in 0, 1: 54*e1fe3e4aSElliott Hughes out_arrays[j] = out = [] 55*e1fe3e4aSElliott Hughes x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j] 56*e1fe3e4aSElliott Hughes 57*e1fe3e4aSElliott Hughes if x1 == x2: 58*e1fe3e4aSElliott Hughes n = len(coords) 59*e1fe3e4aSElliott Hughes if d1 == d2: 60*e1fe3e4aSElliott Hughes out.extend([d1] * n) 61*e1fe3e4aSElliott Hughes else: 62*e1fe3e4aSElliott Hughes out.extend([0] * n) 63*e1fe3e4aSElliott Hughes continue 64*e1fe3e4aSElliott Hughes 65*e1fe3e4aSElliott Hughes if x1 > x2: 66*e1fe3e4aSElliott Hughes x1, x2 = x2, x1 67*e1fe3e4aSElliott Hughes d1, d2 = d2, d1 68*e1fe3e4aSElliott Hughes 69*e1fe3e4aSElliott Hughes # x1 < x2 70*e1fe3e4aSElliott Hughes scale = (d2 - d1) / (x2 - x1) 71*e1fe3e4aSElliott Hughes for pair in coords: 72*e1fe3e4aSElliott Hughes x = pair[j] 73*e1fe3e4aSElliott Hughes 74*e1fe3e4aSElliott Hughes if x <= x1: 75*e1fe3e4aSElliott Hughes d = d1 76*e1fe3e4aSElliott Hughes elif x >= x2: 77*e1fe3e4aSElliott Hughes d = d2 78*e1fe3e4aSElliott Hughes else: 79*e1fe3e4aSElliott Hughes # Interpolate 80*e1fe3e4aSElliott Hughes d = d1 + (x - x1) * scale 81*e1fe3e4aSElliott Hughes 82*e1fe3e4aSElliott Hughes out.append(d) 83*e1fe3e4aSElliott Hughes 84*e1fe3e4aSElliott Hughes return zip(*out_arrays) 85*e1fe3e4aSElliott Hughes 86*e1fe3e4aSElliott Hughes 87*e1fe3e4aSElliott Hughesdef iup_contour(deltas: _DeltaOrNoneSegment, coords: _PointSegment) -> _DeltaSegment: 88*e1fe3e4aSElliott Hughes """For the contour given in `coords`, interpolate any missing 89*e1fe3e4aSElliott Hughes delta values in delta vector `deltas`. 90*e1fe3e4aSElliott Hughes 91*e1fe3e4aSElliott Hughes Returns fully filled-out delta vector.""" 92*e1fe3e4aSElliott Hughes 93*e1fe3e4aSElliott Hughes assert len(deltas) == len(coords) 94*e1fe3e4aSElliott Hughes if None not in deltas: 95*e1fe3e4aSElliott Hughes return deltas 96*e1fe3e4aSElliott Hughes 97*e1fe3e4aSElliott Hughes n = len(deltas) 98*e1fe3e4aSElliott Hughes # indices of points with explicit deltas 99*e1fe3e4aSElliott Hughes indices = [i for i, v in enumerate(deltas) if v is not None] 100*e1fe3e4aSElliott Hughes if not indices: 101*e1fe3e4aSElliott Hughes # All deltas are None. Return 0,0 for all. 102*e1fe3e4aSElliott Hughes return [(0, 0)] * n 103*e1fe3e4aSElliott Hughes 104*e1fe3e4aSElliott Hughes out = [] 105*e1fe3e4aSElliott Hughes it = iter(indices) 106*e1fe3e4aSElliott Hughes start = next(it) 107*e1fe3e4aSElliott Hughes if start != 0: 108*e1fe3e4aSElliott Hughes # Initial segment that wraps around 109*e1fe3e4aSElliott Hughes i1, i2, ri1, ri2 = 0, start, start, indices[-1] 110*e1fe3e4aSElliott Hughes out.extend( 111*e1fe3e4aSElliott Hughes iup_segment( 112*e1fe3e4aSElliott Hughes coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2] 113*e1fe3e4aSElliott Hughes ) 114*e1fe3e4aSElliott Hughes ) 115*e1fe3e4aSElliott Hughes out.append(deltas[start]) 116*e1fe3e4aSElliott Hughes for end in it: 117*e1fe3e4aSElliott Hughes if end - start > 1: 118*e1fe3e4aSElliott Hughes i1, i2, ri1, ri2 = start + 1, end, start, end 119*e1fe3e4aSElliott Hughes out.extend( 120*e1fe3e4aSElliott Hughes iup_segment( 121*e1fe3e4aSElliott Hughes coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2] 122*e1fe3e4aSElliott Hughes ) 123*e1fe3e4aSElliott Hughes ) 124*e1fe3e4aSElliott Hughes out.append(deltas[end]) 125*e1fe3e4aSElliott Hughes start = end 126*e1fe3e4aSElliott Hughes if start != n - 1: 127*e1fe3e4aSElliott Hughes # Final segment that wraps around 128*e1fe3e4aSElliott Hughes i1, i2, ri1, ri2 = start + 1, n, start, indices[0] 129*e1fe3e4aSElliott Hughes out.extend( 130*e1fe3e4aSElliott Hughes iup_segment( 131*e1fe3e4aSElliott Hughes coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2] 132*e1fe3e4aSElliott Hughes ) 133*e1fe3e4aSElliott Hughes ) 134*e1fe3e4aSElliott Hughes 135*e1fe3e4aSElliott Hughes assert len(deltas) == len(out), (len(deltas), len(out)) 136*e1fe3e4aSElliott Hughes return out 137*e1fe3e4aSElliott Hughes 138*e1fe3e4aSElliott Hughes 139*e1fe3e4aSElliott Hughesdef iup_delta( 140*e1fe3e4aSElliott Hughes deltas: _DeltaOrNoneSegment, coords: _PointSegment, ends: _Endpoints 141*e1fe3e4aSElliott Hughes) -> _DeltaSegment: 142*e1fe3e4aSElliott Hughes """For the outline given in `coords`, with contour endpoints given 143*e1fe3e4aSElliott Hughes in sorted increasing order in `ends`, interpolate any missing 144*e1fe3e4aSElliott Hughes delta values in delta vector `deltas`. 145*e1fe3e4aSElliott Hughes 146*e1fe3e4aSElliott Hughes Returns fully filled-out delta vector.""" 147*e1fe3e4aSElliott Hughes 148*e1fe3e4aSElliott Hughes assert sorted(ends) == ends and len(coords) == (ends[-1] + 1 if ends else 0) + 4 149*e1fe3e4aSElliott Hughes n = len(coords) 150*e1fe3e4aSElliott Hughes ends = ends + [n - 4, n - 3, n - 2, n - 1] 151*e1fe3e4aSElliott Hughes out = [] 152*e1fe3e4aSElliott Hughes start = 0 153*e1fe3e4aSElliott Hughes for end in ends: 154*e1fe3e4aSElliott Hughes end += 1 155*e1fe3e4aSElliott Hughes contour = iup_contour(deltas[start:end], coords[start:end]) 156*e1fe3e4aSElliott Hughes out.extend(contour) 157*e1fe3e4aSElliott Hughes start = end 158*e1fe3e4aSElliott Hughes 159*e1fe3e4aSElliott Hughes return out 160*e1fe3e4aSElliott Hughes 161*e1fe3e4aSElliott Hughes 162*e1fe3e4aSElliott Hughes# Optimizer 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes 165*e1fe3e4aSElliott Hughes@cython.cfunc 166*e1fe3e4aSElliott Hughes@cython.inline 167*e1fe3e4aSElliott Hughes@cython.locals( 168*e1fe3e4aSElliott Hughes i=cython.int, 169*e1fe3e4aSElliott Hughes j=cython.int, 170*e1fe3e4aSElliott Hughes # tolerance=cython.double, # https://github.com/fonttools/fonttools/issues/3282 171*e1fe3e4aSElliott Hughes x=cython.double, 172*e1fe3e4aSElliott Hughes y=cython.double, 173*e1fe3e4aSElliott Hughes p=cython.double, 174*e1fe3e4aSElliott Hughes q=cython.double, 175*e1fe3e4aSElliott Hughes) 176*e1fe3e4aSElliott Hughes@cython.returns(int) 177*e1fe3e4aSElliott Hughesdef can_iup_in_between( 178*e1fe3e4aSElliott Hughes deltas: _DeltaSegment, 179*e1fe3e4aSElliott Hughes coords: _PointSegment, 180*e1fe3e4aSElliott Hughes i: Integral, 181*e1fe3e4aSElliott Hughes j: Integral, 182*e1fe3e4aSElliott Hughes tolerance: Real, 183*e1fe3e4aSElliott Hughes): # -> bool: 184*e1fe3e4aSElliott Hughes """Return true if the deltas for points at `i` and `j` (`i < j`) can be 185*e1fe3e4aSElliott Hughes successfully used to interpolate deltas for points in between them within 186*e1fe3e4aSElliott Hughes provided error tolerance.""" 187*e1fe3e4aSElliott Hughes 188*e1fe3e4aSElliott Hughes assert j - i >= 2 189*e1fe3e4aSElliott Hughes interp = iup_segment(coords[i + 1 : j], coords[i], deltas[i], coords[j], deltas[j]) 190*e1fe3e4aSElliott Hughes deltas = deltas[i + 1 : j] 191*e1fe3e4aSElliott Hughes 192*e1fe3e4aSElliott Hughes return all( 193*e1fe3e4aSElliott Hughes abs(complex(x - p, y - q)) <= tolerance 194*e1fe3e4aSElliott Hughes for (x, y), (p, q) in zip(deltas, interp) 195*e1fe3e4aSElliott Hughes ) 196*e1fe3e4aSElliott Hughes 197*e1fe3e4aSElliott Hughes 198*e1fe3e4aSElliott Hughes@cython.locals( 199*e1fe3e4aSElliott Hughes cj=cython.double, 200*e1fe3e4aSElliott Hughes dj=cython.double, 201*e1fe3e4aSElliott Hughes lcj=cython.double, 202*e1fe3e4aSElliott Hughes ldj=cython.double, 203*e1fe3e4aSElliott Hughes ncj=cython.double, 204*e1fe3e4aSElliott Hughes ndj=cython.double, 205*e1fe3e4aSElliott Hughes force=cython.int, 206*e1fe3e4aSElliott Hughes forced=set, 207*e1fe3e4aSElliott Hughes) 208*e1fe3e4aSElliott Hughesdef _iup_contour_bound_forced_set( 209*e1fe3e4aSElliott Hughes deltas: _DeltaSegment, coords: _PointSegment, tolerance: Real = 0 210*e1fe3e4aSElliott Hughes) -> set: 211*e1fe3e4aSElliott Hughes """The forced set is a conservative set of points on the contour that must be encoded 212*e1fe3e4aSElliott Hughes explicitly (ie. cannot be interpolated). Calculating this set allows for significantly 213*e1fe3e4aSElliott Hughes speeding up the dynamic-programming, as well as resolve circularity in DP. 214*e1fe3e4aSElliott Hughes 215*e1fe3e4aSElliott Hughes The set is precise; that is, if an index is in the returned set, then there is no way 216*e1fe3e4aSElliott Hughes that IUP can generate delta for that point, given `coords` and `deltas`. 217*e1fe3e4aSElliott Hughes """ 218*e1fe3e4aSElliott Hughes assert len(deltas) == len(coords) 219*e1fe3e4aSElliott Hughes 220*e1fe3e4aSElliott Hughes n = len(deltas) 221*e1fe3e4aSElliott Hughes forced = set() 222*e1fe3e4aSElliott Hughes # Track "last" and "next" points on the contour as we sweep. 223*e1fe3e4aSElliott Hughes for i in range(len(deltas) - 1, -1, -1): 224*e1fe3e4aSElliott Hughes ld, lc = deltas[i - 1], coords[i - 1] 225*e1fe3e4aSElliott Hughes d, c = deltas[i], coords[i] 226*e1fe3e4aSElliott Hughes nd, nc = deltas[i - n + 1], coords[i - n + 1] 227*e1fe3e4aSElliott Hughes 228*e1fe3e4aSElliott Hughes for j in (0, 1): # For X and for Y 229*e1fe3e4aSElliott Hughes cj = c[j] 230*e1fe3e4aSElliott Hughes dj = d[j] 231*e1fe3e4aSElliott Hughes lcj = lc[j] 232*e1fe3e4aSElliott Hughes ldj = ld[j] 233*e1fe3e4aSElliott Hughes ncj = nc[j] 234*e1fe3e4aSElliott Hughes ndj = nd[j] 235*e1fe3e4aSElliott Hughes 236*e1fe3e4aSElliott Hughes if lcj <= ncj: 237*e1fe3e4aSElliott Hughes c1, c2 = lcj, ncj 238*e1fe3e4aSElliott Hughes d1, d2 = ldj, ndj 239*e1fe3e4aSElliott Hughes else: 240*e1fe3e4aSElliott Hughes c1, c2 = ncj, lcj 241*e1fe3e4aSElliott Hughes d1, d2 = ndj, ldj 242*e1fe3e4aSElliott Hughes 243*e1fe3e4aSElliott Hughes force = False 244*e1fe3e4aSElliott Hughes 245*e1fe3e4aSElliott Hughes # If the two coordinates are the same, then the interpolation 246*e1fe3e4aSElliott Hughes # algorithm produces the same delta if both deltas are equal, 247*e1fe3e4aSElliott Hughes # and zero if they differ. 248*e1fe3e4aSElliott Hughes # 249*e1fe3e4aSElliott Hughes # This test has to be before the next one. 250*e1fe3e4aSElliott Hughes if c1 == c2: 251*e1fe3e4aSElliott Hughes if abs(d1 - d2) > tolerance and abs(dj) > tolerance: 252*e1fe3e4aSElliott Hughes force = True 253*e1fe3e4aSElliott Hughes 254*e1fe3e4aSElliott Hughes # If coordinate for current point is between coordinate of adjacent 255*e1fe3e4aSElliott Hughes # points on the two sides, but the delta for current point is NOT 256*e1fe3e4aSElliott Hughes # between delta for those adjacent points (considering tolerance 257*e1fe3e4aSElliott Hughes # allowance), then there is no way that current point can be IUP-ed. 258*e1fe3e4aSElliott Hughes # Mark it forced. 259*e1fe3e4aSElliott Hughes elif c1 <= cj <= c2: # and c1 != c2 260*e1fe3e4aSElliott Hughes if not (min(d1, d2) - tolerance <= dj <= max(d1, d2) + tolerance): 261*e1fe3e4aSElliott Hughes force = True 262*e1fe3e4aSElliott Hughes 263*e1fe3e4aSElliott Hughes # Otherwise, the delta should either match the closest, or have the 264*e1fe3e4aSElliott Hughes # same sign as the interpolation of the two deltas. 265*e1fe3e4aSElliott Hughes else: # cj < c1 or c2 < cj 266*e1fe3e4aSElliott Hughes if d1 != d2: 267*e1fe3e4aSElliott Hughes if cj < c1: 268*e1fe3e4aSElliott Hughes if ( 269*e1fe3e4aSElliott Hughes abs(dj) > tolerance 270*e1fe3e4aSElliott Hughes and abs(dj - d1) > tolerance 271*e1fe3e4aSElliott Hughes and ((dj - tolerance < d1) != (d1 < d2)) 272*e1fe3e4aSElliott Hughes ): 273*e1fe3e4aSElliott Hughes force = True 274*e1fe3e4aSElliott Hughes else: # c2 < cj 275*e1fe3e4aSElliott Hughes if ( 276*e1fe3e4aSElliott Hughes abs(dj) > tolerance 277*e1fe3e4aSElliott Hughes and abs(dj - d2) > tolerance 278*e1fe3e4aSElliott Hughes and ((d2 < dj + tolerance) != (d1 < d2)) 279*e1fe3e4aSElliott Hughes ): 280*e1fe3e4aSElliott Hughes force = True 281*e1fe3e4aSElliott Hughes 282*e1fe3e4aSElliott Hughes if force: 283*e1fe3e4aSElliott Hughes forced.add(i) 284*e1fe3e4aSElliott Hughes break 285*e1fe3e4aSElliott Hughes 286*e1fe3e4aSElliott Hughes return forced 287*e1fe3e4aSElliott Hughes 288*e1fe3e4aSElliott Hughes 289*e1fe3e4aSElliott Hughes@cython.locals( 290*e1fe3e4aSElliott Hughes i=cython.int, 291*e1fe3e4aSElliott Hughes j=cython.int, 292*e1fe3e4aSElliott Hughes best_cost=cython.double, 293*e1fe3e4aSElliott Hughes best_j=cython.int, 294*e1fe3e4aSElliott Hughes cost=cython.double, 295*e1fe3e4aSElliott Hughes forced=set, 296*e1fe3e4aSElliott Hughes tolerance=cython.double, 297*e1fe3e4aSElliott Hughes) 298*e1fe3e4aSElliott Hughesdef _iup_contour_optimize_dp( 299*e1fe3e4aSElliott Hughes deltas: _DeltaSegment, 300*e1fe3e4aSElliott Hughes coords: _PointSegment, 301*e1fe3e4aSElliott Hughes forced=set(), 302*e1fe3e4aSElliott Hughes tolerance: Real = 0, 303*e1fe3e4aSElliott Hughes lookback: Integral = None, 304*e1fe3e4aSElliott Hughes): 305*e1fe3e4aSElliott Hughes """Straightforward Dynamic-Programming. For each index i, find least-costly encoding of 306*e1fe3e4aSElliott Hughes points 0 to i where i is explicitly encoded. We find this by considering all previous 307*e1fe3e4aSElliott Hughes explicit points j and check whether interpolation can fill points between j and i. 308*e1fe3e4aSElliott Hughes 309*e1fe3e4aSElliott Hughes Note that solution always encodes last point explicitly. Higher-level is responsible 310*e1fe3e4aSElliott Hughes for removing that restriction. 311*e1fe3e4aSElliott Hughes 312*e1fe3e4aSElliott Hughes As major speedup, we stop looking further whenever we see a "forced" point.""" 313*e1fe3e4aSElliott Hughes 314*e1fe3e4aSElliott Hughes n = len(deltas) 315*e1fe3e4aSElliott Hughes if lookback is None: 316*e1fe3e4aSElliott Hughes lookback = n 317*e1fe3e4aSElliott Hughes lookback = min(lookback, MAX_LOOKBACK) 318*e1fe3e4aSElliott Hughes costs = {-1: 0} 319*e1fe3e4aSElliott Hughes chain = {-1: None} 320*e1fe3e4aSElliott Hughes for i in range(0, n): 321*e1fe3e4aSElliott Hughes best_cost = costs[i - 1] + 1 322*e1fe3e4aSElliott Hughes 323*e1fe3e4aSElliott Hughes costs[i] = best_cost 324*e1fe3e4aSElliott Hughes chain[i] = i - 1 325*e1fe3e4aSElliott Hughes 326*e1fe3e4aSElliott Hughes if i - 1 in forced: 327*e1fe3e4aSElliott Hughes continue 328*e1fe3e4aSElliott Hughes 329*e1fe3e4aSElliott Hughes for j in range(i - 2, max(i - lookback, -2), -1): 330*e1fe3e4aSElliott Hughes cost = costs[j] + 1 331*e1fe3e4aSElliott Hughes 332*e1fe3e4aSElliott Hughes if cost < best_cost and can_iup_in_between(deltas, coords, j, i, tolerance): 333*e1fe3e4aSElliott Hughes costs[i] = best_cost = cost 334*e1fe3e4aSElliott Hughes chain[i] = j 335*e1fe3e4aSElliott Hughes 336*e1fe3e4aSElliott Hughes if j in forced: 337*e1fe3e4aSElliott Hughes break 338*e1fe3e4aSElliott Hughes 339*e1fe3e4aSElliott Hughes return chain, costs 340*e1fe3e4aSElliott Hughes 341*e1fe3e4aSElliott Hughes 342*e1fe3e4aSElliott Hughesdef _rot_list(l: list, k: int): 343*e1fe3e4aSElliott Hughes """Rotate list by k items forward. Ie. item at position 0 will be 344*e1fe3e4aSElliott Hughes at position k in returned list. Negative k is allowed.""" 345*e1fe3e4aSElliott Hughes n = len(l) 346*e1fe3e4aSElliott Hughes k %= n 347*e1fe3e4aSElliott Hughes if not k: 348*e1fe3e4aSElliott Hughes return l 349*e1fe3e4aSElliott Hughes return l[n - k :] + l[: n - k] 350*e1fe3e4aSElliott Hughes 351*e1fe3e4aSElliott Hughes 352*e1fe3e4aSElliott Hughesdef _rot_set(s: set, k: int, n: int): 353*e1fe3e4aSElliott Hughes k %= n 354*e1fe3e4aSElliott Hughes if not k: 355*e1fe3e4aSElliott Hughes return s 356*e1fe3e4aSElliott Hughes return {(v + k) % n for v in s} 357*e1fe3e4aSElliott Hughes 358*e1fe3e4aSElliott Hughes 359*e1fe3e4aSElliott Hughesdef iup_contour_optimize( 360*e1fe3e4aSElliott Hughes deltas: _DeltaSegment, coords: _PointSegment, tolerance: Real = 0.0 361*e1fe3e4aSElliott Hughes) -> _DeltaOrNoneSegment: 362*e1fe3e4aSElliott Hughes """For contour with coordinates `coords`, optimize a set of delta 363*e1fe3e4aSElliott Hughes values `deltas` within error `tolerance`. 364*e1fe3e4aSElliott Hughes 365*e1fe3e4aSElliott Hughes Returns delta vector that has most number of None items instead of 366*e1fe3e4aSElliott Hughes the input delta. 367*e1fe3e4aSElliott Hughes """ 368*e1fe3e4aSElliott Hughes 369*e1fe3e4aSElliott Hughes n = len(deltas) 370*e1fe3e4aSElliott Hughes 371*e1fe3e4aSElliott Hughes # Get the easy cases out of the way: 372*e1fe3e4aSElliott Hughes 373*e1fe3e4aSElliott Hughes # If all are within tolerance distance of 0, encode nothing: 374*e1fe3e4aSElliott Hughes if all(abs(complex(*p)) <= tolerance for p in deltas): 375*e1fe3e4aSElliott Hughes return [None] * n 376*e1fe3e4aSElliott Hughes 377*e1fe3e4aSElliott Hughes # If there's exactly one point, return it: 378*e1fe3e4aSElliott Hughes if n == 1: 379*e1fe3e4aSElliott Hughes return deltas 380*e1fe3e4aSElliott Hughes 381*e1fe3e4aSElliott Hughes # If all deltas are exactly the same, return just one (the first one): 382*e1fe3e4aSElliott Hughes d0 = deltas[0] 383*e1fe3e4aSElliott Hughes if all(d0 == d for d in deltas): 384*e1fe3e4aSElliott Hughes return [d0] + [None] * (n - 1) 385*e1fe3e4aSElliott Hughes 386*e1fe3e4aSElliott Hughes # Else, solve the general problem using Dynamic Programming. 387*e1fe3e4aSElliott Hughes 388*e1fe3e4aSElliott Hughes forced = _iup_contour_bound_forced_set(deltas, coords, tolerance) 389*e1fe3e4aSElliott Hughes # The _iup_contour_optimize_dp() routine returns the optimal encoding 390*e1fe3e4aSElliott Hughes # solution given the constraint that the last point is always encoded. 391*e1fe3e4aSElliott Hughes # To remove this constraint, we use two different methods, depending on 392*e1fe3e4aSElliott Hughes # whether forced set is non-empty or not: 393*e1fe3e4aSElliott Hughes 394*e1fe3e4aSElliott Hughes # Debugging: Make the next if always take the second branch and observe 395*e1fe3e4aSElliott Hughes # if the font size changes (reduced); that would mean the forced-set 396*e1fe3e4aSElliott Hughes # has members it should not have. 397*e1fe3e4aSElliott Hughes if forced: 398*e1fe3e4aSElliott Hughes # Forced set is non-empty: rotate the contour start point 399*e1fe3e4aSElliott Hughes # such that the last point in the list is a forced point. 400*e1fe3e4aSElliott Hughes k = (n - 1) - max(forced) 401*e1fe3e4aSElliott Hughes assert k >= 0 402*e1fe3e4aSElliott Hughes 403*e1fe3e4aSElliott Hughes deltas = _rot_list(deltas, k) 404*e1fe3e4aSElliott Hughes coords = _rot_list(coords, k) 405*e1fe3e4aSElliott Hughes forced = _rot_set(forced, k, n) 406*e1fe3e4aSElliott Hughes 407*e1fe3e4aSElliott Hughes # Debugging: Pass a set() instead of forced variable to the next call 408*e1fe3e4aSElliott Hughes # to exercise forced-set computation for under-counting. 409*e1fe3e4aSElliott Hughes chain, costs = _iup_contour_optimize_dp(deltas, coords, forced, tolerance) 410*e1fe3e4aSElliott Hughes 411*e1fe3e4aSElliott Hughes # Assemble solution. 412*e1fe3e4aSElliott Hughes solution = set() 413*e1fe3e4aSElliott Hughes i = n - 1 414*e1fe3e4aSElliott Hughes while i is not None: 415*e1fe3e4aSElliott Hughes solution.add(i) 416*e1fe3e4aSElliott Hughes i = chain[i] 417*e1fe3e4aSElliott Hughes solution.remove(-1) 418*e1fe3e4aSElliott Hughes 419*e1fe3e4aSElliott Hughes # if not forced <= solution: 420*e1fe3e4aSElliott Hughes # print("coord", coords) 421*e1fe3e4aSElliott Hughes # print("deltas", deltas) 422*e1fe3e4aSElliott Hughes # print("len", len(deltas)) 423*e1fe3e4aSElliott Hughes assert forced <= solution, (forced, solution) 424*e1fe3e4aSElliott Hughes 425*e1fe3e4aSElliott Hughes deltas = [deltas[i] if i in solution else None for i in range(n)] 426*e1fe3e4aSElliott Hughes 427*e1fe3e4aSElliott Hughes deltas = _rot_list(deltas, -k) 428*e1fe3e4aSElliott Hughes else: 429*e1fe3e4aSElliott Hughes # Repeat the contour an extra time, solve the new case, then look for solutions of the 430*e1fe3e4aSElliott Hughes # circular n-length problem in the solution for new linear case. I cannot prove that 431*e1fe3e4aSElliott Hughes # this always produces the optimal solution... 432*e1fe3e4aSElliott Hughes chain, costs = _iup_contour_optimize_dp( 433*e1fe3e4aSElliott Hughes deltas + deltas, coords + coords, forced, tolerance, n 434*e1fe3e4aSElliott Hughes ) 435*e1fe3e4aSElliott Hughes best_sol, best_cost = None, n + 1 436*e1fe3e4aSElliott Hughes 437*e1fe3e4aSElliott Hughes for start in range(n - 1, len(costs) - 1): 438*e1fe3e4aSElliott Hughes # Assemble solution. 439*e1fe3e4aSElliott Hughes solution = set() 440*e1fe3e4aSElliott Hughes i = start 441*e1fe3e4aSElliott Hughes while i > start - n: 442*e1fe3e4aSElliott Hughes solution.add(i % n) 443*e1fe3e4aSElliott Hughes i = chain[i] 444*e1fe3e4aSElliott Hughes if i == start - n: 445*e1fe3e4aSElliott Hughes cost = costs[start] - costs[start - n] 446*e1fe3e4aSElliott Hughes if cost <= best_cost: 447*e1fe3e4aSElliott Hughes best_sol, best_cost = solution, cost 448*e1fe3e4aSElliott Hughes 449*e1fe3e4aSElliott Hughes # if not forced <= best_sol: 450*e1fe3e4aSElliott Hughes # print("coord", coords) 451*e1fe3e4aSElliott Hughes # print("deltas", deltas) 452*e1fe3e4aSElliott Hughes # print("len", len(deltas)) 453*e1fe3e4aSElliott Hughes assert forced <= best_sol, (forced, best_sol) 454*e1fe3e4aSElliott Hughes 455*e1fe3e4aSElliott Hughes deltas = [deltas[i] if i in best_sol else None for i in range(n)] 456*e1fe3e4aSElliott Hughes 457*e1fe3e4aSElliott Hughes return deltas 458*e1fe3e4aSElliott Hughes 459*e1fe3e4aSElliott Hughes 460*e1fe3e4aSElliott Hughesdef iup_delta_optimize( 461*e1fe3e4aSElliott Hughes deltas: _DeltaSegment, 462*e1fe3e4aSElliott Hughes coords: _PointSegment, 463*e1fe3e4aSElliott Hughes ends: _Endpoints, 464*e1fe3e4aSElliott Hughes tolerance: Real = 0.0, 465*e1fe3e4aSElliott Hughes) -> _DeltaOrNoneSegment: 466*e1fe3e4aSElliott Hughes """For the outline given in `coords`, with contour endpoints given 467*e1fe3e4aSElliott Hughes in sorted increasing order in `ends`, optimize a set of delta 468*e1fe3e4aSElliott Hughes values `deltas` within error `tolerance`. 469*e1fe3e4aSElliott Hughes 470*e1fe3e4aSElliott Hughes Returns delta vector that has most number of None items instead of 471*e1fe3e4aSElliott Hughes the input delta. 472*e1fe3e4aSElliott Hughes """ 473*e1fe3e4aSElliott Hughes assert sorted(ends) == ends and len(coords) == (ends[-1] + 1 if ends else 0) + 4 474*e1fe3e4aSElliott Hughes n = len(coords) 475*e1fe3e4aSElliott Hughes ends = ends + [n - 4, n - 3, n - 2, n - 1] 476*e1fe3e4aSElliott Hughes out = [] 477*e1fe3e4aSElliott Hughes start = 0 478*e1fe3e4aSElliott Hughes for end in ends: 479*e1fe3e4aSElliott Hughes contour = iup_contour_optimize( 480*e1fe3e4aSElliott Hughes deltas[start : end + 1], coords[start : end + 1], tolerance 481*e1fe3e4aSElliott Hughes ) 482*e1fe3e4aSElliott Hughes assert len(contour) == end - start + 1 483*e1fe3e4aSElliott Hughes out.extend(contour) 484*e1fe3e4aSElliott Hughes start = end + 1 485*e1fe3e4aSElliott Hughes 486*e1fe3e4aSElliott Hughes return out 487