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.
- {esets_lib-0.3.0 → esets_lib-0.3.2}/PKG-INFO +5 -1
- {esets_lib-0.3.0 → esets_lib-0.3.2}/README.md +4 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/esets/cesets.py +23 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/esets/ecombinatorics.py +45 -12
- {esets_lib-0.3.0 → esets_lib-0.3.2}/pyproject.toml +1 -1
- {esets_lib-0.3.0 → esets_lib-0.3.2}/.gitignore +0 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/LICENSE +0 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/esets/__init__.py +0 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/esets/besets.py +0 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/esets/eset.py +0 -0
- {esets_lib-0.3.0 → esets_lib-0.3.2}/esets/numeric.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: esets-lib
|
|
3
|
-
Version: 0.3.
|
|
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(
|
|
192
|
-
|
|
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
|
-
|
|
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(
|
|
442
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|