esets-lib 0.1.0__tar.gz → 0.3.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esets-lib
3
- Version: 0.1.0
3
+ Version: 0.3.2
4
4
  Summary: Lazy, index-addressable enumerated sets, including a full combinatorics family (permutations, combinations, arrangements, subsets, partitions, derangements, and Cartesian products)
5
5
  Project-URL: Homepage, https://github.com/ffavela/esets
6
6
  Project-URL: Repository, https://github.com/ffavela/esets
@@ -50,6 +50,34 @@ still:
50
50
  >>>
51
51
  ```
52
52
 
53
+ ## The docs, if you want to skip ahead
54
+
55
+ This README is a long, informal walkthrough. If you'd rather get to
56
+ the substance directly:
57
+
58
+ * [COMBINATORICS.md](https://github.com/ffavela/esets/blob/main/COMBINATORICS.md)
59
+ -- the combinatorics family: permutations, combinations,
60
+ arrangements, subsets, integer partitions, set partitions,
61
+ derangements, and Cartesian products, all addressable by index with
62
+ no enumeration involved.
63
+ * [POKER.md](https://github.com/ffavela/esets/blob/main/POKER.md) --
64
+ several of those put to work together on one running example, a
65
+ deck of cards.
66
+ * [COMBINATORIALDB.md](https://github.com/ffavela/esets/blob/main/COMBINATORIALDB.md)
67
+ -- a second worked example, a shop database: comparing a raw
68
+ purchase log against a `set`-based, an arrangement-indexed, and a
69
+ combination-indexed representation, for both correctness and size.
70
+ * [INCLUSIONEXCLUSION.md](https://github.com/ffavela/esets/blob/main/INCLUSIONEXCLUSION.md)
71
+ -- the library's own counting recursion benchmarked against
72
+ inclusion-exclusion, with the real performance fixes that
73
+ benchmarking turned up.
74
+ * [FLOAT64S.md](https://github.com/ffavela/esets/blob/main/FLOAT64S.md)
75
+ -- an eset that enumerates all 64-bit floats.
76
+ * [TEXTENCODE.md](https://github.com/ffavela/esets/blob/main/TEXTENCODE.md)
77
+ -- character and word alphabets built from a small text file, encoded
78
+ with `Arranger` and with `Combinator`+`Permutator` by hand, as a
79
+ first step toward larger-text, whole-language "compression."
80
+
53
81
  ## What is it? (An informal introduction)
54
82
 
55
83
  **esets** stands for enumerated sets. They can handle arbitrarily
@@ -26,6 +26,34 @@ still:
26
26
  >>>
27
27
  ```
28
28
 
29
+ ## The docs, if you want to skip ahead
30
+
31
+ This README is a long, informal walkthrough. If you'd rather get to
32
+ the substance directly:
33
+
34
+ * [COMBINATORICS.md](https://github.com/ffavela/esets/blob/main/COMBINATORICS.md)
35
+ -- the combinatorics family: permutations, combinations,
36
+ arrangements, subsets, integer partitions, set partitions,
37
+ derangements, and Cartesian products, all addressable by index with
38
+ no enumeration involved.
39
+ * [POKER.md](https://github.com/ffavela/esets/blob/main/POKER.md) --
40
+ several of those put to work together on one running example, a
41
+ deck of cards.
42
+ * [COMBINATORIALDB.md](https://github.com/ffavela/esets/blob/main/COMBINATORIALDB.md)
43
+ -- a second worked example, a shop database: comparing a raw
44
+ purchase log against a `set`-based, an arrangement-indexed, and a
45
+ combination-indexed representation, for both correctness and size.
46
+ * [INCLUSIONEXCLUSION.md](https://github.com/ffavela/esets/blob/main/INCLUSIONEXCLUSION.md)
47
+ -- the library's own counting recursion benchmarked against
48
+ inclusion-exclusion, with the real performance fixes that
49
+ benchmarking turned up.
50
+ * [FLOAT64S.md](https://github.com/ffavela/esets/blob/main/FLOAT64S.md)
51
+ -- an eset that enumerates all 64-bit floats.
52
+ * [TEXTENCODE.md](https://github.com/ffavela/esets/blob/main/TEXTENCODE.md)
53
+ -- character and word alphabets built from a small text file, encoded
54
+ with `Arranger` and with `Combinator`+`Permutator` by hand, as a
55
+ first step toward larger-text, whole-language "compression."
56
+
29
57
  ## What is it? (An informal introduction)
30
58
 
31
59
  **esets** stands for enumerated sets. They can handle arbitrarily
@@ -17,10 +17,7 @@ class BEvens(BEset):
17
17
 
18
18
 
19
19
  class BWholesSHA256s(BEset):
20
- """A blind eset of SHA256s of the Whole numbers. It uses ascii
21
- encoding since only decimal numbers are expected.
22
-
23
- """
20
+ """A blind eset of SHA256s of the Whole numbers. It uses ascii encoding since only decimal numbers are expected."""
24
21
 
25
22
  def direct_function(self, i):
26
23
  return hashlib.sha256(str(i).encode('ascii')).hexdigest()
@@ -109,7 +109,9 @@ class Distinct_Permutator(Distinct_PermutatorABCMixin):
109
109
  """Delegating to eset_obj like EMixinABC, but also carrying
110
110
  self.elements along when slicing."""
111
111
  if isinstance(key, slice):
112
- return type(self)(xtra_params=(self.eset_obj[key], self.elements))
112
+ result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
113
+ result.sliced = True
114
+ return result
113
115
  return super().__getitem__(key)
114
116
 
115
117
 
@@ -289,9 +291,11 @@ class Permutator(PermutatorABCMixin):
289
291
  """Delegating to eset_obj like EMixinABC, but also carrying
290
292
  self.elements/self.classes along when slicing."""
291
293
  if isinstance(key, slice):
292
- return type(self)(
294
+ result = type(self)(
293
295
  xtra_params=(self.eset_obj[key], self.elements, self.classes)
294
296
  )
297
+ result.sliced = True
298
+ return result
295
299
  return super().__getitem__(key)
296
300
 
297
301
 
@@ -422,7 +426,9 @@ class Distinct_Combinator(Distinct_CombinatorABCMixin):
422
426
 
423
427
  def __getitem__(self, key):
424
428
  if isinstance(key, slice):
425
- return type(self)(xtra_params=(self.eset_obj[key], self.elements, self.k))
429
+ result = type(self)(xtra_params=(self.eset_obj[key], self.elements, self.k))
430
+ result.sliced = True
431
+ return result
426
432
  return super().__getitem__(key)
427
433
 
428
434
 
@@ -592,9 +598,11 @@ class Combinator(CombinatorABCMixin):
592
598
 
593
599
  def __getitem__(self, key):
594
600
  if isinstance(key, slice):
595
- return type(self)(
601
+ result = type(self)(
596
602
  xtra_params=(self.eset_obj[key], self.elements, self.k, self.classes)
597
603
  )
604
+ result.sliced = True
605
+ return result
598
606
  return super().__getitem__(key)
599
607
 
600
608
 
@@ -768,7 +776,9 @@ class Distinct_Arranger(Distinct_ArrangerABCMixin):
768
776
 
769
777
  def __getitem__(self, key):
770
778
  if isinstance(key, slice):
771
- return type(self)(xtra_params=(self.eset_obj[key], self.elements, self.r))
779
+ result = type(self)(xtra_params=(self.eset_obj[key], self.elements, self.r))
780
+ result.sliced = True
781
+ return result
772
782
  return super().__getitem__(key)
773
783
 
774
784
 
@@ -798,6 +808,29 @@ class Natural_Multiset_Arranger(Eset):
798
808
  arrangement #0 agree with Natural_Arranger's ordering when every
799
809
  capacity is 1, and with Natural_Multiset_Permutator's ordering
800
810
  when r equals the full multiset size.
811
+
812
+ That position-by-position shape is also why ranking's recursion
813
+ depth is O(classes * r) in the worst case, not O(classes) --
814
+ each of the r positions can try up to `classes` candidates before
815
+ landing on the right one. A prototype class-by-class ranking
816
+ (decide how many of the smallest remaining class, and which of
817
+ the still-open positions they occupy, via plain get_combination/
818
+ get_combination_number over positions, then recurse into the next
819
+ class for the remaining positions) was benchmarked and verified
820
+ as a correct bijection: it cuts depth to O(classes) and wins big
821
+ on many-classes/tight-capacity shapes (~470x faster before the
822
+ memo fix below existed, still much shallower after), but costs
823
+ more wall-clock time than the position-by-position approach on
824
+ few-classes/large-r shapes (get_combination_number's own O(r)
825
+ candidate search per class-step isn't free), and it ranks
826
+ arrangements in a different order than this class does today, so
827
+ switching isn't behavior-preserving the way the memo fix is.
828
+ Shipping it for real would mean dispatching between the two by
829
+ shape -- the same treatment multiset_combination_count already
830
+ gives inclusion-exclusion -- plus its own benchmark file, not a
831
+ blanket replacement. Not attempted here; left as a known, deferred
832
+ option for whoever next needs ranking to survive a much larger
833
+ number of classes than it currently does.
801
834
  """
802
835
 
803
836
  def __init__(self, *args, **kwargs):
@@ -929,9 +962,11 @@ class Arranger(ArrangerABCMixin):
929
962
 
930
963
  def __getitem__(self, key):
931
964
  if isinstance(key, slice):
932
- return type(self)(
965
+ result = type(self)(
933
966
  xtra_params=(self.eset_obj[key], self.elements, self.r, self.classes)
934
967
  )
968
+ result.sliced = True
969
+ return result
935
970
  return super().__getitem__(key)
936
971
 
937
972
 
@@ -1047,7 +1082,9 @@ class Distinct_Powerset(Distinct_PowersetABCMixin):
1047
1082
 
1048
1083
  def __getitem__(self, key):
1049
1084
  if isinstance(key, slice):
1050
- return type(self)(xtra_params=(self.eset_obj[key], self.elements))
1085
+ result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
1086
+ result.sliced = True
1087
+ return result
1051
1088
  return super().__getitem__(key)
1052
1089
 
1053
1090
 
@@ -1188,9 +1225,11 @@ class Powerset(PowersetABCMixin):
1188
1225
 
1189
1226
  def __getitem__(self, key):
1190
1227
  if isinstance(key, slice):
1191
- return type(self)(
1228
+ result = type(self)(
1192
1229
  xtra_params=(self.eset_obj[key], self.elements, self.classes)
1193
1230
  )
1231
+ result.sliced = True
1232
+ return result
1194
1233
  return super().__getitem__(key)
1195
1234
 
1196
1235
 
@@ -1349,7 +1388,9 @@ class Set_Partitioner(Set_PartitionerABCMixin):
1349
1388
 
1350
1389
  def __getitem__(self, key):
1351
1390
  if isinstance(key, slice):
1352
- return type(self)(xtra_params=(self.eset_obj[key], self.elements))
1391
+ result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
1392
+ result.sliced = True
1393
+ return result
1353
1394
  return super().__getitem__(key)
1354
1395
 
1355
1396
 
@@ -1508,7 +1549,9 @@ class Distinct_Derangement(Distinct_DerangementABCMixin):
1508
1549
 
1509
1550
  def __getitem__(self, key):
1510
1551
  if isinstance(key, slice):
1511
- return type(self)(xtra_params=(self.eset_obj[key], self.elements))
1552
+ result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
1553
+ result.sliced = True
1554
+ return result
1512
1555
  return super().__getitem__(key)
1513
1556
 
1514
1557
 
@@ -1633,5 +1676,7 @@ class Cartesian_Product(Cartesian_ProductABCMixin):
1633
1676
 
1634
1677
  def __getitem__(self, key):
1635
1678
  if isinstance(key, slice):
1636
- return type(self)(xtra_params=(self.eset_obj[key], self.sources))
1679
+ result = type(self)(xtra_params=(self.eset_obj[key], self.sources))
1680
+ result.sliced = True
1681
+ return result
1637
1682
  return super().__getitem__(key)
@@ -153,8 +153,54 @@ def get_combination_number(combination: tuple[int], n: int) -> int | None:
153
153
  return rank(list(range(n)), list(combination))
154
154
 
155
155
 
156
- def multiset_combination_count(multiplicities: tuple[int, ...], k: int) -> int:
157
- memo: dict[tuple[tuple[int, ...], int], int] = {}
156
+ # Below this many remaining classes, inclusion-exclusion's 2**len(rem)
157
+ # subsets are cheap regardless of capacity size -- and above this many
158
+ # remaining classes, that same sum stops being cheap. Below this
159
+ # capacity, the branching recursion's own cost (tied to capacity, not
160
+ # class count) is already small enough that switching buys nothing.
161
+ # Both are load-bearing together, not independently: inclusion-exclusion
162
+ # has its own weak spot (a target near half of a subproblem's own total
163
+ # capacity prunes poorly, regardless of how large that capacity is), so
164
+ # this only dispatches when there are few *enough* classes left that even
165
+ # that weak spot stays cheap -- checked up to 15 classes at capacity 100,
166
+ # worst case (target at exactly half), before shipping this threshold.
167
+ _INCLUSION_EXCLUSION_CLASS_THRESHOLD = 15
168
+ _INCLUSION_EXCLUSION_CAPACITY_THRESHOLD = 15
169
+
170
+
171
+ def _inclusion_exclusion_combination_count(
172
+ rem: tuple[int, ...], remaining_k: int
173
+ ) -> int:
174
+ m = len(rem)
175
+ total_answer = 0
176
+
177
+ def rec(i: int, excess: int, sign: int) -> None:
178
+ nonlocal total_answer
179
+ if excess > remaining_k:
180
+ return
181
+ if i == m:
182
+ total_answer += sign * comb(m - 1 + remaining_k - excess, m - 1)
183
+ return
184
+ rec(i + 1, excess, sign)
185
+ rec(i + 1, excess + rem[i] + 1, -sign)
186
+
187
+ rec(0, 0, 1)
188
+ return total_answer
189
+
190
+
191
+ def multiset_combination_count(
192
+ multiplicities: tuple[int, ...],
193
+ k: int,
194
+ _memo: dict[tuple[tuple[int, ...], int], int] | None = None,
195
+ ) -> int:
196
+ # get_multiset_combination/get_multiset_combination_number call this
197
+ # once per candidate tried at every class, and get_multiset_subset/
198
+ # get_multiset_subset_number call it once per size -- both pass their
199
+ # own long-lived _memo through every call instead of letting each one
200
+ # start over from empty, since overlapping subproblems (same
201
+ # sorted-suffix, same remaining_k) come up constantly across those
202
+ # calls.
203
+ memo: dict[tuple[tuple[int, ...], int], int] = {} if _memo is None else _memo
158
204
 
159
205
  def count(rem: tuple[int, ...], remaining_k: int) -> int:
160
206
  key = (rem, remaining_k)
@@ -164,24 +210,63 @@ def multiset_combination_count(multiplicities: tuple[int, ...], k: int) -> int:
164
210
  result = 1
165
211
  elif not rem:
166
212
  result = 0
167
- elif remaining_k <= min(rem):
168
- # Capacities can't bind: plain stars-and-bars (mset's C1 shortcut).
169
- result = comb(len(rem) + remaining_k - 1, remaining_k)
170
213
  else:
171
- result = sum(
172
- count(rem[1:], remaining_k - t)
173
- for t in range(min(rem[0], remaining_k) + 1)
174
- )
214
+ total = sum(rem)
215
+ if remaining_k > total:
216
+ # Even every remaining class at full capacity can't reach
217
+ # remaining_k: no need to branch to find that out.
218
+ result = 0
219
+ elif remaining_k <= min(rem):
220
+ # Capacities can't bind: plain stars-and-bars (mset's C1 shortcut).
221
+ result = comb(len(rem) + remaining_k - 1, remaining_k)
222
+ elif total - remaining_k <= min(rem):
223
+ # Gaps can't bind: x_i <-> c_i - x_i is a bijection between
224
+ # assignments summing to remaining_k and ones summing to
225
+ # total - remaining_k (unused capacity), so the same
226
+ # "can't bind" shortcut applies to that leftover instead.
227
+ # Only fires when the leftover slack could fit inside a
228
+ # single remaining class's own capacity -- when even the
229
+ # smallest remaining capacity is less than the slack, this
230
+ # doesn't trigger and the recursion still has to walk down
231
+ # to the original shortcut above, one class at a time.
232
+ gap = total - remaining_k
233
+ result = comb(len(rem) + gap - 1, gap)
234
+ elif (
235
+ len(rem) <= _INCLUSION_EXCLUSION_CLASS_THRESHOLD
236
+ and min(rem) >= _INCLUSION_EXCLUSION_CAPACITY_THRESHOLD
237
+ ):
238
+ # Few classes, each with plenty of room: this is exactly
239
+ # where this recursion's own branching factor (tied to
240
+ # capacity size, not class count) stops being competitive
241
+ # with inclusion-exclusion's 2**len(rem), which doesn't
242
+ # care how large the capacities are at all.
243
+ result = _inclusion_exclusion_combination_count(rem, remaining_k)
244
+ else:
245
+ # t (how many go to this class) can't be so small that the
246
+ # rest of the classes, even maxed out, can't cover what's
247
+ # left -- that's a hopeless branch just as much as t being
248
+ # too large is, so bound the loop on both ends instead of
249
+ # generating and then discarding those calls.
250
+ tail_capacity = total - rem[0]
251
+ lo = max(0, remaining_k - tail_capacity)
252
+ hi = min(rem[0], remaining_k)
253
+ result = sum(count(rem[1:], remaining_k - t) for t in range(lo, hi + 1))
175
254
  memo[key] = result
176
255
  return result
177
256
 
178
- return count(multiplicities, k)
257
+ # The count is invariant under reordering the classes (it depends only
258
+ # on the multiset of capacities, not their positions), but the
259
+ # recursion's cost isn't: ascending order lets the "capacities can't
260
+ # bind" shortcut above trigger as soon as only large-capacity classes
261
+ # are left, instead of branching on them first.
262
+ return count(tuple(sorted(multiplicities)), k)
179
263
 
180
264
 
181
265
  def get_multiset_combination(
182
266
  val: int, multiplicities: tuple[int, ...], k: int
183
267
  ) -> tuple[int] | None:
184
- total = multiset_combination_count(multiplicities, k)
268
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
269
+ total = multiset_combination_count(multiplicities, k, memo)
185
270
  if val >= total or val < 0:
186
271
  return None
187
272
 
@@ -211,7 +296,7 @@ def get_multiset_combination(
211
296
  retlist: list[int],
212
297
  t: int,
213
298
  ) -> list[int]:
214
- block = multiset_combination_count(classes[1:], remaining_k - t)
299
+ block = multiset_combination_count(classes[1:], remaining_k - t, memo)
215
300
  if resval < block:
216
301
  return place(
217
302
  resval,
@@ -237,6 +322,8 @@ def get_multiset_combination_number(
237
322
  ):
238
323
  return None
239
324
 
325
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
326
+
240
327
  def rank(
241
328
  classes: tuple[int, ...], remaining_k: int, class_id: int, seq: list[int]
242
329
  ) -> int:
@@ -257,7 +344,7 @@ def get_multiset_combination_number(
257
344
  t: int,
258
345
  actual: int,
259
346
  ) -> int:
260
- block = multiset_combination_count(classes[1:], remaining_k - t)
347
+ block = multiset_combination_count(classes[1:], remaining_k - t, memo)
261
348
  if t == actual:
262
349
  return rank(classes[1:], remaining_k - t, class_id + 1, seq[t:])
263
350
  return block + locate(classes, remaining_k, class_id, seq, t - 1, actual)
@@ -365,8 +452,18 @@ def get_arrangement_number(arrangement: tuple[int], n: int) -> int | None:
365
452
  return get_number(list(arrangement), r)
366
453
 
367
454
 
368
- def multiset_arrangement_count(multiplicities: tuple[int, ...], r: int) -> int:
369
- memo: dict[tuple[tuple[int, ...], int], int] = {}
455
+ def multiset_arrangement_count(
456
+ multiplicities: tuple[int, ...],
457
+ r: int,
458
+ _memo: dict[tuple[tuple[int, ...], int], int] | None = None,
459
+ ) -> int:
460
+ # get_multiset_arrangement/get_multiset_arrangement_number call this
461
+ # once per candidate tried at every position, so they pass their own
462
+ # long-lived _memo through every call instead of letting each call
463
+ # start over from an empty one -- the two share overlapping
464
+ # subproblems (same sorted-suffix, same remaining_r) constantly, and
465
+ # without a shared memo those get recomputed from scratch every time.
466
+ memo: dict[tuple[tuple[int, ...], int], int] = {} if _memo is None else _memo
370
467
 
371
468
  def g(rem: tuple[int, ...], remaining_r: int) -> int:
372
469
  key = (rem, remaining_r)
@@ -377,23 +474,40 @@ def multiset_arrangement_count(multiplicities: tuple[int, ...], r: int) -> int:
377
474
  elif not rem:
378
475
  result = 0
379
476
  else:
380
- # Choose x occurrences of the current class, choose which x of
381
- # the remaining_r open slots they take (comb), and recurse on
382
- # the rest with the later classes.
383
- result = sum(
384
- comb(remaining_r, x) * g(rem[1:], remaining_r - x)
385
- for x in range(min(rem[0], remaining_r) + 1)
386
- )
477
+ total = sum(rem)
478
+ if remaining_r > total:
479
+ # Even every remaining class at full capacity can't reach
480
+ # remaining_r: no need to branch to find that out.
481
+ result = 0
482
+ elif remaining_r <= min(rem):
483
+ # Capacities can't bind: every one of the remaining_r ordered
484
+ # slots can freely pick any of the len(rem) remaining classes.
485
+ result = len(rem) ** remaining_r
486
+ else:
487
+ # x (how many go to this class) can't be so small that the
488
+ # rest of the classes, even maxed out, can't cover what's
489
+ # left -- bound the loop on both ends instead of generating
490
+ # and then discarding those calls.
491
+ tail_capacity = total - rem[0]
492
+ lo = max(0, remaining_r - tail_capacity)
493
+ hi = min(rem[0], remaining_r)
494
+ result = sum(
495
+ comb(remaining_r, x) * g(rem[1:], remaining_r - x)
496
+ for x in range(lo, hi + 1)
497
+ )
387
498
  memo[key] = result
388
499
  return result
389
500
 
390
- return g(multiplicities, r)
501
+ # See multiset_combination_count: the count doesn't depend on class
502
+ # order, but ascending order lets the shortcut above trigger sooner.
503
+ return g(tuple(sorted(multiplicities)), r)
391
504
 
392
505
 
393
506
  def get_multiset_arrangement(
394
507
  val: int, multiplicities: tuple[int, ...], r: int
395
508
  ) -> tuple[int] | None:
396
- total = multiset_arrangement_count(multiplicities, r)
509
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
510
+ total = multiset_arrangement_count(multiplicities, r, memo)
397
511
  if val >= total or val < 0:
398
512
  return None
399
513
 
@@ -419,7 +533,7 @@ def get_multiset_arrangement(
419
533
  new_mult = list(remaining_mult)
420
534
  new_mult[label] -= 1
421
535
  rest_mult = tuple(new_mult)
422
- block = multiset_arrangement_count(rest_mult, remaining_r - 1)
536
+ block = multiset_arrangement_count(rest_mult, remaining_r - 1, memo)
423
537
  if resval < block:
424
538
  return place(resval, rest_mult, remaining_r - 1, retlist + [label])
425
539
  return try_candidate(
@@ -440,6 +554,8 @@ def get_multiset_arrangement_number(
440
554
  ):
441
555
  return None
442
556
 
557
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
558
+
443
559
  def rank(remaining_mult: tuple[int, ...], remaining_r: int, seq: list[int]) -> int:
444
560
  if not seq:
445
561
  return 0
@@ -456,7 +572,7 @@ def get_multiset_arrangement_number(
456
572
  new_mult = list(remaining_mult)
457
573
  new_mult[label] -= 1
458
574
  rest_mult = tuple(new_mult)
459
- block = multiset_arrangement_count(rest_mult, remaining_r - 1)
575
+ block = multiset_arrangement_count(rest_mult, remaining_r - 1, memo)
460
576
  if label == seq[0]:
461
577
  return rank(rest_mult, remaining_r - 1, seq[1:])
462
578
  return block + locate(remaining_mult, remaining_r, seq, candidates[1:])
@@ -503,8 +619,10 @@ def get_multiset_subset(val: int, multiplicities: tuple[int, ...]) -> tuple[int]
503
619
  if val >= total or val < 0:
504
620
  return None
505
621
 
622
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
623
+
506
624
  def try_size(resval: int, k: int) -> tuple[int]:
507
- block = multiset_combination_count(multiplicities, k)
625
+ block = multiset_combination_count(multiplicities, k, memo)
508
626
  if resval < block:
509
627
  return get_multiset_combination(resval, multiplicities, k)
510
628
  return try_size(resval - block, k + 1)
@@ -519,10 +637,14 @@ def get_multiset_subset_number(
519
637
  if combo_rank is None:
520
638
  return None
521
639
 
640
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
641
+
522
642
  def offset(size: int) -> int:
523
643
  if size == 0:
524
644
  return 0
525
- return multiset_combination_count(multiplicities, size - 1) + offset(size - 1)
645
+ return multiset_combination_count(multiplicities, size - 1, memo) + offset(
646
+ size - 1
647
+ )
526
648
 
527
649
  return offset(len(subset)) + combo_rank
528
650
 
@@ -470,7 +470,9 @@ class EABCMixinFactory(abc.ABC):
470
470
  def __getitem__(self, key):
471
471
  """Delegating __getitem__ to the eset_obj"""
472
472
  if isinstance(key, slice):
473
- return type(self)(xtra_params=(self.eset_obj[key],))
473
+ result = type(self)(xtra_params=(self.eset_obj[key],))
474
+ result.sliced = True
475
+ return result
474
476
  elif isinstance(key, int):
475
477
  mix_val = self.get_mix_val(self.eset_obj[key])
476
478
  return mix_val
@@ -386,7 +386,9 @@ class Float64s(Eset):
386
386
 
387
387
  """
388
388
  if isinstance(key, slice):
389
- return Float64s(xtra_params=(self.f64tpls[key],))
389
+ result = Float64s(xtra_params=(self.f64tpls[key],))
390
+ result.sliced = True
391
+ return result
390
392
  elif isinstance(key, int):
391
393
  return tpl2float(self.f64tpls[key])
392
394
  raise ValueError('Need a slice or an integer')
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "esets-lib"
7
- version = "0.1.0"
7
+ version = "0.3.2"
8
8
  description = "Lazy, index-addressable enumerated sets, including a full combinatorics family (permutations, combinations, arrangements, subsets, partitions, derangements, and Cartesian products)"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
File without changes
File without changes
File without changes