esets-lib 0.3.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.3.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
@@ -73,6 +73,10 @@ the substance directly:
73
73
  benchmarking turned up.
74
74
  * [FLOAT64S.md](https://github.com/ffavela/esets/blob/main/FLOAT64S.md)
75
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."
76
80
 
77
81
  ## What is it? (An informal introduction)
78
82
 
@@ -49,6 +49,10 @@ the substance directly:
49
49
  benchmarking turned up.
50
50
  * [FLOAT64S.md](https://github.com/ffavela/esets/blob/main/FLOAT64S.md)
51
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."
52
56
 
53
57
  ## What is it? (An informal introduction)
54
58
 
@@ -808,6 +808,29 @@ class Natural_Multiset_Arranger(Eset):
808
808
  arrangement #0 agree with Natural_Arranger's ordering when every
809
809
  capacity is 1, and with Natural_Multiset_Permutator's ordering
810
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.
811
834
  """
812
835
 
813
836
  def __init__(self, *args, **kwargs):
@@ -188,8 +188,19 @@ def _inclusion_exclusion_combination_count(
188
188
  return total_answer
189
189
 
190
190
 
191
- def multiset_combination_count(multiplicities: tuple[int, ...], k: int) -> int:
192
- memo: dict[tuple[tuple[int, ...], int], int] = {}
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
193
204
 
194
205
  def count(rem: tuple[int, ...], remaining_k: int) -> int:
195
206
  key = (rem, remaining_k)
@@ -254,7 +265,8 @@ def multiset_combination_count(multiplicities: tuple[int, ...], k: int) -> int:
254
265
  def get_multiset_combination(
255
266
  val: int, multiplicities: tuple[int, ...], k: int
256
267
  ) -> tuple[int] | None:
257
- total = multiset_combination_count(multiplicities, k)
268
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
269
+ total = multiset_combination_count(multiplicities, k, memo)
258
270
  if val >= total or val < 0:
259
271
  return None
260
272
 
@@ -284,7 +296,7 @@ def get_multiset_combination(
284
296
  retlist: list[int],
285
297
  t: int,
286
298
  ) -> list[int]:
287
- block = multiset_combination_count(classes[1:], remaining_k - t)
299
+ block = multiset_combination_count(classes[1:], remaining_k - t, memo)
288
300
  if resval < block:
289
301
  return place(
290
302
  resval,
@@ -310,6 +322,8 @@ def get_multiset_combination_number(
310
322
  ):
311
323
  return None
312
324
 
325
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
326
+
313
327
  def rank(
314
328
  classes: tuple[int, ...], remaining_k: int, class_id: int, seq: list[int]
315
329
  ) -> int:
@@ -330,7 +344,7 @@ def get_multiset_combination_number(
330
344
  t: int,
331
345
  actual: int,
332
346
  ) -> int:
333
- block = multiset_combination_count(classes[1:], remaining_k - t)
347
+ block = multiset_combination_count(classes[1:], remaining_k - t, memo)
334
348
  if t == actual:
335
349
  return rank(classes[1:], remaining_k - t, class_id + 1, seq[t:])
336
350
  return block + locate(classes, remaining_k, class_id, seq, t - 1, actual)
@@ -438,8 +452,18 @@ def get_arrangement_number(arrangement: tuple[int], n: int) -> int | None:
438
452
  return get_number(list(arrangement), r)
439
453
 
440
454
 
441
- def multiset_arrangement_count(multiplicities: tuple[int, ...], r: int) -> int:
442
- 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
443
467
 
444
468
  def g(rem: tuple[int, ...], remaining_r: int) -> int:
445
469
  key = (rem, remaining_r)
@@ -482,7 +506,8 @@ def multiset_arrangement_count(multiplicities: tuple[int, ...], r: int) -> int:
482
506
  def get_multiset_arrangement(
483
507
  val: int, multiplicities: tuple[int, ...], r: int
484
508
  ) -> tuple[int] | None:
485
- total = multiset_arrangement_count(multiplicities, r)
509
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
510
+ total = multiset_arrangement_count(multiplicities, r, memo)
486
511
  if val >= total or val < 0:
487
512
  return None
488
513
 
@@ -508,7 +533,7 @@ def get_multiset_arrangement(
508
533
  new_mult = list(remaining_mult)
509
534
  new_mult[label] -= 1
510
535
  rest_mult = tuple(new_mult)
511
- block = multiset_arrangement_count(rest_mult, remaining_r - 1)
536
+ block = multiset_arrangement_count(rest_mult, remaining_r - 1, memo)
512
537
  if resval < block:
513
538
  return place(resval, rest_mult, remaining_r - 1, retlist + [label])
514
539
  return try_candidate(
@@ -529,6 +554,8 @@ def get_multiset_arrangement_number(
529
554
  ):
530
555
  return None
531
556
 
557
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
558
+
532
559
  def rank(remaining_mult: tuple[int, ...], remaining_r: int, seq: list[int]) -> int:
533
560
  if not seq:
534
561
  return 0
@@ -545,7 +572,7 @@ def get_multiset_arrangement_number(
545
572
  new_mult = list(remaining_mult)
546
573
  new_mult[label] -= 1
547
574
  rest_mult = tuple(new_mult)
548
- block = multiset_arrangement_count(rest_mult, remaining_r - 1)
575
+ block = multiset_arrangement_count(rest_mult, remaining_r - 1, memo)
549
576
  if label == seq[0]:
550
577
  return rank(rest_mult, remaining_r - 1, seq[1:])
551
578
  return block + locate(remaining_mult, remaining_r, seq, candidates[1:])
@@ -592,8 +619,10 @@ def get_multiset_subset(val: int, multiplicities: tuple[int, ...]) -> tuple[int]
592
619
  if val >= total or val < 0:
593
620
  return None
594
621
 
622
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
623
+
595
624
  def try_size(resval: int, k: int) -> tuple[int]:
596
- block = multiset_combination_count(multiplicities, k)
625
+ block = multiset_combination_count(multiplicities, k, memo)
597
626
  if resval < block:
598
627
  return get_multiset_combination(resval, multiplicities, k)
599
628
  return try_size(resval - block, k + 1)
@@ -608,10 +637,14 @@ def get_multiset_subset_number(
608
637
  if combo_rank is None:
609
638
  return None
610
639
 
640
+ memo: dict[tuple[tuple[int, ...], int], int] = {}
641
+
611
642
  def offset(size: int) -> int:
612
643
  if size == 0:
613
644
  return 0
614
- return multiset_combination_count(multiplicities, size - 1) + offset(size - 1)
645
+ return multiset_combination_count(multiplicities, size - 1, memo) + offset(
646
+ size - 1
647
+ )
615
648
 
616
649
  return offset(len(subset)) + combo_rank
617
650
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "esets-lib"
7
- version = "0.3.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
File without changes
File without changes
File without changes