xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/iup.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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