slicecompose 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slicecompose/__init__.py +872 -0
- slicecompose/_version.py +24 -0
- slicecompose-0.1.0.dist-info/METADATA +206 -0
- slicecompose-0.1.0.dist-info/RECORD +9 -0
- slicecompose-0.1.0.dist-info/WHEEL +5 -0
- slicecompose-0.1.0.dist-info/licenses/LICENSE +21 -0
- slicecompose-0.1.0.dist-info/scm_file_list.json +15 -0
- slicecompose-0.1.0.dist-info/scm_version.json +8 -0
- slicecompose-0.1.0.dist-info/top_level.txt +1 -0
slicecompose/__init__.py
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
"""Exact composition of Python slices.
|
|
2
|
+
|
|
3
|
+
``compose(s1, s2)`` returns a slice ``s3`` such that ``arr[s1][s2] == arr[s3]``
|
|
4
|
+
for *every* sequence ``arr`` (equivalently, for every length ``n >= 0``), or
|
|
5
|
+
``None`` if no such slice exists. Both directions are exact:
|
|
6
|
+
|
|
7
|
+
- soundness: a returned slice is equivalent for all ``n`` (not just tested ones),
|
|
8
|
+
- completeness: ``None`` is returned only if no equivalent slice exists.
|
|
9
|
+
|
|
10
|
+
``SliceChain(*slices)`` holds a sequence of slices applied in order and reduces
|
|
11
|
+
adjacent pairs with ``compose`` until no adjacent pair merges.
|
|
12
|
+
|
|
13
|
+
``SlicePlan`` (with the ready-made empty plan ``s_``) gathers slices lazily
|
|
14
|
+
through ordinary bracket notation -- ``s_[2:][3:]`` -- and reduces its stored
|
|
15
|
+
chain in place on first query.
|
|
16
|
+
|
|
17
|
+
See EXPLANATION.md for the full soundness/completeness argument. In brief:
|
|
18
|
+
|
|
19
|
+
- The action of a slice on a length-``n`` sequence is the index progression
|
|
20
|
+
``A(n) + k*j`` for ``j < L(n)``. For a composition, ``A``/``L`` are, on each
|
|
21
|
+
of finitely many computable intervals of ``n`` ("stretches"), either affine,
|
|
22
|
+
an exact ceiling-division staircase, or affine plus a period-``|step1|``
|
|
23
|
+
sawtooth. Single slices produce the same forms without the sawtooth.
|
|
24
|
+
- Equality of two such functions on a stretch is decided exactly from a small
|
|
25
|
+
set of probe evaluations (endpoints, support/length thresholds, staircase
|
|
26
|
+
jump alignment) -- valid for arbitrarily large field values, no enumeration.
|
|
27
|
+
- If an equivalent slice exists, its anchor structure (front/back for start
|
|
28
|
+
and stop, step sign) and its parameters are forced by the composition's tail
|
|
29
|
+
behavior, staircase jump phase, and support boundaries; the generator emits
|
|
30
|
+
the forced candidates (plus tolerance wiggles) and the verifier filters.
|
|
31
|
+
|
|
32
|
+
Standard library only.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
__all__ = ["compose", "SlicePlan", "s_", "SliceChain"]
|
|
38
|
+
|
|
39
|
+
_INF = None # sentinel for "unbounded stretch end"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --------------------------------------------------------------------------
|
|
43
|
+
# Ground-truth evaluation (exact Python slice semantics, arbitrary ints)
|
|
44
|
+
# --------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _norm3(s):
|
|
48
|
+
"""slice -> (start, stop, step) with step a nonzero int."""
|
|
49
|
+
if not isinstance(s, slice):
|
|
50
|
+
raise TypeError(f"expected a slice, got {type(s).__name__}")
|
|
51
|
+
a, b, k = s.start, s.stop, s.step
|
|
52
|
+
k = 1 if k is None else int(k)
|
|
53
|
+
if k == 0:
|
|
54
|
+
raise ValueError("slice step cannot be zero")
|
|
55
|
+
a = None if a is None else int(a)
|
|
56
|
+
b = None if b is None else int(b)
|
|
57
|
+
return a, b, k
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _to_slice(t):
|
|
61
|
+
a, b, k = t
|
|
62
|
+
return slice(a, b, None if k == 1 else k)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _indices(t, n):
|
|
66
|
+
"""Exact equivalent of slice(*t).indices(n) for arbitrary ints."""
|
|
67
|
+
a, b, k = t
|
|
68
|
+
if k > 0:
|
|
69
|
+
if a is None:
|
|
70
|
+
a2 = 0
|
|
71
|
+
elif a >= 0:
|
|
72
|
+
a2 = a if a < n else n
|
|
73
|
+
else:
|
|
74
|
+
a2 = a + n if a + n > 0 else 0
|
|
75
|
+
if b is None:
|
|
76
|
+
b2 = n
|
|
77
|
+
elif b >= 0:
|
|
78
|
+
b2 = b if b < n else n
|
|
79
|
+
else:
|
|
80
|
+
b2 = b + n if b + n > 0 else 0
|
|
81
|
+
else:
|
|
82
|
+
if a is None:
|
|
83
|
+
a2 = n - 1
|
|
84
|
+
elif a >= 0:
|
|
85
|
+
a2 = a if a < n else n - 1
|
|
86
|
+
else:
|
|
87
|
+
a2 = a + n if a + n > -1 else -1
|
|
88
|
+
if b is None:
|
|
89
|
+
b2 = -1
|
|
90
|
+
elif b >= 0:
|
|
91
|
+
b2 = b if b < n else n - 1
|
|
92
|
+
else:
|
|
93
|
+
b2 = b + n if b + n > -1 else -1
|
|
94
|
+
return a2, b2, k
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _eval1(t, n):
|
|
98
|
+
"""(first_index, step, length) of slice-triple t applied to length n."""
|
|
99
|
+
a, b, k = _indices(t, n)
|
|
100
|
+
if k > 0:
|
|
101
|
+
length = (b - a + k - 1) // k
|
|
102
|
+
else:
|
|
103
|
+
length = (b - a + k + 1) // k
|
|
104
|
+
return a, k, (length if length > 0 else 0)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _eval2(t1, t2, n):
|
|
108
|
+
"""(first_index, step, length) of applying t1 then t2 to length n."""
|
|
109
|
+
a1, k1, m = _eval1(t1, n)
|
|
110
|
+
a2, k2, length = _eval1(t2, m)
|
|
111
|
+
return a1 + k1 * a2, k1 * k2, length
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _pt_eq(x, y):
|
|
115
|
+
"""Do two (first, step, length) actions select the same elements?"""
|
|
116
|
+
a1, k1, n1 = x
|
|
117
|
+
a2, k2, n2 = y
|
|
118
|
+
if n1 != n2:
|
|
119
|
+
return False
|
|
120
|
+
if n1 == 0:
|
|
121
|
+
return True
|
|
122
|
+
if a1 != a2:
|
|
123
|
+
return False
|
|
124
|
+
return n1 == 1 or k1 == k2
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
# Mirror reduction: arr[::-1][u] == arr[mirror(u)] for every u and arr.
|
|
129
|
+
# Mirroring start/stop fields is bitwise-not (None stays None), step negates.
|
|
130
|
+
# Applying it to s1 reduces every problem to step1 >= 1:
|
|
131
|
+
# s1 = rev ; mirror(s1) hence s1 . s2 merges iff mirror(s1) . s2 does,
|
|
132
|
+
# and the merged results correspond via mirror as well.
|
|
133
|
+
# --------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _mirror(t):
|
|
137
|
+
a, b, k = t
|
|
138
|
+
return (None if a is None else ~a, None if b is None else ~b, -k)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# --------------------------------------------------------------------------
|
|
142
|
+
# Monotone searches (predicates guaranteed monotone by the structure lemmas)
|
|
143
|
+
# --------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _first_true(lo, hi, pred):
|
|
147
|
+
"""Smallest n in [lo, hi] with pred(n), else None; pred monotone F->T."""
|
|
148
|
+
if lo > hi:
|
|
149
|
+
return None
|
|
150
|
+
if pred(lo):
|
|
151
|
+
return lo
|
|
152
|
+
if not pred(hi):
|
|
153
|
+
return None
|
|
154
|
+
while hi - lo > 1:
|
|
155
|
+
mid = (lo + hi) // 2
|
|
156
|
+
if pred(mid):
|
|
157
|
+
hi = mid
|
|
158
|
+
else:
|
|
159
|
+
lo = mid
|
|
160
|
+
return hi
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _last_true(lo, hi, pred):
|
|
164
|
+
"""Largest n in [lo, hi] with pred(n), else None; pred monotone T->F."""
|
|
165
|
+
if lo > hi:
|
|
166
|
+
return None
|
|
167
|
+
if pred(hi):
|
|
168
|
+
return hi
|
|
169
|
+
if not pred(lo):
|
|
170
|
+
return None
|
|
171
|
+
while hi - lo > 1:
|
|
172
|
+
mid = (lo + hi) // 2
|
|
173
|
+
if pred(mid):
|
|
174
|
+
lo = mid
|
|
175
|
+
else:
|
|
176
|
+
hi = mid
|
|
177
|
+
return lo
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------------
|
|
181
|
+
# Breakpoints and stretches.
|
|
182
|
+
#
|
|
183
|
+
# Every regime boundary of a slice's normalized (start, stop, length) as a
|
|
184
|
+
# function of the sequence length is of the form x + y (+/- 2 guard) where
|
|
185
|
+
# x, y range over {0, +-field values}: each boundary is one comparison between
|
|
186
|
+
# two clamped anchor forms, each involving at most one field. We therefore
|
|
187
|
+
# take all pairwise combinations -- a generous, provably sufficient superset.
|
|
188
|
+
#
|
|
189
|
+
# For the composed map the intermediate length m(n) = len1(n) feeds slice s2,
|
|
190
|
+
# so s2's critical values live in m-space and are pulled back to n-space by
|
|
191
|
+
# monotone search (m is monotone between s1's own breakpoints, and changes by
|
|
192
|
+
# at most 1 per step, so it cannot skip a critical value).
|
|
193
|
+
# --------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _combo_vals(t):
|
|
197
|
+
"""Pairwise +-field combinations (with guards) for one slice triple."""
|
|
198
|
+
base = {0, 1, 2, 3}
|
|
199
|
+
for f in (t[0], t[1]):
|
|
200
|
+
if f is not None:
|
|
201
|
+
base.add(f)
|
|
202
|
+
base.add(-f)
|
|
203
|
+
vals = set()
|
|
204
|
+
for x in base:
|
|
205
|
+
for y in base:
|
|
206
|
+
for d in (-2, -1, 0, 1, 2):
|
|
207
|
+
vals.add(x + y + d)
|
|
208
|
+
return vals
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _magnitude(t1, t2):
|
|
212
|
+
m = 8
|
|
213
|
+
for t in (t1, t2):
|
|
214
|
+
for f in t:
|
|
215
|
+
if f is not None:
|
|
216
|
+
m += abs(f)
|
|
217
|
+
return m
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _pullback_breaks(t1, t2):
|
|
221
|
+
"""n-space breakpoints of the composed action of (t1, t2), t1.step >= 1."""
|
|
222
|
+
q = t1[2]
|
|
223
|
+
n_breaks = {v for v in _combo_vals(t1) if v >= 0}
|
|
224
|
+
v_set = _combo_vals(t2)
|
|
225
|
+
v_max = max(abs(v) for v in v_set)
|
|
226
|
+
mag = _magnitude(t1, t2)
|
|
227
|
+
span = q * (v_max + 4) + 4 * mag + 64
|
|
228
|
+
|
|
229
|
+
def m_of(n):
|
|
230
|
+
return _eval1(t1, n)[2]
|
|
231
|
+
|
|
232
|
+
pieces = sorted(n_breaks | {0})
|
|
233
|
+
out = set(n_breaks)
|
|
234
|
+
for i, lo in enumerate(pieces):
|
|
235
|
+
hi = pieces[i + 1] - 1 if i + 1 < len(pieces) else lo + span
|
|
236
|
+
if hi < lo:
|
|
237
|
+
continue
|
|
238
|
+
m_lo, m_hi = m_of(lo), m_of(hi)
|
|
239
|
+
if m_lo == m_hi:
|
|
240
|
+
continue # m constant on the piece: no crossings inside
|
|
241
|
+
inc = m_hi > m_lo
|
|
242
|
+
for v in v_set:
|
|
243
|
+
if inc:
|
|
244
|
+
if not (m_lo < v <= m_hi):
|
|
245
|
+
continue
|
|
246
|
+
hit = _first_true(lo, hi, lambda n, v=v: m_of(n) >= v)
|
|
247
|
+
else:
|
|
248
|
+
if not (m_hi < v <= m_lo):
|
|
249
|
+
continue
|
|
250
|
+
hit = _first_true(lo, hi, lambda n, v=v: m_of(n) < v)
|
|
251
|
+
if hit is not None:
|
|
252
|
+
out.update((hit - 2, hit - 1, hit, hit + 1, hit + 2))
|
|
253
|
+
return {v for v in out if v >= 0} | {0, 1, 2, 3}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _stretches(breaks, extra=()):
|
|
257
|
+
"""Sorted breakpoints -> list of (u, v) closed integer stretches.
|
|
258
|
+
|
|
259
|
+
The last stretch is (u, _INF). Regimes of all involved functions are
|
|
260
|
+
constant on each stretch.
|
|
261
|
+
"""
|
|
262
|
+
pts = sorted(set(breaks) | {v for v in extra if v is not None and v >= 0} | {0})
|
|
263
|
+
out = []
|
|
264
|
+
for i, u in enumerate(pts):
|
|
265
|
+
if i + 1 < len(pts):
|
|
266
|
+
if pts[i + 1] - 1 >= u:
|
|
267
|
+
out.append((u, pts[i + 1] - 1))
|
|
268
|
+
else:
|
|
269
|
+
out.append((u, _INF))
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# --------------------------------------------------------------------------
|
|
274
|
+
# Exact verifier.
|
|
275
|
+
#
|
|
276
|
+
# On each stretch, both the composed action and a candidate slice have:
|
|
277
|
+
# length L(n): constant, or an exact monotone staircase ceil((+-n + c)/D)
|
|
278
|
+
# clamped at 0 (composed D = step1 * |step2|, candidate D = |step3|);
|
|
279
|
+
# first index A(n): affine, or affine plus a period-step1 sawtooth (composed
|
|
280
|
+
# only; present only when s2's start is back-anchored, the
|
|
281
|
+
# intermediate length is non-constant on the stretch and step1 > 1).
|
|
282
|
+
# Equality of such functions on a stretch follows from finitely many checks:
|
|
283
|
+
# - equal L-support thresholds (first/last n with L >= 1 and with L >= 2),
|
|
284
|
+
# - equal values at stretch endpoints / far probes,
|
|
285
|
+
# - equal first staircase-jump position plus equal step divisor D
|
|
286
|
+
# (two exact same-D monotone unit-jump staircases agreeing at a jump and
|
|
287
|
+
# in level are identical; divisors match because step3 must equal
|
|
288
|
+
# step1*step2 wherever some L >= 2 exists),
|
|
289
|
+
# - A: both sides affine -> two points suffice; a sawtooth with a wrap
|
|
290
|
+
# inside the checked span deviates from any affine by +-step1 at one of
|
|
291
|
+
# the checked points; spans >= step1 with an active sawtooth are rejected
|
|
292
|
+
# structurally (a full wrap makes A non-affine, no slice can match).
|
|
293
|
+
# --------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _l_range(evalf, u, v, lim):
|
|
297
|
+
"""Contiguous {n in [u, v]: L(n) >= lim} for monotone-L stretch, or None.
|
|
298
|
+
|
|
299
|
+
Returns (w1, w2). L is monotone on the stretch (structure lemma), so the
|
|
300
|
+
region is a prefix or suffix of [u, v].
|
|
301
|
+
"""
|
|
302
|
+
lu = evalf(u)[2] >= lim
|
|
303
|
+
lv = evalf(v)[2] >= lim
|
|
304
|
+
if lu and lv:
|
|
305
|
+
return (u, v)
|
|
306
|
+
if not lu and not lv:
|
|
307
|
+
return None
|
|
308
|
+
if lv: # increasing: suffix
|
|
309
|
+
w = _first_true(u, v, lambda n: evalf(n)[2] >= lim)
|
|
310
|
+
return (w, v)
|
|
311
|
+
w = _last_true(u, v, lambda n: evalf(n)[2] >= lim)
|
|
312
|
+
return (u, w)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _first_jump(evalf, w1, w2):
|
|
316
|
+
"""First n in (w1, w2] where L(n) != L(w1); L monotone on [w1, w2]."""
|
|
317
|
+
l0 = evalf(w1)[2]
|
|
318
|
+
if evalf(w2)[2] == l0:
|
|
319
|
+
return None
|
|
320
|
+
return _first_true(w1 + 1, w2, lambda n: evalf(n)[2] != l0)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _check_stretch_finite(fc, fx, u, v, q, t2):
|
|
324
|
+
"""Exact equality of composed fc and candidate fx on finite [u, v]."""
|
|
325
|
+
|
|
326
|
+
def same(n):
|
|
327
|
+
return _pt_eq(fc(n), fx(n))
|
|
328
|
+
|
|
329
|
+
pts = {u, u + 1, (u + v) // 2, v - 1, v}
|
|
330
|
+
# support thresholds (L >= 1) and step-relevance thresholds (L >= 2)
|
|
331
|
+
for lim in (1, 2):
|
|
332
|
+
rc = _l_range(fc, u, v, lim)
|
|
333
|
+
rx = _l_range(fx, u, v, lim)
|
|
334
|
+
if (rc is None) != (rx is None):
|
|
335
|
+
return False
|
|
336
|
+
if rc is None:
|
|
337
|
+
continue
|
|
338
|
+
if rc != rx:
|
|
339
|
+
return False
|
|
340
|
+
w1, w2 = rc
|
|
341
|
+
pts.update((w1, w1 + 1, (w1 + w2) // 2, w2 - 1, w2))
|
|
342
|
+
if lim == 2:
|
|
343
|
+
# staircase jump alignment inside the L>=2 region
|
|
344
|
+
jc = _first_jump(fc, w1, w2)
|
|
345
|
+
jx = _first_jump(fx, w1, w2)
|
|
346
|
+
if jc != jx:
|
|
347
|
+
return False
|
|
348
|
+
if jc is not None:
|
|
349
|
+
pts.update((jc - 1, jc, jc + 1))
|
|
350
|
+
# sawtooth wrap rejection for A on the L>=1 region
|
|
351
|
+
r1 = _l_range(fc, u, v, 1)
|
|
352
|
+
if r1 is not None:
|
|
353
|
+
w1, w2 = r1
|
|
354
|
+
if w2 - w1 + 1 >= max(q, 2) and _sawtooth_active(fc, w1, w2, q, t2):
|
|
355
|
+
return False
|
|
356
|
+
for n in pts:
|
|
357
|
+
if u <= n <= v and not same(n):
|
|
358
|
+
return False
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _sawtooth_active(fc, w1, w2, q, t2):
|
|
363
|
+
"""Is the composed A of form affine + nontrivial sawtooth on [w1, w2]?
|
|
364
|
+
|
|
365
|
+
True iff step1 = q > 1, s2's start is effectively back-anchored on this
|
|
366
|
+
stretch (slope of a2 in m equals 1) and the intermediate length m is
|
|
367
|
+
non-constant here. Regimes are constant per stretch, so endpoint
|
|
368
|
+
measurements are exact.
|
|
369
|
+
"""
|
|
370
|
+
if q <= 1:
|
|
371
|
+
return False
|
|
372
|
+
m1 = fc.m(w1)
|
|
373
|
+
m2 = fc.m(w2)
|
|
374
|
+
if m1 == m2:
|
|
375
|
+
return False
|
|
376
|
+
a21 = _eval1(t2, m1)[0]
|
|
377
|
+
a22 = _eval1(t2, m2)[0]
|
|
378
|
+
# a2 is affine in m on the stretch with slope 0 or 1
|
|
379
|
+
return (a22 - a21) == (m2 - m1)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class _ComposedEval:
|
|
383
|
+
"""Composed ground-truth evaluator, also exposing the intermediate m."""
|
|
384
|
+
|
|
385
|
+
__slots__ = ("t1", "t2")
|
|
386
|
+
|
|
387
|
+
def __init__(self, t1, t2):
|
|
388
|
+
self.t1 = t1
|
|
389
|
+
self.t2 = t2
|
|
390
|
+
|
|
391
|
+
def __call__(self, n):
|
|
392
|
+
return _eval2(self.t1, self.t2, n)
|
|
393
|
+
|
|
394
|
+
def m(self, n):
|
|
395
|
+
return _eval1(self.t1, n)[2]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _check_stretch_inf(fc, fx, u, q, t2, dmax):
|
|
399
|
+
"""Exact equality of fc and fx on [u, infinity).
|
|
400
|
+
|
|
401
|
+
On the final stretch both sides are constant or exactly-periodic
|
|
402
|
+
staircases with divisor <= dmax; all thresholds and the first jump lie
|
|
403
|
+
within u + 2*dmax + 5, and levels/growth are pinned by far probes.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def same(n):
|
|
407
|
+
return _pt_eq(fc(n), fx(n))
|
|
408
|
+
|
|
409
|
+
hi = u + 2 * dmax + 5
|
|
410
|
+
far = u + 3 * dmax + 7
|
|
411
|
+
for n in (u, u + 1, u + 2, hi, far, far + 1, far + dmax):
|
|
412
|
+
if not same(n):
|
|
413
|
+
return False
|
|
414
|
+
for lim in (1, 2):
|
|
415
|
+
rc = _l_range(fc, u, hi, lim)
|
|
416
|
+
rx = _l_range(fx, u, hi, lim)
|
|
417
|
+
if (rc is None) != (rx is None):
|
|
418
|
+
return False
|
|
419
|
+
if rc is None:
|
|
420
|
+
continue # thresholds beyond hi impossible: growth shows by hi
|
|
421
|
+
if rc != rx:
|
|
422
|
+
return False
|
|
423
|
+
for n in (rc[0], rc[0] + 1, rc[1]):
|
|
424
|
+
if not same(n):
|
|
425
|
+
return False
|
|
426
|
+
if lim == 2:
|
|
427
|
+
jc = _first_jump(fc, rc[0], hi)
|
|
428
|
+
jx = _first_jump(fx, rc[0], hi)
|
|
429
|
+
if jc != jx:
|
|
430
|
+
return False
|
|
431
|
+
if jc is not None:
|
|
432
|
+
for n in (jc - 1, jc, jc + 1):
|
|
433
|
+
if not same(n):
|
|
434
|
+
return False
|
|
435
|
+
# A-sawtooth on the (infinite) support region: any active sawtooth wraps,
|
|
436
|
+
# making A non-affine there, which no single slice can match.
|
|
437
|
+
if fc(far)[2] >= 1 and _sawtooth_active(fc, u, far, q, t2):
|
|
438
|
+
return False
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _verify(t1, t2, cand, base_breaks):
|
|
443
|
+
"""Exact check: does slice-triple cand equal the composition of t1, t2?"""
|
|
444
|
+
q = t1[2]
|
|
445
|
+
d_comp = q * abs(t2[2])
|
|
446
|
+
dmax = max(d_comp, abs(cand[2]), 1)
|
|
447
|
+
breaks = set(base_breaks) | {v for v in _combo_vals(cand) if v >= 0}
|
|
448
|
+
fc = _ComposedEval(t1, t2)
|
|
449
|
+
|
|
450
|
+
def fx(n):
|
|
451
|
+
return _eval1(cand, n)
|
|
452
|
+
|
|
453
|
+
for u, v in _stretches(breaks):
|
|
454
|
+
if v is _INF:
|
|
455
|
+
if not _check_stretch_inf(fc, fx, u, q, t2, dmax):
|
|
456
|
+
return False
|
|
457
|
+
else:
|
|
458
|
+
if not _check_stretch_finite(fc, fx, u, v, q, t2):
|
|
459
|
+
return False
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# --------------------------------------------------------------------------
|
|
464
|
+
# Profile: measured structural facts about the composed action, from which
|
|
465
|
+
# candidate parameters are forced (see EXPLANATION.md for the forcing lemmas).
|
|
466
|
+
# --------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
_UNBOUNDED = float("inf")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class _Profile:
|
|
472
|
+
__slots__ = (
|
|
473
|
+
"q",
|
|
474
|
+
"k2",
|
|
475
|
+
"K",
|
|
476
|
+
"D",
|
|
477
|
+
"breaks",
|
|
478
|
+
"stretches",
|
|
479
|
+
"fc",
|
|
480
|
+
"tail_mode",
|
|
481
|
+
"P",
|
|
482
|
+
"max_len",
|
|
483
|
+
"N0",
|
|
484
|
+
"N1",
|
|
485
|
+
"n_hat",
|
|
486
|
+
"A_hat",
|
|
487
|
+
"sigma",
|
|
488
|
+
"n_j",
|
|
489
|
+
"L_j",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _profile(t1, t2):
|
|
494
|
+
pro = _Profile()
|
|
495
|
+
pro.q = t1[2]
|
|
496
|
+
pro.k2 = t2[2]
|
|
497
|
+
pro.K = pro.q * pro.k2
|
|
498
|
+
pro.D = pro.q * abs(pro.k2)
|
|
499
|
+
pro.breaks = _pullback_breaks(t1, t2)
|
|
500
|
+
pro.stretches = _stretches(pro.breaks)
|
|
501
|
+
fc = pro.fc = _ComposedEval(t1, t2)
|
|
502
|
+
D = pro.D
|
|
503
|
+
|
|
504
|
+
# tail classification on the final (regime-settled) stretch
|
|
505
|
+
u_inf = pro.stretches[-1][0]
|
|
506
|
+
f1 = u_inf + 2 * D + 5
|
|
507
|
+
f2 = f1 + 2 * D + 5
|
|
508
|
+
l1, l2 = fc(f1)[2], fc(f2)[2]
|
|
509
|
+
if l2 > l1:
|
|
510
|
+
pro.tail_mode, pro.P = "grow", None
|
|
511
|
+
elif l1 == 0:
|
|
512
|
+
pro.tail_mode, pro.P = "empty", 0
|
|
513
|
+
elif l2 == l1:
|
|
514
|
+
pro.tail_mode, pro.P = "const", l1
|
|
515
|
+
else: # decreasing final stretch: excluded by the structure lemmas
|
|
516
|
+
pro.tail_mode, pro.P = "shrink", None
|
|
517
|
+
|
|
518
|
+
# support bounds (first / last n with nonempty result)
|
|
519
|
+
pro.N0 = pro.N1 = None
|
|
520
|
+
for u, v in pro.stretches:
|
|
521
|
+
vv = f2 if v is _INF else v
|
|
522
|
+
r = _l_range(fc, u, vv, 1)
|
|
523
|
+
if r is not None:
|
|
524
|
+
if pro.N0 is None:
|
|
525
|
+
pro.N0 = r[0]
|
|
526
|
+
pro.N1 = r[1]
|
|
527
|
+
if pro.tail_mode in ("grow", "const"):
|
|
528
|
+
pro.N1 = _UNBOUNDED
|
|
529
|
+
|
|
530
|
+
# maximum result length over all n (monotone per stretch -> endpoints)
|
|
531
|
+
if pro.tail_mode == "grow":
|
|
532
|
+
pro.max_len = _UNBOUNDED
|
|
533
|
+
else:
|
|
534
|
+
best = 0
|
|
535
|
+
for u, v in pro.stretches:
|
|
536
|
+
vv = f2 if v is _INF else v
|
|
537
|
+
best = max(best, fc(u)[2], fc(vv)[2])
|
|
538
|
+
pro.max_len = best
|
|
539
|
+
|
|
540
|
+
# tail probes: first index A and its slope sigma at a settled point
|
|
541
|
+
pro.n_hat = f1
|
|
542
|
+
pro.A_hat = pro.sigma = pro.n_j = pro.L_j = None
|
|
543
|
+
if pro.tail_mode in ("grow", "const"):
|
|
544
|
+
pro.A_hat = fc(f1)[0]
|
|
545
|
+
pro.sigma = fc(f1 + 1)[0] - pro.A_hat
|
|
546
|
+
if pro.tail_mode == "grow":
|
|
547
|
+
lf1 = fc(f1)[2]
|
|
548
|
+
pro.n_j = _first_true(f1 + 1, f1 + D + 2, lambda n: fc(n)[2] != lf1)
|
|
549
|
+
pro.L_j = fc(pro.n_j)[2] if pro.n_j is not None else None
|
|
550
|
+
return pro
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# --------------------------------------------------------------------------
|
|
554
|
+
# Candidate generation. Anchor structure and parameters of any equivalent
|
|
555
|
+
# slice are forced by the profile (tail anchors, staircase jump phase,
|
|
556
|
+
# support boundaries); +-1 wiggles are included as convention armor and are
|
|
557
|
+
# filtered by the exact verifier, so they never affect soundness.
|
|
558
|
+
# --------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _wig(v):
|
|
562
|
+
return (v - 1, v, v + 1)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _stop_pos(cands, s, e, k):
|
|
566
|
+
"""Append (s, stop, k) for k > 0 families with back-anchored stop e <= 0."""
|
|
567
|
+
if e < 0:
|
|
568
|
+
cands.append((s, e, k))
|
|
569
|
+
elif e == 0:
|
|
570
|
+
cands.append((s, None, k))
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _stop_negstep_front(cands, s, e, k):
|
|
574
|
+
"""Append (s, stop, k) for k < 0 families with front-anchored stop e >= -1."""
|
|
575
|
+
if e >= 0:
|
|
576
|
+
cands.append((s, e, k))
|
|
577
|
+
elif e == -1:
|
|
578
|
+
cands.append((s, None, k))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _cand_grow(pro):
|
|
582
|
+
K, sig, a_hat, n_hat, n_j, l_j = (
|
|
583
|
+
pro.K,
|
|
584
|
+
pro.sigma,
|
|
585
|
+
pro.A_hat,
|
|
586
|
+
pro.n_hat,
|
|
587
|
+
pro.n_j,
|
|
588
|
+
pro.L_j,
|
|
589
|
+
)
|
|
590
|
+
cands = []
|
|
591
|
+
if n_j is None:
|
|
592
|
+
return cands
|
|
593
|
+
if sig == 0 and K > 0 and a_hat >= 0:
|
|
594
|
+
s = a_hat
|
|
595
|
+
for e in _wig(s + K * (l_j - 1) + 1 - n_j):
|
|
596
|
+
_stop_pos(cands, s, e, K)
|
|
597
|
+
elif sig == 1 and K < 0:
|
|
598
|
+
s = a_hat - n_hat
|
|
599
|
+
if s < 0:
|
|
600
|
+
for e in _wig(n_j + s + K * (l_j - 1) - 1):
|
|
601
|
+
_stop_negstep_front(cands, s, e, K)
|
|
602
|
+
return cands
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _cand_window(pro):
|
|
606
|
+
K, P, sig, a_hat, n_hat, n0 = pro.K, pro.P, pro.sigma, pro.A_hat, pro.n_hat, pro.N0
|
|
607
|
+
cands = []
|
|
608
|
+
if sig == 0 and a_hat >= 0:
|
|
609
|
+
s = a_hat
|
|
610
|
+
if K > 0:
|
|
611
|
+
for e in (s + K * (P - 1) + 1, s + K * P):
|
|
612
|
+
if e > s:
|
|
613
|
+
cands.append((s, e, K))
|
|
614
|
+
else:
|
|
615
|
+
# the support of [s:e:K] starts at e + 2 (at 1 for stop None),
|
|
616
|
+
# which pins e via the measured support start N0
|
|
617
|
+
for e in _wig(n0 - 2):
|
|
618
|
+
_stop_negstep_front(cands, s, e, K)
|
|
619
|
+
elif sig == 1:
|
|
620
|
+
s = a_hat - n_hat
|
|
621
|
+
if s < 0:
|
|
622
|
+
if K > 0:
|
|
623
|
+
for e in _wig(1 - n0):
|
|
624
|
+
_stop_pos(cands, s, e, K)
|
|
625
|
+
else:
|
|
626
|
+
for e in (s + K * (P - 1) - 1, s + K * P):
|
|
627
|
+
if e < s:
|
|
628
|
+
cands.append((s, e, K))
|
|
629
|
+
return cands
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _cand_single_tail(pro):
|
|
633
|
+
sig, n_hat, n0 = pro.sigma, pro.n_hat, pro.N0
|
|
634
|
+
e_val = pro.A_hat # the single selected index at the tail probe
|
|
635
|
+
cands = []
|
|
636
|
+
if sig == 0 and e_val >= 0:
|
|
637
|
+
c = e_val
|
|
638
|
+
cands.append((c, c + 1, 1))
|
|
639
|
+
# "min-shape": select min(c, n-1) via a negative-step slice
|
|
640
|
+
for e in _wig(n0 - 2):
|
|
641
|
+
kappa = max(1, c - e)
|
|
642
|
+
_stop_negstep_front(cands, c, e, -kappa)
|
|
643
|
+
elif sig == 1:
|
|
644
|
+
s = e_val - n_hat
|
|
645
|
+
if s < 0:
|
|
646
|
+
if s + 1 < 0:
|
|
647
|
+
cands.append((s, s + 1, 1))
|
|
648
|
+
else:
|
|
649
|
+
cands.append((s, None, 1))
|
|
650
|
+
for e in _wig(1 - n0):
|
|
651
|
+
kappa = max(1, e - s)
|
|
652
|
+
_stop_pos(cands, s, e, kappa)
|
|
653
|
+
return cands
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _cand_single_bounded(pro):
|
|
657
|
+
fc, n0, n1 = pro.fc, pro.N0, pro.N1
|
|
658
|
+
e0, e1 = fc(n0)[0], fc(n1)[0]
|
|
659
|
+
cands = []
|
|
660
|
+
# forward bump family: selects [n+s]+ while it stays below stop
|
|
661
|
+
for s in {e1 - n1, e0 - n0}:
|
|
662
|
+
if s < 0:
|
|
663
|
+
for e in _wig(n1 + s + 1):
|
|
664
|
+
if e >= 1:
|
|
665
|
+
cands.append((s, e, max(1, e - s)))
|
|
666
|
+
# backward bump family: selects min(s', n-1) while it stays above stop
|
|
667
|
+
for sp in {e1, e0}:
|
|
668
|
+
if sp >= 0:
|
|
669
|
+
for ep in _wig(sp - n1 - 1):
|
|
670
|
+
if ep <= -1:
|
|
671
|
+
cands.append((sp, ep, -max(1, sp + 1)))
|
|
672
|
+
return cands
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _cand_bounded(pro):
|
|
676
|
+
K, fc, n0, n1 = pro.K, pro.fc, pro.N0, pro.N1
|
|
677
|
+
a0, a1 = fc(n0)[0], fc(n1)[0]
|
|
678
|
+
cands = []
|
|
679
|
+
if K > 0:
|
|
680
|
+
for s in {a1 - n1, a0 - n0}:
|
|
681
|
+
if s < 0:
|
|
682
|
+
for e in _wig(n1 + s + 1):
|
|
683
|
+
if e >= 1:
|
|
684
|
+
cands.append((s, e, K))
|
|
685
|
+
else:
|
|
686
|
+
for sp in {a1, a0}:
|
|
687
|
+
if sp >= 0:
|
|
688
|
+
for ep in _wig(sp - n1 - 1):
|
|
689
|
+
if ep <= -1:
|
|
690
|
+
cands.append((sp, ep, K))
|
|
691
|
+
return cands
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _candidates(pro):
|
|
695
|
+
if pro.max_len == 0:
|
|
696
|
+
raw = [(0, 0, 1)]
|
|
697
|
+
elif pro.max_len == 1:
|
|
698
|
+
raw = _cand_single_tail(pro) if pro.N1 is _UNBOUNDED else _cand_single_bounded(pro)
|
|
699
|
+
elif pro.tail_mode == "grow":
|
|
700
|
+
raw = _cand_grow(pro)
|
|
701
|
+
elif pro.tail_mode == "const":
|
|
702
|
+
raw = _cand_window(pro)
|
|
703
|
+
elif pro.tail_mode == "empty":
|
|
704
|
+
raw = _cand_bounded(pro)
|
|
705
|
+
else: # 'shrink': excluded by the structure lemmas
|
|
706
|
+
raw = []
|
|
707
|
+
out, seen = [], set()
|
|
708
|
+
for c in raw:
|
|
709
|
+
c = _prettify(c)
|
|
710
|
+
if c not in seen:
|
|
711
|
+
seen.add(c)
|
|
712
|
+
out.append(c)
|
|
713
|
+
return out
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _prettify(t):
|
|
717
|
+
"""Semantics-preserving cosmetic normalization of a slice triple."""
|
|
718
|
+
a, b, k = t
|
|
719
|
+
if k > 0 and a == 0:
|
|
720
|
+
a = None
|
|
721
|
+
if k < 0 and a == -1:
|
|
722
|
+
a = None
|
|
723
|
+
return (a, b, k)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# --------------------------------------------------------------------------
|
|
727
|
+
# Public API
|
|
728
|
+
# --------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def compose(s1, s2):
|
|
732
|
+
"""Return a slice s3 with arr[s1][s2] == arr[s3] for every sequence arr,
|
|
733
|
+
or None if no such slice exists.
|
|
734
|
+
"""
|
|
735
|
+
t1, t2 = _norm3(s1), _norm3(s2)
|
|
736
|
+
if t1[2] < 0:
|
|
737
|
+
# arr[s1] == arr[::-1][mirror(s1)] with mirror(s1).step > 0, and
|
|
738
|
+
# rev-first compositions transfer exactly through mirroring.
|
|
739
|
+
r = _compose_triples(_mirror(t1), t2)
|
|
740
|
+
return None if r is None else _to_slice(_prettify(_mirror(r)))
|
|
741
|
+
r = _compose_triples(t1, t2)
|
|
742
|
+
return None if r is None else _to_slice(r)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _compose_triples(t1, t2):
|
|
746
|
+
pro = _profile(t1, t2)
|
|
747
|
+
for cand in _candidates(pro):
|
|
748
|
+
if _verify(t1, t2, cand, pro.breaks):
|
|
749
|
+
return cand
|
|
750
|
+
return None
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class SlicePlan:
|
|
754
|
+
"""A lazily reduced chain of slices, built with bracket notation.
|
|
755
|
+
|
|
756
|
+
A plan gathers slices without doing any work: ``plan[1:][::2]`` returns
|
|
757
|
+
a new plan remembering both slices and leaves ``plan`` unchanged. ``s_``
|
|
758
|
+
is the ready-made empty plan, so ``s_[2:][3:]`` builds a plan directly
|
|
759
|
+
from slicing syntax. On first query (``slices``, ``slice``, ``apply``,
|
|
760
|
+
``==`` or ``hash``) the stored chain is reduced like a
|
|
761
|
+
:class:`SliceChain` and replaced in place by the reduced form, and the
|
|
762
|
+
plan marks itself reduced, so the work happens at most once per batch
|
|
763
|
+
of gathered slices. A plan derived from an already-reduced plan
|
|
764
|
+
inherits the collapsed prefix and only has its new tail to reduce;
|
|
765
|
+
because the reduction stack runs left to right, this yields the same
|
|
766
|
+
result as reducing the raw history, so query timing never affects the
|
|
767
|
+
outcome. ``repr`` shows the stored chain as-is without triggering
|
|
768
|
+
reduction.
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
__slots__ = ("_slices", "_reduced")
|
|
772
|
+
|
|
773
|
+
def __init__(self, *slices):
|
|
774
|
+
self._slices = tuple(_to_slice(_prettify(_norm3(s))) for s in slices)
|
|
775
|
+
self._reduced = len(self._slices) <= 1
|
|
776
|
+
|
|
777
|
+
def __getitem__(self, item):
|
|
778
|
+
if not isinstance(item, slice):
|
|
779
|
+
raise TypeError("SlicePlan supports slice indices only")
|
|
780
|
+
return SlicePlan(*self._slices, item)
|
|
781
|
+
|
|
782
|
+
@property
|
|
783
|
+
def slices(self):
|
|
784
|
+
"""The reduced tuple of slices; reduces (once) on first access."""
|
|
785
|
+
if not self._reduced:
|
|
786
|
+
self._slices = SliceChain(*self._slices).slices
|
|
787
|
+
self._reduced = True
|
|
788
|
+
return self._slices
|
|
789
|
+
|
|
790
|
+
@property
|
|
791
|
+
def slice(self):
|
|
792
|
+
"""The single equivalent slice, or None if the chain does not
|
|
793
|
+
reduce to one. The empty plan gives ``slice(None)``."""
|
|
794
|
+
red = self.slices
|
|
795
|
+
if len(red) > 1:
|
|
796
|
+
return None
|
|
797
|
+
return red[0] if red else slice(None)
|
|
798
|
+
|
|
799
|
+
def apply(self, seq):
|
|
800
|
+
"""Apply the (reduced) slices to seq in order."""
|
|
801
|
+
for s in self.slices:
|
|
802
|
+
seq = seq[s]
|
|
803
|
+
return seq
|
|
804
|
+
|
|
805
|
+
def __eq__(self, other):
|
|
806
|
+
if not isinstance(other, SlicePlan):
|
|
807
|
+
return NotImplemented
|
|
808
|
+
return self.slices == other.slices
|
|
809
|
+
|
|
810
|
+
def __hash__(self):
|
|
811
|
+
return hash(tuple((s.start, s.stop, s.step) for s in self.slices))
|
|
812
|
+
|
|
813
|
+
def __repr__(self):
|
|
814
|
+
inner = ", ".join(repr(s) for s in self._slices)
|
|
815
|
+
return f"SlicePlan({inner})"
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
s_ = SlicePlan()
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
class SliceChain:
|
|
822
|
+
"""A sequence of slices applied in order, e.g. seq[s0][s1]...[sk].
|
|
823
|
+
|
|
824
|
+
On construction, adjacent pairs are merged with :func:`compose` until no
|
|
825
|
+
adjacent pair merges (a merge is retried against the new left neighbor,
|
|
826
|
+
so cascading reductions are found). ``apply`` works the same whether or
|
|
827
|
+
not any merging happened.
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
__slots__ = ("_slices",)
|
|
831
|
+
|
|
832
|
+
def __init__(self, *slices):
|
|
833
|
+
stack = []
|
|
834
|
+
for s in slices:
|
|
835
|
+
cur = _to_slice(_prettify(_norm3(s)))
|
|
836
|
+
while stack:
|
|
837
|
+
merged = compose(stack[-1], cur)
|
|
838
|
+
if merged is None:
|
|
839
|
+
break
|
|
840
|
+
stack.pop()
|
|
841
|
+
cur = merged
|
|
842
|
+
stack.append(cur)
|
|
843
|
+
self._slices = tuple(stack)
|
|
844
|
+
|
|
845
|
+
@property
|
|
846
|
+
def slices(self):
|
|
847
|
+
"""The reduced tuple of slices."""
|
|
848
|
+
return self._slices
|
|
849
|
+
|
|
850
|
+
def apply(self, seq):
|
|
851
|
+
"""Apply the (reduced) slices to seq in order."""
|
|
852
|
+
for s in self._slices:
|
|
853
|
+
seq = seq[s]
|
|
854
|
+
return seq
|
|
855
|
+
|
|
856
|
+
def __len__(self):
|
|
857
|
+
return len(self._slices)
|
|
858
|
+
|
|
859
|
+
def __iter__(self):
|
|
860
|
+
return iter(self._slices)
|
|
861
|
+
|
|
862
|
+
def __eq__(self, other):
|
|
863
|
+
if not isinstance(other, SliceChain):
|
|
864
|
+
return NotImplemented
|
|
865
|
+
return self._slices == other._slices
|
|
866
|
+
|
|
867
|
+
def __hash__(self):
|
|
868
|
+
return hash(tuple((s.start, s.stop, s.step) for s in self._slices))
|
|
869
|
+
|
|
870
|
+
def __repr__(self):
|
|
871
|
+
inner = ", ".join(repr(s) for s in self._slices)
|
|
872
|
+
return f"SliceChain({inner})"
|
slicecompose/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slicecompose
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact composition of Python slices
|
|
5
|
+
Author-email: István Sárándi <istvan.sarandi@uni-tuebingen.de>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 István Sárándi
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/isarandi/slicecompose
|
|
29
|
+
Project-URL: Repository, https://github.com/isarandi/slicecompose
|
|
30
|
+
Project-URL: Issues, https://github.com/isarandi/slicecompose/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/isarandi/slicecompose/releases
|
|
32
|
+
Project-URL: Author, https://istvansarandi.com
|
|
33
|
+
Keywords: slice,slicing,composition,indexing,sequences,lazy
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
38
|
+
Classifier: Programming Language :: Python
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
45
|
+
Classifier: Operating System :: OS Independent
|
|
46
|
+
Requires-Python: >=3.10
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
License-File: LICENSE
|
|
49
|
+
Provides-Extra: test
|
|
50
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
51
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
52
|
+
Provides-Extra: lint
|
|
53
|
+
Requires-Dist: ruff; extra == "lint"
|
|
54
|
+
Requires-Dist: pre-commit; extra == "lint"
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: slicecompose[lint,test]; extra == "dev"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
# slicecompose
|
|
60
|
+
|
|
61
|
+
Exact composition of Python slices — pure Python, standard library only.
|
|
62
|
+
|
|
63
|
+
`compose(s1, s2)` returns a slice `s3` such that `arr[s1][s2] == arr[s3]` for
|
|
64
|
+
**every** sequence `arr` (every length, including 0), or `None` if no such
|
|
65
|
+
slice exists. Both directions of the decision are exact:
|
|
66
|
+
|
|
67
|
+
- **Sound** — a returned slice is equivalent for *all* lengths, not just
|
|
68
|
+
tested ones.
|
|
69
|
+
- **Complete** — `None` is returned only when no equivalent single slice
|
|
70
|
+
exists.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
>>> from slicecompose import compose, SliceChain
|
|
74
|
+
|
|
75
|
+
>>> compose(slice(2, None), slice(3, None)) # arr[2:][3:] == arr[5:]
|
|
76
|
+
slice(5, None, None)
|
|
77
|
+
|
|
78
|
+
>>> compose(slice(None, None, -1), slice(None, 1)) # arr[::-1][:1] == arr[-1:]
|
|
79
|
+
slice(-1, None, None)
|
|
80
|
+
|
|
81
|
+
>>> compose(slice(-2, None), slice(None, 1)) # arr[-2:][:1] == arr[-2::2]
|
|
82
|
+
slice(-2, None, 2)
|
|
83
|
+
|
|
84
|
+
>>> compose(slice(None, None, 2), slice(None, None, -1)) is None
|
|
85
|
+
True
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The last pair really has no equivalent: `arr[::2][::-1]` starts at index
|
|
89
|
+
`n-1` for odd `n` but `n-2` for even `n`, and no fixed slice can depend on
|
|
90
|
+
the parity of the length. The third example shows the opposite surprise: the
|
|
91
|
+
result must select the single element at index `max(0, n-2)`, which no
|
|
92
|
+
"obvious" candidate like `[-2:-1]` does (it is empty at `n == 1`) — but
|
|
93
|
+
`[-2::2]` does, for every `n`.
|
|
94
|
+
|
|
95
|
+
Why this is harder than plugging into `slice.indices()`: the answer must be
|
|
96
|
+
a fixed slice that works *universally*, without knowing the length of the
|
|
97
|
+
sequence it will be applied to. Clamping, negative indices, and the
|
|
98
|
+
sign-dependent defaults make the composed selection a piecewise function of
|
|
99
|
+
the length; `compose` decides exactly whether that function is realizable by
|
|
100
|
+
a single slice.
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
### `compose(s1, s2) -> slice | None`
|
|
105
|
+
|
|
106
|
+
The core decision procedure described above. Accepts any valid slice
|
|
107
|
+
objects, including huge field values (no ranges are ever materialized;
|
|
108
|
+
everything is decided arithmetically).
|
|
109
|
+
|
|
110
|
+
### `SliceChain(*slices)`
|
|
111
|
+
|
|
112
|
+
Container for a sequence of slices applied in order, e.g.
|
|
113
|
+
`seq[s0][s1]...[sk]`.
|
|
114
|
+
|
|
115
|
+
- On construction, adjacent pairs are merged with `compose` until no
|
|
116
|
+
adjacent pair merges; a successful merge is retried against its new left
|
|
117
|
+
neighbor, so cascading reductions are found (e.g. `[::2], [::-1], [::-1]`
|
|
118
|
+
reduces to `[::2]` — the last two merge first).
|
|
119
|
+
- `.slices` — the reduced tuple of slices.
|
|
120
|
+
- `.apply(seq)` — applies the (reduced) slices in order; correct whether or
|
|
121
|
+
not any merging happened.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
>>> ch = SliceChain(slice(None, None, 2), slice(None, None, -1), slice(None, None, -1))
|
|
125
|
+
>>> ch.slices
|
|
126
|
+
(slice(None, None, 2),)
|
|
127
|
+
>>> SliceChain(slice(2, None), slice(3, None)).apply("abcdefgh")
|
|
128
|
+
'fgh'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Note that the reduced chain is *a* fixpoint of adjacent-pair merging, not
|
|
132
|
+
necessarily the shortest one: pairwise reduction is not confluent, and there
|
|
133
|
+
are chains whose total effect is a single slice even though no adjacent pair
|
|
134
|
+
merges at all. See `EXPLANATION.md` §7 for verified examples and details.
|
|
135
|
+
|
|
136
|
+
### `SlicePlan` / `s_`
|
|
137
|
+
|
|
138
|
+
Lazy plan building with ordinary bracket notation. `s_` is the ready-made
|
|
139
|
+
empty plan; slicing a plan returns a *new* plan (the original is unchanged),
|
|
140
|
+
and nothing is computed while slices are gathered. On the first query —
|
|
141
|
+
`.slices`, `.slice`, `.apply(seq)`, `==`/`hash` — the plan reduces its
|
|
142
|
+
stored chain in place and remembers that it did, so the reduction runs at
|
|
143
|
+
most once per batch of gathered slices.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
>>> from slicecompose import s_
|
|
147
|
+
>>> s_[2:][3:].slice
|
|
148
|
+
slice(5, None, None)
|
|
149
|
+
>>> plan = s_[100:]
|
|
150
|
+
>>> plan[::2].slices # branching; plan itself is unchanged
|
|
151
|
+
(slice(100, None, 2),)
|
|
152
|
+
>>> s_[::2][::-1].slice is None # irreducible pairs stay a chain
|
|
153
|
+
True
|
|
154
|
+
>>> s_[::2][::-1].apply(tuple(range(9)))
|
|
155
|
+
(8, 6, 4, 2, 0)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This is aimed at expensive media (video streams, remote arrays): build the
|
|
159
|
+
access plan symbolically with normal slicing syntax, reduce it, and only
|
|
160
|
+
then touch the data. A reduced slice needs no length to be executed — its
|
|
161
|
+
anchors are only "offset from the front" and "offset from the end" — so the
|
|
162
|
+
plan works even when the element count is unknown or unreliable.
|
|
163
|
+
|
|
164
|
+
## Correctness
|
|
165
|
+
|
|
166
|
+
`EXPLANATION.md` contains the full argument for why the implementation is
|
|
167
|
+
sound and complete for every length — the infinite quantification over `n`
|
|
168
|
+
is reduced to finitely many exact checks (regime breakpoints, per-stretch
|
|
169
|
+
staircase/sawtooth normal forms, an exact verifier, and profile-forced
|
|
170
|
+
candidate generation). Returned slices are always validated by the exact
|
|
171
|
+
verifier, so soundness does not rest on the candidate generator.
|
|
172
|
+
|
|
173
|
+
The test suite (`test_slicecompose.py`) additionally audits both failure
|
|
174
|
+
modes empirically: exhaustive small-field grids checked for wrong merges by
|
|
175
|
+
brute force and for missed merges against large candidate tables, huge-field
|
|
176
|
+
randomized soundness checks, structural consistency probes, and the problem
|
|
177
|
+
statement's examples.
|
|
178
|
+
|
|
179
|
+
## Installation
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install slicecompose # from PyPI, once published
|
|
183
|
+
pip install . # from a checkout
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Pure Python, no dependencies.
|
|
187
|
+
|
|
188
|
+
## Running the tests
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pytest # quick tier, ~30 s
|
|
192
|
+
SLICECOMPOSE_FULL=1 pytest # exhaustive grids, ~35 min
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`verify_examples.py` independently re-checks the examples claimed in
|
|
196
|
+
`PROBLEM.md` by direct evaluation.
|
|
197
|
+
|
|
198
|
+
## Files
|
|
199
|
+
|
|
200
|
+
| file | contents |
|
|
201
|
+
|------------------------------|----------------------------------------------------------|
|
|
202
|
+
| `src/slicecompose/` | the library (`compose`, `SlicePlan`/`s_`, `SliceChain`) |
|
|
203
|
+
| `tests/test_slicecompose.py` | test suite (quick and exhaustive tiers) |
|
|
204
|
+
| `PROBLEM.md` | the problem statement |
|
|
205
|
+
| `EXPLANATION.md` | soundness/completeness argument |
|
|
206
|
+
| `verify_examples.py` | standalone check of the problem's examples |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
slicecompose/__init__.py,sha256=Vs66wRek-2ezMIoYsVvx83L5Iw74kSNoLkh7Kz53hXA,28149
|
|
2
|
+
slicecompose/_version.py,sha256=n_5vdJsPNu7wZ57LGuRL585uvll-hiuvZUBWzdG0RQU,520
|
|
3
|
+
slicecompose-0.1.0.dist-info/licenses/LICENSE,sha256=Yo-WMDNvrjL9OitVRBuKaYeo33FiSJLpONpT68bvA-s,1074
|
|
4
|
+
slicecompose-0.1.0.dist-info/METADATA,sha256=2TyUJcOuUn-wWwI-b7dKJ_ydQfsgchy8vbbfmhl0N8w,8753
|
|
5
|
+
slicecompose-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
slicecompose-0.1.0.dist-info/scm_file_list.json,sha256=etrWSXBGntF9qzj2pRZ43j2X3RjZJ3K9dLJigVy7uT8,289
|
|
7
|
+
slicecompose-0.1.0.dist-info/scm_version.json,sha256=GBIDAdGgdV59UHaoI0hz1u_QOuisrSMw_jyTixf-Rpk,160
|
|
8
|
+
slicecompose-0.1.0.dist-info/top_level.txt,sha256=nnony279CCiGALJDWZB3u4KZ9GeSHCeeUTbqyU92S28,13
|
|
9
|
+
slicecompose-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 István Sárándi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".gitignore",
|
|
4
|
+
"README.md",
|
|
5
|
+
"EXPLANATION.md",
|
|
6
|
+
"CLAUDE.md",
|
|
7
|
+
"pyproject.toml",
|
|
8
|
+
"PROBLEM.md",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"verify_examples.py",
|
|
11
|
+
"src/slicecompose/__init__.py",
|
|
12
|
+
".github/workflows/python-publish.yml",
|
|
13
|
+
"tests/test_slicecompose.py"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
slicecompose
|