esets-lib 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.
esets/cesets.py ADDED
@@ -0,0 +1,1637 @@
1
+ from .eset import Eset, EABCMixinFactory
2
+ from . import ecombinatorics as ecomb
3
+ from math import factorial, comb, perm
4
+ from collections import Counter
5
+
6
+
7
+ class Natural_Permutator(Eset):
8
+ """A basic eset that handles permutations without repetition"""
9
+
10
+ def __init__(self, *args, **kwargs):
11
+ if 'xtra_params' in kwargs:
12
+ if len(kwargs['xtra_params']) != 0:
13
+ self.VALUE = kwargs['xtra_params'][0]
14
+ super().__init__(*args, **kwargs)
15
+ elif len(args) == 1:
16
+ if not isinstance(args[0], int) or args[0] < 0:
17
+ raise ValueError('Need a non-negative integer to initialize')
18
+ self.VALUE = args[0]
19
+ super().__init__(xtra_params=(self.VALUE,))
20
+ else:
21
+ raise ValueError('Need a non-negative integer to initialize')
22
+
23
+ def direct_function(self, i):
24
+ return ecomb.get_permutation(i, self.VALUE)
25
+
26
+ def inverse_function(self, val):
27
+ return ecomb.get_permutation_number(val)
28
+
29
+ def stop_init(self):
30
+ return factorial(self.VALUE)
31
+
32
+ def contains(self, val):
33
+ if not isinstance(val, tuple):
34
+ return False
35
+
36
+ restupl = tuple(range(self.VALUE))
37
+ if tuple(sorted(val)) != restupl:
38
+ return False
39
+
40
+ return self.slice_contains(val)
41
+
42
+
43
+ Distinct_PermutatorABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Permutator(1))
44
+
45
+
46
+ class Distinct_Permutator(Distinct_PermutatorABCMixin):
47
+ """An eset of every permutation of a finite sequence of unique
48
+ elements, built via EABCMixinFactory on top of
49
+ Natural_Permutator: Natural_Permutator enumerates the
50
+ positions, this class only translates between positions and the
51
+ elements it was given. The sequence as given is permutation #0.
52
+ An empty sequence is valid too and yields the single trivial
53
+ permutation, the empty tuple.
54
+
55
+ Note: element/position lookups go through list.index(), which is
56
+ O(n) per call. Fine while n stays small (n! grows fast enough that
57
+ it usually does), but if this becomes a bottleneck, building an
58
+ elements -> position dict once in __init__ would make
59
+ eset_obj_val/inverse_function/contains_mixin_check O(1) instead.
60
+
61
+ """
62
+
63
+ def __init__(self, *args, **kwargs):
64
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
65
+ eset_obj, self.elements = kwargs['xtra_params']
66
+ super().__init__(xtra_params=(eset_obj,))
67
+ elif len(args) == 1:
68
+ elements = tuple(args[0])
69
+ if len(set(elements)) != len(elements):
70
+ raise ValueError('Elements must be unique')
71
+ self.elements = elements
72
+ super().__init__(xtra_params=(Natural_Permutator(len(elements)),))
73
+ else:
74
+ raise ValueError(
75
+ 'Need a finite sequence (list, tuple, or string) of unique elements'
76
+ )
77
+
78
+ def init_check(self):
79
+ return True
80
+
81
+ def _pos_to_elems(self, pos_tpl):
82
+ return tuple(self.elements[p] for p in pos_tpl)
83
+
84
+ def _elems_to_pos(self, val):
85
+ return tuple(self.elements.index(v) for v in val)
86
+
87
+ def direct_function(self, i):
88
+ return self._pos_to_elems(self.eset_obj[i])
89
+
90
+ def inverse_function(self, val):
91
+ return self.eset_obj.index(self._elems_to_pos(val))
92
+
93
+ def eset_obj_val(self, val):
94
+ """The tuple of positions (Natural_Permutator's own
95
+ vocabulary) corresponding to a tuple of elements."""
96
+ return self._elems_to_pos(val)
97
+
98
+ def get_mix_val(self, val):
99
+ """The tuple of elements corresponding to a
100
+ Natural_Permutator tuple of positions."""
101
+ return self._pos_to_elems(val)
102
+
103
+ def contains_mixin_check(self, val):
104
+ if not isinstance(val, tuple) or len(val) != len(self.elements):
105
+ return False
106
+ return all(v in self.elements for v in val)
107
+
108
+ def __getitem__(self, key):
109
+ """Delegating to eset_obj like EMixinABC, but also carrying
110
+ self.elements along when slicing."""
111
+ if isinstance(key, slice):
112
+ return type(self)(xtra_params=(self.eset_obj[key], self.elements))
113
+ return super().__getitem__(key)
114
+
115
+
116
+ class Natural_Multiset_Permutator(Eset):
117
+ """A basic eset that handles permutations with repetition: the
118
+ distinguishable arrangements of a multiset. Parametrized either by
119
+ the original sequence expressed as canonical labels (small
120
+ integers 0..k-1 for the k distinct classes present, repeats
121
+ allowed), or by a multiplicities tuple, one positive integer count
122
+ per class, optionally followed by a group_order index (default 0)
123
+ selecting which arrangement of the classes themselves (via
124
+ Natural_Permutator) to expand: multiplicities (2, 3, 1) with the
125
+ default group_order expands to labels (0, 0, 1, 1, 1, 2).
126
+
127
+ The two forms never collide: a valid canonical labels tuple always
128
+ contains 0 (its distinct values are exactly 0..k-1), while a valid
129
+ multiplicities tuple never does (its entries are strictly
130
+ positive), so a single argument is unambiguous either way.
131
+ """
132
+
133
+ def __init__(self, *args, **kwargs):
134
+ if 'xtra_params' in kwargs:
135
+ if len(kwargs['xtra_params']) != 0:
136
+ self.LABELS = kwargs['xtra_params'][0]
137
+ super().__init__(*args, **kwargs)
138
+ elif len(args) in (1, 2):
139
+ first = tuple(args[0])
140
+ if len(args) == 1 and ecomb.is_canonical_labels(first):
141
+ self.LABELS = first
142
+ else:
143
+ multiplicities = first
144
+ group_order = args[1] if len(args) == 2 else 0
145
+ for count in multiplicities:
146
+ if not isinstance(count, int) or count <= 0:
147
+ raise ValueError('Multiplicities must be positive integers')
148
+ group_perm = Natural_Permutator(len(multiplicities))[group_order]
149
+ self.LABELS = tuple(
150
+ class_id
151
+ for class_id in group_perm
152
+ for _ in range(multiplicities[class_id])
153
+ )
154
+ super().__init__(xtra_params=(self.LABELS,))
155
+ else:
156
+ raise ValueError(
157
+ 'Need a tuple of canonical labels: 0..k-1, repeats allowed, or'
158
+ ' a multiplicities tuple optionally followed by a group_order'
159
+ ' index'
160
+ )
161
+
162
+ def direct_function(self, i):
163
+ return ecomb.get_multiset_permutation(i, self.LABELS)
164
+
165
+ def inverse_function(self, val):
166
+ return ecomb.get_multiset_permutation_number(val, self.LABELS)
167
+
168
+ def stop_init(self):
169
+ return ecomb.multinomial(Counter(self.LABELS))
170
+
171
+ def contains(self, val):
172
+ if not isinstance(val, tuple):
173
+ return False
174
+
175
+ if Counter(val) != Counter(self.LABELS):
176
+ return False
177
+
178
+ return self.slice_contains(val)
179
+
180
+
181
+ PermutatorABCMixin = EABCMixinFactory.create_ABC_mixin(
182
+ Natural_Multiset_Permutator((0,))
183
+ )
184
+
185
+
186
+ class Permutator(PermutatorABCMixin):
187
+ """An eset of every distinguishable permutation of a finite
188
+ sequence, repeated elements allowed, built via EABCMixinFactory on
189
+ top of Natural_Multiset_Permutator: Natural_Multiset_Permutator
190
+ enumerates the canonical labels, this class only translates
191
+ between labels and the elements it was given. The sequence as
192
+ given is permutation #0. An empty sequence is valid too and yields
193
+ the single trivial permutation, the empty tuple.
194
+
195
+ Unlike Distinct_Permutator, elements need not be unique: the count
196
+ is the multinomial coefficient n!/(m1!*m2!*...*mk!) rather than
197
+ n!, so repeated elements collapse indistinguishable rearrangements
198
+ into a single entry instead of counting them separately.
199
+
200
+ An optional second argument, alphabet, fixes the canonical class
201
+ order instead of deriving it from first appearance in elements.
202
+ alphabet must contain every distinct value present in elements
203
+ (it may also contain extra, unused values); anything in elements
204
+ missing from alphabet raises a ValueError. Supplying an alphabet
205
+ changes what permutation #0 means: instead of "elements exactly as
206
+ given", it becomes "elements sorted by alphabet priority", which
207
+ is what makes the resulting enumeration depend only on the
208
+ alphabet and the multiset, not on the arrangement elements
209
+ happened to be given in.
210
+
211
+ elements may also be a dict (a Counter, typically): a histogram
212
+ mapping each distinct value to its multiplicity, walked in the
213
+ dict's own iteration order in place of "first appearance in a
214
+ sequence". Every value must be a positive integer; anything else
215
+ raises a ValueError. Everything downstream, alphabet included,
216
+ works the same either way, since the histogram is expanded into a
217
+ plain tuple before any of that logic runs.
218
+ """
219
+
220
+ def __init__(self, *args, **kwargs):
221
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
222
+ eset_obj, self.elements, self.classes = kwargs['xtra_params']
223
+ super().__init__(xtra_params=(eset_obj,))
224
+ elif len(args) == 1:
225
+ elements = self._expand_elements(args[0])
226
+ self.elements = elements
227
+ self.classes = list(dict.fromkeys(elements))
228
+ labels = tuple(self.classes.index(e) for e in elements)
229
+ super().__init__(xtra_params=(Natural_Multiset_Permutator(labels),))
230
+ elif len(args) == 2:
231
+ elements = self._expand_elements(args[0])
232
+ alphabet = tuple(args[1])
233
+ if len(set(alphabet)) != len(alphabet):
234
+ raise ValueError('Alphabet entries must be unique')
235
+ missing = set(elements) - set(alphabet)
236
+ if missing:
237
+ raise ValueError(f'Elements not present in alphabet: {sorted(missing)}')
238
+ self.elements = elements
239
+ present = set(elements)
240
+ self.classes = [a for a in alphabet if a in present]
241
+ labels = tuple(sorted(self.classes.index(e) for e in elements))
242
+ super().__init__(xtra_params=(Natural_Multiset_Permutator(labels),))
243
+ else:
244
+ raise ValueError(
245
+ 'Need a finite sequence (list, tuple, string, or Counter),'
246
+ ' optionally followed by an alphabet'
247
+ )
248
+
249
+ def init_check(self):
250
+ return True
251
+
252
+ @staticmethod
253
+ def _expand_elements(source):
254
+ if isinstance(source, dict):
255
+ for count in source.values():
256
+ if not isinstance(count, int) or count <= 0:
257
+ raise ValueError('Counter/dict values must be positive integers')
258
+ return tuple(key for key, count in source.items() for _ in range(count))
259
+ return tuple(source)
260
+
261
+ def _labels_to_elems(self, label_tpl):
262
+ return tuple(self.classes[l] for l in label_tpl)
263
+
264
+ def _elems_to_labels(self, val):
265
+ return tuple(self.classes.index(v) for v in val)
266
+
267
+ def direct_function(self, i):
268
+ return self._labels_to_elems(self.eset_obj[i])
269
+
270
+ def inverse_function(self, val):
271
+ return self.eset_obj.index(self._elems_to_labels(val))
272
+
273
+ def eset_obj_val(self, val):
274
+ """The tuple of canonical labels (Natural_Multiset_Permutator's
275
+ own vocabulary) corresponding to a tuple of elements."""
276
+ return self._elems_to_labels(val)
277
+
278
+ def get_mix_val(self, val):
279
+ """The tuple of elements corresponding to a
280
+ Natural_Multiset_Permutator tuple of canonical labels."""
281
+ return self._labels_to_elems(val)
282
+
283
+ def contains_mixin_check(self, val):
284
+ if not isinstance(val, tuple) or len(val) != len(self.elements):
285
+ return False
286
+ return Counter(val) == Counter(self.elements)
287
+
288
+ def __getitem__(self, key):
289
+ """Delegating to eset_obj like EMixinABC, but also carrying
290
+ self.elements/self.classes along when slicing."""
291
+ if isinstance(key, slice):
292
+ return type(self)(
293
+ xtra_params=(self.eset_obj[key], self.elements, self.classes)
294
+ )
295
+ return super().__getitem__(key)
296
+
297
+
298
+ class Natural_Combinator(Eset):
299
+ """A basic eset that handles combinations without repetition: the
300
+ k-subsets of range(n), each represented as an ascending tuple of k
301
+ distinct indices in 0..n-1. Combination #0 is (0, 1, ..., k-1), the
302
+ lexicographically smallest; the count is math.comb(n, k).
303
+
304
+ Like every other Natural_* class in this module, value identity
305
+ matters here: only the canonical ascending tuple is a member, a
306
+ differently-ordered tuple of the same indices is not. Order
307
+ independence (any arrangement of a valid choice counts the same)
308
+ is a higher-level concern, handled by Distinct_Combinator and
309
+ Combinator instead, since they're the ones translating to actual,
310
+ order-irrelevant elements.
311
+
312
+ Note: like the reversal caveat documented for Permutator, the last
313
+ combination is not generally an order-reversal or complement of
314
+ the first. For n=4, k=2, index 2 is (0, 3) and index 3 is (1, 2):
315
+ complementary under i -> n-1-i, but not reverse-index counterparts
316
+ of one another (that pairing only holds towards the ends).
317
+ """
318
+
319
+ def __init__(self, *args, **kwargs):
320
+ if 'xtra_params' in kwargs:
321
+ if len(kwargs['xtra_params']) != 0:
322
+ self.N, self.K = kwargs['xtra_params']
323
+ super().__init__(*args, **kwargs)
324
+ elif len(args) == 2:
325
+ n, k = args
326
+ if not isinstance(n, int) or n < 0 or not isinstance(k, int) or k < 0:
327
+ raise ValueError('Need two non-negative integers: n and k')
328
+ self.N, self.K = n, k
329
+ super().__init__(xtra_params=(self.N, self.K))
330
+ else:
331
+ raise ValueError('Need two non-negative integers: n and k')
332
+
333
+ def direct_function(self, i):
334
+ return ecomb.get_combination(i, self.N, self.K)
335
+
336
+ def inverse_function(self, val):
337
+ return ecomb.get_combination_number(tuple(sorted(val)), self.N)
338
+
339
+ def stop_init(self):
340
+ return comb(self.N, self.K)
341
+
342
+ def contains(self, val):
343
+ if not isinstance(val, tuple) or len(val) != self.K:
344
+ return False
345
+ if len(set(val)) != len(val):
346
+ return False
347
+ if any(v < 0 or v >= self.N for v in val):
348
+ return False
349
+ return self.slice_contains(val)
350
+
351
+
352
+ Distinct_CombinatorABCMixin = EABCMixinFactory.create_ABC_mixin(
353
+ Natural_Combinator(1, 0)
354
+ )
355
+
356
+
357
+ class Distinct_Combinator(Distinct_CombinatorABCMixin):
358
+ """An eset of every k-combination of a finite sequence of unique
359
+ elements, built via EABCMixinFactory on top of Natural_Combinator:
360
+ Natural_Combinator enumerates the chosen positions, this class
361
+ only translates between positions and the elements it was given.
362
+ Combination #0 is the first k elements, in the order given.
363
+
364
+ Like Natural_Combinator, a combination is unordered: contains()
365
+ and index() accept elements in any order, ranking/comparing them
366
+ by the multiset of elements chosen rather than the exact tuple
367
+ given. direct_function always returns elements in the order they
368
+ appear in the original sequence.
369
+ """
370
+
371
+ def __init__(self, *args, **kwargs):
372
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
373
+ eset_obj, self.elements, self.k = kwargs['xtra_params']
374
+ super().__init__(xtra_params=(eset_obj,))
375
+ elif len(args) == 2:
376
+ elements = tuple(args[0])
377
+ k = args[1]
378
+ if len(set(elements)) != len(elements):
379
+ raise ValueError('Elements must be unique')
380
+ if not isinstance(k, int) or k < 0:
381
+ raise ValueError('Need a non-negative integer k')
382
+ self.elements = elements
383
+ self.k = k
384
+ super().__init__(xtra_params=(Natural_Combinator(len(elements), k),))
385
+ else:
386
+ raise ValueError(
387
+ 'Need a finite sequence (list, tuple, or string) of unique'
388
+ ' elements, and k'
389
+ )
390
+
391
+ def init_check(self):
392
+ return True
393
+
394
+ def _pos_to_elems(self, pos_tpl):
395
+ return tuple(self.elements[p] for p in pos_tpl)
396
+
397
+ def _elems_to_pos(self, val):
398
+ return tuple(sorted(self.elements.index(v) for v in val))
399
+
400
+ def direct_function(self, i):
401
+ return self._pos_to_elems(self.eset_obj[i])
402
+
403
+ def inverse_function(self, val):
404
+ return self.eset_obj.index(self._elems_to_pos(val))
405
+
406
+ def eset_obj_val(self, val):
407
+ return self._elems_to_pos(val)
408
+
409
+ def get_mix_val(self, val):
410
+ return self._pos_to_elems(val)
411
+
412
+ def id_contains(self, val):
413
+ if val == val and Counter(val) != Counter(
414
+ self.direct_function(self.inverse_function(val))
415
+ ):
416
+ return False
417
+
418
+ def contains_mixin_check(self, val):
419
+ if not isinstance(val, tuple) or len(val) != self.k:
420
+ return False
421
+ return all(v in self.elements for v in val) and len(set(val)) == len(val)
422
+
423
+ def __getitem__(self, key):
424
+ if isinstance(key, slice):
425
+ return type(self)(xtra_params=(self.eset_obj[key], self.elements, self.k))
426
+ return super().__getitem__(key)
427
+
428
+
429
+ class Natural_Multiset_Combinator(Eset):
430
+ """A basic eset that handles combinations with repetition: the
431
+ ways of choosing k elements (canonical labels 0..m-1) from a
432
+ multiset with per-class capacities, given as a multiplicities
433
+ tuple (multiplicities[j] = how many of class j are available).
434
+ Each combination is a non-decreasing tuple of length k. Combination
435
+ #0 greedily takes as many of the smallest class as possible, then
436
+ the next, and so on; the count is multiset_combination_count(
437
+ multiplicities, k), computed with the same memoized recursion (and
438
+ the capacities-can't-bind shortcut) mset uses for multiset
439
+ combination counting.
440
+
441
+ Like Natural_Combinator, value identity matters here: only the
442
+ canonical non-decreasing tuple is a member. Order independence is
443
+ handled at the Combinator level instead.
444
+ """
445
+
446
+ def __init__(self, *args, **kwargs):
447
+ if 'xtra_params' in kwargs:
448
+ if len(kwargs['xtra_params']) != 0:
449
+ self.MULTIPLICITIES, self.K = kwargs['xtra_params']
450
+ super().__init__(*args, **kwargs)
451
+ elif len(args) == 2:
452
+ multiplicities = tuple(args[0])
453
+ k = args[1]
454
+ for count in multiplicities:
455
+ if not isinstance(count, int) or count <= 0:
456
+ raise ValueError('Multiplicities must be positive integers')
457
+ if not isinstance(k, int) or k < 0:
458
+ raise ValueError('Need a non-negative integer k')
459
+ self.MULTIPLICITIES, self.K = multiplicities, k
460
+ super().__init__(xtra_params=(self.MULTIPLICITIES, self.K))
461
+ else:
462
+ raise ValueError('Need a multiplicities tuple of positive integers, and k')
463
+
464
+ def direct_function(self, i):
465
+ return ecomb.get_multiset_combination(i, self.MULTIPLICITIES, self.K)
466
+
467
+ def inverse_function(self, val):
468
+ return ecomb.get_multiset_combination_number(
469
+ tuple(sorted(val)), self.MULTIPLICITIES
470
+ )
471
+
472
+ def stop_init(self):
473
+ return ecomb.multiset_combination_count(self.MULTIPLICITIES, self.K)
474
+
475
+ def contains(self, val):
476
+ if not isinstance(val, tuple) or len(val) != self.K:
477
+ return False
478
+ counts = Counter(val)
479
+ if any(
480
+ c < 0 or c >= len(self.MULTIPLICITIES) or counts[c] > self.MULTIPLICITIES[c]
481
+ for c in counts
482
+ ):
483
+ return False
484
+ return self.slice_contains(val)
485
+
486
+
487
+ CombinatorABCMixin = EABCMixinFactory.create_ABC_mixin(
488
+ Natural_Multiset_Combinator((1,), 0)
489
+ )
490
+
491
+
492
+ class Combinator(CombinatorABCMixin):
493
+ """An eset of every k-combination of a finite sequence, repeated
494
+ elements allowed, built via EABCMixinFactory on top of
495
+ Natural_Multiset_Combinator: Natural_Multiset_Combinator enumerates
496
+ the canonical label combinations, this class only translates
497
+ between labels and the elements it was given. Combination #0
498
+ greedily takes as many of the first-appearing element as possible,
499
+ then the next, and so on (or, with an alphabet, by alphabet
500
+ priority instead of first appearance -- see below).
501
+
502
+ Unlike Distinct_Combinator, elements need not be unique: choosing
503
+ is capped per distinct value at how many of it are actually
504
+ present, rather than at 1.
505
+
506
+ A combination is inherently unordered (unlike a Permutator's
507
+ arrangement): contains() and index() accept any ordering of a
508
+ valid k-element sub-multiset of elements, ranking/comparing them
509
+ by content rather than by the exact tuple given. direct_function
510
+ always returns one canonical order: elements grouped by class,
511
+ classes in canonical order.
512
+
513
+ An optional alphabet argument, exactly as for Permutator, fixes
514
+ the canonical class order instead of deriving it from first
515
+ appearance in elements; it must cover every distinct value present
516
+ in elements (extra, unused values are fine), and anything in
517
+ elements missing from alphabet raises a ValueError.
518
+
519
+ elements may also be a dict (a Counter, typically): a histogram
520
+ mapping each distinct value to its available count, walked in the
521
+ dict's own iteration order in place of "first appearance in a
522
+ sequence" -- the same convention Permutator uses.
523
+ """
524
+
525
+ def __init__(self, *args, **kwargs):
526
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
527
+ eset_obj, self.elements, self.k, self.classes = kwargs['xtra_params']
528
+ super().__init__(xtra_params=(eset_obj,))
529
+ elif len(args) in (2, 3):
530
+ elements = Permutator._expand_elements(args[0])
531
+ k = args[1]
532
+ if not isinstance(k, int) or k < 0:
533
+ raise ValueError('Need a non-negative integer k')
534
+ self.elements = elements
535
+ self.k = k
536
+ if len(args) == 3:
537
+ alphabet = tuple(args[2])
538
+ if len(set(alphabet)) != len(alphabet):
539
+ raise ValueError('Alphabet entries must be unique')
540
+ missing = set(elements) - set(alphabet)
541
+ if missing:
542
+ raise ValueError(
543
+ f'Elements not present in alphabet: {sorted(missing)}'
544
+ )
545
+ present = set(elements)
546
+ self.classes = [a for a in alphabet if a in present]
547
+ else:
548
+ self.classes = list(dict.fromkeys(elements))
549
+ multiplicities = tuple(Counter(elements)[c] for c in self.classes)
550
+ super().__init__(
551
+ xtra_params=(Natural_Multiset_Combinator(multiplicities, k),)
552
+ )
553
+ else:
554
+ raise ValueError(
555
+ 'Need a finite sequence (list, tuple, string, or Counter), k,'
556
+ ' optionally followed by an alphabet'
557
+ )
558
+
559
+ def init_check(self):
560
+ return True
561
+
562
+ def _labels_to_elems(self, label_tpl):
563
+ return tuple(self.classes[l] for l in label_tpl)
564
+
565
+ def _elems_to_labels(self, val):
566
+ return tuple(sorted(self.classes.index(v) for v in val))
567
+
568
+ def direct_function(self, i):
569
+ return self._labels_to_elems(self.eset_obj[i])
570
+
571
+ def inverse_function(self, val):
572
+ return self.eset_obj.index(self._elems_to_labels(val))
573
+
574
+ def eset_obj_val(self, val):
575
+ return self._elems_to_labels(val)
576
+
577
+ def get_mix_val(self, val):
578
+ return self._labels_to_elems(val)
579
+
580
+ def id_contains(self, val):
581
+ if val == val and Counter(val) != Counter(
582
+ self.direct_function(self.inverse_function(val))
583
+ ):
584
+ return False
585
+
586
+ def contains_mixin_check(self, val):
587
+ if not isinstance(val, tuple) or len(val) != self.k:
588
+ return False
589
+ val_counts = Counter(val)
590
+ elem_counts = Counter(self.elements)
591
+ return all(val_counts[v] <= elem_counts.get(v, 0) for v in val_counts)
592
+
593
+ def __getitem__(self, key):
594
+ if isinstance(key, slice):
595
+ return type(self)(
596
+ xtra_params=(self.eset_obj[key], self.elements, self.k, self.classes)
597
+ )
598
+ return super().__getitem__(key)
599
+
600
+
601
+ class Partitioner(Eset):
602
+ """A basic eset that handles integer partitions: every way of
603
+ writing a non-negative integer n as a sum of positive integers,
604
+ order disregarded. Each partition is represented as a
605
+ non-increasing tuple of positive integers summing to n, the
606
+ standard convention (4 = (3, 1), not (1, 3)).
607
+
608
+ Unlike Permutator/Combinator, there's no arbitrary elements domain
609
+ to translate to and from here: a partition's parts already are the
610
+ integers, so this is a single, self-contained class, no Natural_*
611
+ plus wrapper split.
612
+
613
+ Partition #0 is (1, 1, ..., 1), n copies of 1; the count grows as
614
+ the largest part is allowed to grow, ending at partition -1, the
615
+ single-part partition (n,). The count itself, p(n), has no
616
+ closed-form shortcut the way n! or comb(n, k) do; it's computed by
617
+ the classic memoized recursion partitions_count(n, m) = partitions
618
+ of n using parts <= m, splitting on whether a part of size m is
619
+ used at all.
620
+
621
+ There's no alphabet-equivalent for this class: an alphabet fixes
622
+ an ordering over an arbitrary elements domain, but a partition's
623
+ "elements" are already just sizes. The analogous higher-level
624
+ object, partitioning n actual distinguishable elements into
625
+ non-empty groups (Stirling numbers of the second kind / Bell
626
+ numbers), is a genuinely different combinatorial object and would
627
+ need its own class, not a wrapper on top of this one.
628
+ """
629
+
630
+ def __init__(self, *args, **kwargs):
631
+ if 'xtra_params' in kwargs:
632
+ if len(kwargs['xtra_params']) != 0:
633
+ self.N = kwargs['xtra_params'][0]
634
+ super().__init__(*args, **kwargs)
635
+ elif len(args) == 1:
636
+ if not isinstance(args[0], int) or args[0] < 0:
637
+ raise ValueError('Need a non-negative integer to initialize')
638
+ self.N = args[0]
639
+ super().__init__(xtra_params=(self.N,))
640
+ else:
641
+ raise ValueError('Need a non-negative integer to initialize')
642
+
643
+ def direct_function(self, i):
644
+ return ecomb.get_partition(i, self.N)
645
+
646
+ def inverse_function(self, val):
647
+ return ecomb.get_partition_number(val, self.N)
648
+
649
+ def stop_init(self):
650
+ return ecomb.partitions_count(self.N, self.N)
651
+
652
+ def contains(self, val):
653
+ if not isinstance(val, tuple):
654
+ return False
655
+ if any(not isinstance(p, int) or p <= 0 for p in val):
656
+ return False
657
+ if sum(val) != self.N:
658
+ return False
659
+ if list(val) != sorted(val, reverse=True):
660
+ return False
661
+ return self.slice_contains(val)
662
+
663
+
664
+ class Natural_Arranger(Eset):
665
+ """A basic eset that handles arrangements without repetition: the
666
+ ordered ways of choosing r distinct indices from range(n), i.e.
667
+ nPr (n! / (n-r)!), sometimes called partial permutations or
668
+ r-permutations of n. Each arrangement is a tuple of r distinct
669
+ indices in 0..n-1; unlike Natural_Combinator, order matters here,
670
+ so every ordering of a given r-subset is its own distinct member,
671
+ the same "value identity matters" convention as Natural_Permutator.
672
+
673
+ Arrangement #0 is (0, 1, ..., r-1); ranking uses the same
674
+ factorial-number-system trick as Natural_Permutator, just stopped
675
+ after r digits instead of continuing through all n, so this class
676
+ reduces to Natural_Permutator exactly when r == n.
677
+ """
678
+
679
+ def __init__(self, *args, **kwargs):
680
+ if 'xtra_params' in kwargs:
681
+ if len(kwargs['xtra_params']) != 0:
682
+ self.N, self.R = kwargs['xtra_params']
683
+ super().__init__(*args, **kwargs)
684
+ elif len(args) == 2:
685
+ n, r = args
686
+ if not isinstance(n, int) or n < 0 or not isinstance(r, int) or r < 0:
687
+ raise ValueError('Need two non-negative integers: n and r')
688
+ self.N, self.R = n, r
689
+ super().__init__(xtra_params=(self.N, self.R))
690
+ else:
691
+ raise ValueError('Need two non-negative integers: n and r')
692
+
693
+ def direct_function(self, i):
694
+ return ecomb.get_arrangement(i, self.N, self.R)
695
+
696
+ def inverse_function(self, val):
697
+ return ecomb.get_arrangement_number(val, self.N)
698
+
699
+ def stop_init(self):
700
+ return perm(self.N, self.R)
701
+
702
+ def contains(self, val):
703
+ if not isinstance(val, tuple) or len(val) != self.R:
704
+ return False
705
+ if len(set(val)) != len(val):
706
+ return False
707
+ if any(v < 0 or v >= self.N for v in val):
708
+ return False
709
+ return self.slice_contains(val)
710
+
711
+
712
+ Distinct_ArrangerABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Arranger(1, 0))
713
+
714
+
715
+ class Distinct_Arranger(Distinct_ArrangerABCMixin):
716
+ """An eset of every way to choose and arrange r elements from a
717
+ finite sequence of unique elements, built via EABCMixinFactory on
718
+ top of Natural_Arranger. Order matters, same convention as
719
+ Distinct_Permutator: a differently-ordered selection of the same
720
+ elements is a different member, not the same one.
721
+ """
722
+
723
+ def __init__(self, *args, **kwargs):
724
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
725
+ eset_obj, self.elements, self.r = kwargs['xtra_params']
726
+ super().__init__(xtra_params=(eset_obj,))
727
+ elif len(args) == 2:
728
+ elements = tuple(args[0])
729
+ r = args[1]
730
+ if len(set(elements)) != len(elements):
731
+ raise ValueError('Elements must be unique')
732
+ if not isinstance(r, int) or r < 0:
733
+ raise ValueError('Need a non-negative integer r')
734
+ self.elements = elements
735
+ self.r = r
736
+ super().__init__(xtra_params=(Natural_Arranger(len(elements), r),))
737
+ else:
738
+ raise ValueError(
739
+ 'Need a finite sequence (list, tuple, or string) of unique'
740
+ ' elements, and r'
741
+ )
742
+
743
+ def init_check(self):
744
+ return True
745
+
746
+ def _pos_to_elems(self, pos_tpl):
747
+ return tuple(self.elements[p] for p in pos_tpl)
748
+
749
+ def _elems_to_pos(self, val):
750
+ return tuple(self.elements.index(v) for v in val)
751
+
752
+ def direct_function(self, i):
753
+ return self._pos_to_elems(self.eset_obj[i])
754
+
755
+ def inverse_function(self, val):
756
+ return self.eset_obj.index(self._elems_to_pos(val))
757
+
758
+ def eset_obj_val(self, val):
759
+ return self._elems_to_pos(val)
760
+
761
+ def get_mix_val(self, val):
762
+ return self._pos_to_elems(val)
763
+
764
+ def contains_mixin_check(self, val):
765
+ if not isinstance(val, tuple) or len(val) != self.r:
766
+ return False
767
+ return all(v in self.elements for v in val) and len(set(val)) == len(val)
768
+
769
+ def __getitem__(self, key):
770
+ if isinstance(key, slice):
771
+ return type(self)(xtra_params=(self.eset_obj[key], self.elements, self.r))
772
+ return super().__getitem__(key)
773
+
774
+
775
+ class Natural_Multiset_Arranger(Eset):
776
+ """A basic eset that handles arrangements with repetition: the
777
+ ordered ways of choosing r elements (canonical labels 0..m-1) from
778
+ a multiset with per-class capacities. Each arrangement is a tuple
779
+ of length r; order matters, so unlike Natural_Multiset_Combinator,
780
+ every ordering of a given r-element sub-multiset is its own
781
+ distinct member.
782
+
783
+ The count has no simple closed form the way nPr or comb(n,k) do:
784
+ it's computed by multiset_arrangement_count(multiplicities, r):
785
+ multiset_arrangement_count(L, r) = sum_x comb(r, x) *
786
+ multiset_arrangement_count(L[1:], r - x), deciding how many of the
787
+ r (still unfilled) slots the current class takes (comb(r, x) ways
788
+ to pick which ones, unrelated to any capacity), then recursing on
789
+ the rest. This is also the total across every
790
+ Natural_Multiset_Combinator combination of size r of that
791
+ combination's own multinomial coefficient, just computed directly
792
+ rather than by enumerating combinations first.
793
+
794
+ Ranking/unranking, though, is done one output position at a time
795
+ (mirroring Natural_Multiset_Permutator's place/try_candidate
796
+ exactly, using multiset_arrangement_count as the block size
797
+ instead of multinomial), not class-by-class -- which is what makes
798
+ arrangement #0 agree with Natural_Arranger's ordering when every
799
+ capacity is 1, and with Natural_Multiset_Permutator's ordering
800
+ when r equals the full multiset size.
801
+ """
802
+
803
+ def __init__(self, *args, **kwargs):
804
+ if 'xtra_params' in kwargs:
805
+ if len(kwargs['xtra_params']) != 0:
806
+ self.MULTIPLICITIES, self.R = kwargs['xtra_params']
807
+ super().__init__(*args, **kwargs)
808
+ elif len(args) == 2:
809
+ multiplicities = tuple(args[0])
810
+ r = args[1]
811
+ for count in multiplicities:
812
+ if not isinstance(count, int) or count <= 0:
813
+ raise ValueError('Multiplicities must be positive integers')
814
+ if not isinstance(r, int) or r < 0:
815
+ raise ValueError('Need a non-negative integer r')
816
+ self.MULTIPLICITIES, self.R = multiplicities, r
817
+ super().__init__(xtra_params=(self.MULTIPLICITIES, self.R))
818
+ else:
819
+ raise ValueError('Need a multiplicities tuple of positive integers, and r')
820
+
821
+ def direct_function(self, i):
822
+ return ecomb.get_multiset_arrangement(i, self.MULTIPLICITIES, self.R)
823
+
824
+ def inverse_function(self, val):
825
+ return ecomb.get_multiset_arrangement_number(val, self.MULTIPLICITIES)
826
+
827
+ def stop_init(self):
828
+ return ecomb.multiset_arrangement_count(self.MULTIPLICITIES, self.R)
829
+
830
+ def contains(self, val):
831
+ if not isinstance(val, tuple) or len(val) != self.R:
832
+ return False
833
+ counts = Counter(val)
834
+ if any(
835
+ c < 0 or c >= len(self.MULTIPLICITIES) or counts[c] > self.MULTIPLICITIES[c]
836
+ for c in counts
837
+ ):
838
+ return False
839
+ return self.slice_contains(val)
840
+
841
+
842
+ ArrangerABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Multiset_Arranger((1,), 0))
843
+
844
+
845
+ class Arranger(ArrangerABCMixin):
846
+ """An eset of every way to choose and arrange r elements from a
847
+ finite sequence, repeated elements allowed, built via
848
+ EABCMixinFactory on top of Natural_Multiset_Arranger. Order
849
+ matters here, same convention as Permutator: a differently-ordered
850
+ selection of the same content is a different member.
851
+
852
+ This is the object you'd get by taking a Combinator (choosing
853
+ which r elements, content only) and, for each of its results,
854
+ running a Permutator over that specific selection -- Arranger
855
+ computes the same eventual set directly (and lazily) instead of
856
+ going through that composition, since finding "which combination"
857
+ an index falls under by scanning them would defeat the point of
858
+ an eset.
859
+
860
+ An optional alphabet argument works exactly as it does for
861
+ Permutator and Combinator, fixing the canonical class order
862
+ instead of deriving it from first appearance in elements.
863
+
864
+ elements may also be a dict (a Counter, typically), exactly as for
865
+ Permutator and Combinator.
866
+ """
867
+
868
+ def __init__(self, *args, **kwargs):
869
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
870
+ eset_obj, self.elements, self.r, self.classes = kwargs['xtra_params']
871
+ super().__init__(xtra_params=(eset_obj,))
872
+ elif len(args) in (2, 3):
873
+ elements = Permutator._expand_elements(args[0])
874
+ r = args[1]
875
+ if not isinstance(r, int) or r < 0:
876
+ raise ValueError('Need a non-negative integer r')
877
+ self.elements = elements
878
+ self.r = r
879
+ if len(args) == 3:
880
+ alphabet = tuple(args[2])
881
+ if len(set(alphabet)) != len(alphabet):
882
+ raise ValueError('Alphabet entries must be unique')
883
+ missing = set(elements) - set(alphabet)
884
+ if missing:
885
+ raise ValueError(
886
+ f'Elements not present in alphabet: {sorted(missing)}'
887
+ )
888
+ present = set(elements)
889
+ self.classes = [a for a in alphabet if a in present]
890
+ else:
891
+ self.classes = list(dict.fromkeys(elements))
892
+ multiplicities = tuple(Counter(elements)[c] for c in self.classes)
893
+ super().__init__(
894
+ xtra_params=(Natural_Multiset_Arranger(multiplicities, r),)
895
+ )
896
+ else:
897
+ raise ValueError(
898
+ 'Need a finite sequence (list, tuple, string, or Counter), r,'
899
+ ' optionally followed by an alphabet'
900
+ )
901
+
902
+ def init_check(self):
903
+ return True
904
+
905
+ def _labels_to_elems(self, label_tpl):
906
+ return tuple(self.classes[l] for l in label_tpl)
907
+
908
+ def _elems_to_labels(self, val):
909
+ return tuple(self.classes.index(v) for v in val)
910
+
911
+ def direct_function(self, i):
912
+ return self._labels_to_elems(self.eset_obj[i])
913
+
914
+ def inverse_function(self, val):
915
+ return self.eset_obj.index(self._elems_to_labels(val))
916
+
917
+ def eset_obj_val(self, val):
918
+ return self._elems_to_labels(val)
919
+
920
+ def get_mix_val(self, val):
921
+ return self._labels_to_elems(val)
922
+
923
+ def contains_mixin_check(self, val):
924
+ if not isinstance(val, tuple) or len(val) != self.r:
925
+ return False
926
+ val_counts = Counter(val)
927
+ elem_counts = Counter(self.elements)
928
+ return all(val_counts[v] <= elem_counts.get(v, 0) for v in val_counts)
929
+
930
+ def __getitem__(self, key):
931
+ if isinstance(key, slice):
932
+ return type(self)(
933
+ xtra_params=(self.eset_obj[key], self.elements, self.r, self.classes)
934
+ )
935
+ return super().__getitem__(key)
936
+
937
+
938
+ class Natural_Powerset(Eset):
939
+ """A basic eset that handles the power set of range(n): every
940
+ subset, of every size from 0 to n, 2**n of them in total. Each
941
+ subset is a sorted tuple of distinct indices in 0..n-1.
942
+
943
+ Subsets are enumerated in graded order: every subset of size 0
944
+ first, then every subset of size 1, and so on up to size n, with
945
+ each size-k block internally ordered exactly as
946
+ Natural_Combinator(n, k) orders it (indeed, that's exactly what
947
+ this delegates to, using comb(n, k) to find which block an index
948
+ falls in). Subset #0 is always the empty tuple; the last is
949
+ always (0, 1, ..., n-1).
950
+
951
+ Like Natural_Combinator, value identity matters here: only the
952
+ canonical sorted tuple is a member. Order independence belongs to
953
+ Distinct_Powerset/Powerset instead.
954
+ """
955
+
956
+ def __init__(self, *args, **kwargs):
957
+ if 'xtra_params' in kwargs:
958
+ if len(kwargs['xtra_params']) != 0:
959
+ self.N = kwargs['xtra_params'][0]
960
+ super().__init__(*args, **kwargs)
961
+ elif len(args) == 1:
962
+ if not isinstance(args[0], int) or args[0] < 0:
963
+ raise ValueError('Need a non-negative integer to initialize')
964
+ self.N = args[0]
965
+ super().__init__(xtra_params=(self.N,))
966
+ else:
967
+ raise ValueError('Need a non-negative integer to initialize')
968
+
969
+ def direct_function(self, i):
970
+ return ecomb.get_subset(i, self.N)
971
+
972
+ def inverse_function(self, val):
973
+ return ecomb.get_subset_number(val, self.N)
974
+
975
+ def stop_init(self):
976
+ return 2**self.N
977
+
978
+ def contains(self, val):
979
+ if not isinstance(val, tuple):
980
+ return False
981
+ if len(set(val)) != len(val):
982
+ return False
983
+ if any(v < 0 or v >= self.N for v in val):
984
+ return False
985
+ if list(val) != sorted(val):
986
+ return False
987
+ return self.slice_contains(val)
988
+
989
+
990
+ Distinct_PowersetABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Powerset(0))
991
+
992
+
993
+ class Distinct_Powerset(Distinct_PowersetABCMixin):
994
+ """An eset of every subset of a finite sequence of unique
995
+ elements, of every size, built via EABCMixinFactory on top of
996
+ Natural_Powerset. A subset is unordered, so contains()/index()
997
+ accept any ordering of a valid subset, the same convention
998
+ Distinct_Combinator uses.
999
+ """
1000
+
1001
+ def __init__(self, *args, **kwargs):
1002
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
1003
+ eset_obj, self.elements = kwargs['xtra_params']
1004
+ super().__init__(xtra_params=(eset_obj,))
1005
+ elif len(args) == 1:
1006
+ elements = tuple(args[0])
1007
+ if len(set(elements)) != len(elements):
1008
+ raise ValueError('Elements must be unique')
1009
+ self.elements = elements
1010
+ super().__init__(xtra_params=(Natural_Powerset(len(elements)),))
1011
+ else:
1012
+ raise ValueError(
1013
+ 'Need a finite sequence (list, tuple, or string) of unique elements'
1014
+ )
1015
+
1016
+ def init_check(self):
1017
+ return True
1018
+
1019
+ def _pos_to_elems(self, pos_tpl):
1020
+ return tuple(self.elements[p] for p in pos_tpl)
1021
+
1022
+ def _elems_to_pos(self, val):
1023
+ return tuple(sorted(self.elements.index(v) for v in val))
1024
+
1025
+ def direct_function(self, i):
1026
+ return self._pos_to_elems(self.eset_obj[i])
1027
+
1028
+ def inverse_function(self, val):
1029
+ return self.eset_obj.index(self._elems_to_pos(val))
1030
+
1031
+ def eset_obj_val(self, val):
1032
+ return self._elems_to_pos(val)
1033
+
1034
+ def get_mix_val(self, val):
1035
+ return self._pos_to_elems(val)
1036
+
1037
+ def id_contains(self, val):
1038
+ if val == val and Counter(val) != Counter(
1039
+ self.direct_function(self.inverse_function(val))
1040
+ ):
1041
+ return False
1042
+
1043
+ def contains_mixin_check(self, val):
1044
+ if not isinstance(val, tuple):
1045
+ return False
1046
+ return all(v in self.elements for v in val) and len(set(val)) == len(val)
1047
+
1048
+ def __getitem__(self, key):
1049
+ if isinstance(key, slice):
1050
+ return type(self)(xtra_params=(self.eset_obj[key], self.elements))
1051
+ return super().__getitem__(key)
1052
+
1053
+
1054
+ class Natural_Multiset_Powerset(Eset):
1055
+ """A basic eset that handles the power set of a multiset with
1056
+ per-class capacities: every sub-multiset, of every size, given a
1057
+ multiplicities tuple. Each subset is a sorted tuple of canonical
1058
+ labels.
1059
+
1060
+ The count needs no recursion at all, unlike every other counting
1061
+ function in this module: each class independently contributes
1062
+ anywhere from 0 up to its own capacity, so it's a plain mixed-radix
1063
+ digit count, multiset_powerset_count(L) = product(c + 1 for c in
1064
+ L) -- Natural_Powerset is the special case where every capacity is
1065
+ 1, reducing this product to 2**n.
1066
+
1067
+ Subsets are enumerated in graded order, exactly as Natural_Powerset
1068
+ does: every size-0 sub-multiset first, then every size-1, and so
1069
+ on, each size-k block ordered exactly as
1070
+ Natural_Multiset_Combinator(multiplicities, k) orders it.
1071
+ """
1072
+
1073
+ def __init__(self, *args, **kwargs):
1074
+ if 'xtra_params' in kwargs:
1075
+ if len(kwargs['xtra_params']) != 0:
1076
+ self.MULTIPLICITIES = kwargs['xtra_params'][0]
1077
+ super().__init__(*args, **kwargs)
1078
+ elif len(args) == 1:
1079
+ multiplicities = tuple(args[0])
1080
+ for count in multiplicities:
1081
+ if not isinstance(count, int) or count <= 0:
1082
+ raise ValueError('Multiplicities must be positive integers')
1083
+ self.MULTIPLICITIES = multiplicities
1084
+ super().__init__(xtra_params=(self.MULTIPLICITIES,))
1085
+ else:
1086
+ raise ValueError('Need a multiplicities tuple of positive integers')
1087
+
1088
+ def direct_function(self, i):
1089
+ return ecomb.get_multiset_subset(i, self.MULTIPLICITIES)
1090
+
1091
+ def inverse_function(self, val):
1092
+ return ecomb.get_multiset_subset_number(val, self.MULTIPLICITIES)
1093
+
1094
+ def stop_init(self):
1095
+ return ecomb.multiset_powerset_count(self.MULTIPLICITIES)
1096
+
1097
+ def contains(self, val):
1098
+ if not isinstance(val, tuple):
1099
+ return False
1100
+ counts = Counter(val)
1101
+ if any(
1102
+ c < 0 or c >= len(self.MULTIPLICITIES) or counts[c] > self.MULTIPLICITIES[c]
1103
+ for c in counts
1104
+ ):
1105
+ return False
1106
+ if list(val) != sorted(val):
1107
+ return False
1108
+ return self.slice_contains(val)
1109
+
1110
+
1111
+ PowersetABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Multiset_Powerset((1,)))
1112
+
1113
+
1114
+ class Powerset(PowersetABCMixin):
1115
+ """An eset of every sub-multiset of a finite sequence, of every
1116
+ size, repeated elements allowed, built via EABCMixinFactory on top
1117
+ of Natural_Multiset_Powerset the same way Combinator wraps
1118
+ Natural_Multiset_Combinator. A subset is inherently unordered:
1119
+ contains()/index() accept any ordering of a valid sub-multiset of
1120
+ elements, same convention as Combinator.
1121
+
1122
+ An optional alphabet argument works exactly as it does for
1123
+ Permutator/Combinator/Arranger. elements may also be a dict (a
1124
+ Counter, typically), exactly as for the others.
1125
+ """
1126
+
1127
+ def __init__(self, *args, **kwargs):
1128
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
1129
+ eset_obj, self.elements, self.classes = kwargs['xtra_params']
1130
+ super().__init__(xtra_params=(eset_obj,))
1131
+ elif len(args) in (1, 2):
1132
+ elements = Permutator._expand_elements(args[0])
1133
+ self.elements = elements
1134
+ if len(args) == 2:
1135
+ alphabet = tuple(args[1])
1136
+ if len(set(alphabet)) != len(alphabet):
1137
+ raise ValueError('Alphabet entries must be unique')
1138
+ missing = set(elements) - set(alphabet)
1139
+ if missing:
1140
+ raise ValueError(
1141
+ f'Elements not present in alphabet: {sorted(missing)}'
1142
+ )
1143
+ present = set(elements)
1144
+ self.classes = [a for a in alphabet if a in present]
1145
+ else:
1146
+ self.classes = list(dict.fromkeys(elements))
1147
+ multiplicities = tuple(Counter(elements)[c] for c in self.classes)
1148
+ super().__init__(xtra_params=(Natural_Multiset_Powerset(multiplicities),))
1149
+ else:
1150
+ raise ValueError(
1151
+ 'Need a finite sequence (list, tuple, string, or Counter),'
1152
+ ' optionally followed by an alphabet'
1153
+ )
1154
+
1155
+ def init_check(self):
1156
+ return True
1157
+
1158
+ def _labels_to_elems(self, label_tpl):
1159
+ return tuple(self.classes[l] for l in label_tpl)
1160
+
1161
+ def _elems_to_labels(self, val):
1162
+ return tuple(sorted(self.classes.index(v) for v in val))
1163
+
1164
+ def direct_function(self, i):
1165
+ return self._labels_to_elems(self.eset_obj[i])
1166
+
1167
+ def inverse_function(self, val):
1168
+ return self.eset_obj.index(self._elems_to_labels(val))
1169
+
1170
+ def eset_obj_val(self, val):
1171
+ return self._elems_to_labels(val)
1172
+
1173
+ def get_mix_val(self, val):
1174
+ return self._labels_to_elems(val)
1175
+
1176
+ def id_contains(self, val):
1177
+ if val == val and Counter(val) != Counter(
1178
+ self.direct_function(self.inverse_function(val))
1179
+ ):
1180
+ return False
1181
+
1182
+ def contains_mixin_check(self, val):
1183
+ if not isinstance(val, tuple):
1184
+ return False
1185
+ val_counts = Counter(val)
1186
+ elem_counts = Counter(self.elements)
1187
+ return all(val_counts[v] <= elem_counts.get(v, 0) for v in val_counts)
1188
+
1189
+ def __getitem__(self, key):
1190
+ if isinstance(key, slice):
1191
+ return type(self)(
1192
+ xtra_params=(self.eset_obj[key], self.elements, self.classes)
1193
+ )
1194
+ return super().__getitem__(key)
1195
+
1196
+
1197
+ class Natural_Set_Partitioner(Eset):
1198
+ """A basic eset that handles set partitions: every way of
1199
+ grouping range(n) into non-empty, unordered, disjoint blocks whose
1200
+ union is everything, counted by the Bell number B(n). Unlike every
1201
+ other Natural_* class in this module, elements here are never
1202
+ repeated or chosen from a capacity -- a set partition is
1203
+ fundamentally about grouping distinguishable positions, so there's
1204
+ no Distinct_/Multiset_ split, just this and Set_Partitioner.
1205
+
1206
+ Each partition is represented as a restricted growth string (RGS):
1207
+ a tuple a_0..a_{n-1} where a_0 == 0 and each subsequent a_i is at
1208
+ most one more than the running maximum of what came before --
1209
+ the standard bijective encoding, where block labels are introduced
1210
+ in order of first appearance. Partition #0 is (0, 0, ..., 0), one
1211
+ block holding everything; the last is (0, 1, 2, ..., n-1), every
1212
+ position its own singleton block.
1213
+
1214
+ The count has no closed form: B(n) = set_partition_count(n-1, 1),
1215
+ a recursion on "how many ways to extend a RGS with m blocks
1216
+ already established, for r more positions" -- at each position,
1217
+ join one of the m existing blocks or start a new one (m+1
1218
+ choices), recursing on the result. Ranking/unranking follows the
1219
+ same place/try_candidate shape as the rest of this module, just
1220
+ with m+1 candidates per position instead of a fixed alphabet.
1221
+ """
1222
+
1223
+ def __init__(self, *args, **kwargs):
1224
+ if 'xtra_params' in kwargs:
1225
+ if len(kwargs['xtra_params']) != 0:
1226
+ self.N = kwargs['xtra_params'][0]
1227
+ super().__init__(*args, **kwargs)
1228
+ elif len(args) == 1:
1229
+ if not isinstance(args[0], int) or args[0] < 0:
1230
+ raise ValueError('Need a non-negative integer to initialize')
1231
+ self.N = args[0]
1232
+ super().__init__(xtra_params=(self.N,))
1233
+ else:
1234
+ raise ValueError('Need a non-negative integer to initialize')
1235
+
1236
+ def direct_function(self, i):
1237
+ return ecomb.get_set_partition(i, self.N)
1238
+
1239
+ def inverse_function(self, val):
1240
+ return ecomb.get_set_partition_number(val)
1241
+
1242
+ def stop_init(self):
1243
+ return 1 if self.N == 0 else ecomb.set_partition_count(self.N - 1, 1)
1244
+
1245
+ def contains(self, val):
1246
+ if not isinstance(val, tuple) or len(val) != self.N:
1247
+ return False
1248
+ if not ecomb.is_valid_rgs(val):
1249
+ return False
1250
+ return self.slice_contains(val)
1251
+
1252
+
1253
+ Set_PartitionerABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Set_Partitioner(0))
1254
+
1255
+
1256
+ class Set_Partitioner(Set_PartitionerABCMixin):
1257
+ """An eset of every way to partition a finite sequence of unique
1258
+ elements into non-empty, unordered groups, built via
1259
+ EABCMixinFactory on top of Natural_Set_Partitioner: that class
1260
+ enumerates restricted growth strings over positions, this class
1261
+ only translates between an RGS and the actual groups of elements
1262
+ it was given, elements sharing a label going into the same group,
1263
+ in label order (0, 1, 2, ... -- the order those groups first
1264
+ appear scanning elements left to right).
1265
+
1266
+ A set partition is unordered at two levels -- the groups
1267
+ themselves, and the elements within each group -- so
1268
+ contains()/index() accept any reordering of either and rank it the
1269
+ same, normalizing by re-deriving the RGS a given grouping implies
1270
+ (which element ends up in which position-label) rather than
1271
+ requiring the exact grouping/ordering direct_function happens to
1272
+ produce.
1273
+
1274
+ Elements must be unique, the same requirement Distinct_Permutator
1275
+ and friends have, since a set partition is fundamentally about
1276
+ grouping distinguishable items -- there's no meaningful analogue
1277
+ of "repeated elements" the way Permutator/Combinator/Arranger/
1278
+ Powerset have one, so unlike those, there's no alphabet argument
1279
+ or Counter/dict input here either: with elements already forced
1280
+ unique, first-appearance order is unambiguous on its own.
1281
+ """
1282
+
1283
+ def __init__(self, *args, **kwargs):
1284
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
1285
+ eset_obj, self.elements = kwargs['xtra_params']
1286
+ super().__init__(xtra_params=(eset_obj,))
1287
+ elif len(args) == 1:
1288
+ elements = tuple(args[0])
1289
+ if len(set(elements)) != len(elements):
1290
+ raise ValueError('Elements must be unique')
1291
+ self.elements = elements
1292
+ super().__init__(xtra_params=(Natural_Set_Partitioner(len(elements)),))
1293
+ else:
1294
+ raise ValueError(
1295
+ 'Need a finite sequence (list, tuple, or string) of unique elements'
1296
+ )
1297
+
1298
+ def init_check(self):
1299
+ return True
1300
+
1301
+ def _rgs_to_blocks(self, rgs):
1302
+ blocks = {}
1303
+ for pos, label in enumerate(rgs):
1304
+ blocks.setdefault(label, []).append(self.elements[pos])
1305
+ return tuple(tuple(blocks[label]) for label in sorted(blocks))
1306
+
1307
+ def _elems_to_rgs(self, val):
1308
+ elem_to_block_id = {}
1309
+ for block_id, block in enumerate(val):
1310
+ for e in block:
1311
+ elem_to_block_id[e] = block_id
1312
+ label_map = {}
1313
+ rgs = []
1314
+ for e in self.elements:
1315
+ block_id = elem_to_block_id[e]
1316
+ if block_id not in label_map:
1317
+ label_map[block_id] = len(label_map)
1318
+ rgs.append(label_map[block_id])
1319
+ return tuple(rgs)
1320
+
1321
+ def direct_function(self, i):
1322
+ return self._rgs_to_blocks(self.eset_obj[i])
1323
+
1324
+ def inverse_function(self, val):
1325
+ return self.eset_obj.index(self._elems_to_rgs(val))
1326
+
1327
+ def eset_obj_val(self, val):
1328
+ return self._elems_to_rgs(val)
1329
+
1330
+ def get_mix_val(self, val):
1331
+ return self._rgs_to_blocks(val)
1332
+
1333
+ def id_contains(self, val):
1334
+ canonical = self.direct_function(self.inverse_function(val))
1335
+ if val == val and set(frozenset(b) for b in val) != set(
1336
+ frozenset(b) for b in canonical
1337
+ ):
1338
+ return False
1339
+
1340
+ def contains_mixin_check(self, val):
1341
+ if not isinstance(val, tuple):
1342
+ return False
1343
+ if not all(isinstance(b, tuple) and len(b) > 0 for b in val):
1344
+ return False
1345
+ all_elems = [e for b in val for e in b]
1346
+ if len(set(all_elems)) != len(all_elems):
1347
+ return False
1348
+ return set(all_elems) == set(self.elements)
1349
+
1350
+ def __getitem__(self, key):
1351
+ if isinstance(key, slice):
1352
+ return type(self)(xtra_params=(self.eset_obj[key], self.elements))
1353
+ return super().__getitem__(key)
1354
+
1355
+
1356
+ class Compositioner(Eset):
1357
+ """A basic eset that handles compositions: every way of writing a
1358
+ non-negative integer n as an ORDERED sum of positive integers, so
1359
+ unlike Partitioner, 4 = 1 + 3 and 4 = 3 + 1 are two distinct
1360
+ compositions, not one. Like Partitioner, there's no elements domain
1361
+ involved and no Natural_*/wrapper split: a composition's parts
1362
+ already are the integers.
1363
+
1364
+ The count needs no new combinatorics at all: a composition of n
1365
+ corresponds exactly to a subset of the n-1 gaps between n items in
1366
+ a row (does a divider go there or not), the classic stars-and-bars
1367
+ bijection, so the count is 2**(n-1) and direct_function/
1368
+ inverse_function reuse get_subset/get_subset_number (the same
1369
+ functions Natural_Powerset itself uses) directly, applied to the
1370
+ complementary "no divider here" gaps -- chosen so that composition
1371
+ #0 is (1, 1, ..., 1) and the last is (n,), matching Partitioner's
1372
+ own convention at both ends, even though the two objects enumerate
1373
+ completely different, differently-sized sets.
1374
+ """
1375
+
1376
+ def __init__(self, *args, **kwargs):
1377
+ if 'xtra_params' in kwargs:
1378
+ if len(kwargs['xtra_params']) != 0:
1379
+ self.N = kwargs['xtra_params'][0]
1380
+ super().__init__(*args, **kwargs)
1381
+ elif len(args) == 1:
1382
+ if not isinstance(args[0], int) or args[0] < 0:
1383
+ raise ValueError('Need a non-negative integer to initialize')
1384
+ self.N = args[0]
1385
+ super().__init__(xtra_params=(self.N,))
1386
+ else:
1387
+ raise ValueError('Need a non-negative integer to initialize')
1388
+
1389
+ def direct_function(self, i):
1390
+ return ecomb.get_composition(i, self.N)
1391
+
1392
+ def inverse_function(self, val):
1393
+ return ecomb.get_composition_number(val, self.N)
1394
+
1395
+ def stop_init(self):
1396
+ return 1 if self.N == 0 else 2 ** (self.N - 1)
1397
+
1398
+ def contains(self, val):
1399
+ if not isinstance(val, tuple):
1400
+ return False
1401
+ if any(not isinstance(p, int) or p <= 0 for p in val):
1402
+ return False
1403
+ if sum(val) != self.N:
1404
+ return False
1405
+ return self.slice_contains(val)
1406
+
1407
+
1408
+ class Natural_Derangement(Eset):
1409
+ """A basic eset that handles derangements: permutations of
1410
+ range(n) with no fixed point (direct_function(i)[p] != p for every
1411
+ position p), counted by the subfactorial !n. Derangement #0 is
1412
+ whichever one the ranking scheme constructs first (see
1413
+ get_derangement in lib/ecombinatorics.py for the actual
1414
+ construction, a classic two-case bijection -- a 2-cycle closing
1415
+ off, or a smaller derangement with one value relabeled -- that
1416
+ proves !n = (n-1)*(!(n-1) + !(n-2))).
1417
+
1418
+ Only two of the n! permutations of a 1-element set would need to
1419
+ be considered, and neither is a derangement, so !1 == 0: this eset
1420
+ is legitimately empty for n == 1, the same way Natural_Combinator
1421
+ can be empty when k > n.
1422
+ """
1423
+
1424
+ def __init__(self, *args, **kwargs):
1425
+ if 'xtra_params' in kwargs:
1426
+ if len(kwargs['xtra_params']) != 0:
1427
+ self.N = kwargs['xtra_params'][0]
1428
+ super().__init__(*args, **kwargs)
1429
+ elif len(args) == 1:
1430
+ if not isinstance(args[0], int) or args[0] < 0:
1431
+ raise ValueError('Need a non-negative integer to initialize')
1432
+ self.N = args[0]
1433
+ super().__init__(xtra_params=(self.N,))
1434
+ else:
1435
+ raise ValueError('Need a non-negative integer to initialize')
1436
+
1437
+ def direct_function(self, i):
1438
+ return ecomb.get_derangement(i, self.N)
1439
+
1440
+ def inverse_function(self, val):
1441
+ return ecomb.get_derangement_number(val, self.N)
1442
+
1443
+ def stop_init(self):
1444
+ return ecomb.derangement_count(self.N)
1445
+
1446
+ def contains(self, val):
1447
+ if not isinstance(val, tuple) or len(val) != self.N:
1448
+ return False
1449
+ if sorted(val) != list(range(self.N)):
1450
+ return False
1451
+ if any(val[p] == p for p in range(self.N)):
1452
+ return False
1453
+ return self.slice_contains(val)
1454
+
1455
+
1456
+ Distinct_DerangementABCMixin = EABCMixinFactory.create_ABC_mixin(Natural_Derangement(0))
1457
+
1458
+
1459
+ class Distinct_Derangement(Distinct_DerangementABCMixin):
1460
+ """An eset of every derangement of a finite sequence of unique
1461
+ elements, built via EABCMixinFactory on top of Natural_Derangement:
1462
+ Natural_Derangement enumerates the positions, this class only
1463
+ translates between positions and the elements it was given. The
1464
+ sequence as given is the reference arrangement that every
1465
+ derangement moves every single element away from.
1466
+ """
1467
+
1468
+ def __init__(self, *args, **kwargs):
1469
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
1470
+ eset_obj, self.elements = kwargs['xtra_params']
1471
+ super().__init__(xtra_params=(eset_obj,))
1472
+ elif len(args) == 1:
1473
+ elements = tuple(args[0])
1474
+ if len(set(elements)) != len(elements):
1475
+ raise ValueError('Elements must be unique')
1476
+ self.elements = elements
1477
+ super().__init__(xtra_params=(Natural_Derangement(len(elements)),))
1478
+ else:
1479
+ raise ValueError(
1480
+ 'Need a finite sequence (list, tuple, or string) of unique elements'
1481
+ )
1482
+
1483
+ def init_check(self):
1484
+ return True
1485
+
1486
+ def _pos_to_elems(self, pos_tpl):
1487
+ return tuple(self.elements[p] for p in pos_tpl)
1488
+
1489
+ def _elems_to_pos(self, val):
1490
+ return tuple(self.elements.index(v) for v in val)
1491
+
1492
+ def direct_function(self, i):
1493
+ return self._pos_to_elems(self.eset_obj[i])
1494
+
1495
+ def inverse_function(self, val):
1496
+ return self.eset_obj.index(self._elems_to_pos(val))
1497
+
1498
+ def eset_obj_val(self, val):
1499
+ return self._elems_to_pos(val)
1500
+
1501
+ def get_mix_val(self, val):
1502
+ return self._pos_to_elems(val)
1503
+
1504
+ def contains_mixin_check(self, val):
1505
+ if not isinstance(val, tuple) or len(val) != len(self.elements):
1506
+ return False
1507
+ return all(v in self.elements for v in val) and len(set(val)) == len(val)
1508
+
1509
+ def __getitem__(self, key):
1510
+ if isinstance(key, slice):
1511
+ return type(self)(xtra_params=(self.eset_obj[key], self.elements))
1512
+ return super().__getitem__(key)
1513
+
1514
+
1515
+ class Natural_Cartesian_Product(Eset):
1516
+ """A basic eset that handles the Cartesian product of several
1517
+ ranges: every tuple (i_1, ..., i_k) with 0 <= i_j < sizes[j],
1518
+ enumerated in the same order itertools.product(*[range(s) for s in
1519
+ sizes]) uses (the last size varies fastest). Unlike every other
1520
+ Natural_* class in this module, this one takes several independent
1521
+ sizes rather than one, since it's about combining multiple
1522
+ distinct sources rather than choosing/arranging within a single
1523
+ one.
1524
+
1525
+ The count needs no recursion, just the product of the sizes
1526
+ (0 if any size is 0, 1 if sizes is empty), and ranking/unranking is
1527
+ a direct mixed-radix decomposition: unrank by peeling off the
1528
+ least significant "digit" (the last size) first via divmod, rank
1529
+ by folding the digits back together most significant first.
1530
+ """
1531
+
1532
+ def __init__(self, *args, **kwargs):
1533
+ if 'xtra_params' in kwargs:
1534
+ if len(kwargs['xtra_params']) != 0:
1535
+ self.SIZES = kwargs['xtra_params'][0]
1536
+ super().__init__(*args, **kwargs)
1537
+ elif len(args) == 1:
1538
+ sizes = tuple(args[0])
1539
+ for size in sizes:
1540
+ if not isinstance(size, int) or size < 0:
1541
+ raise ValueError('Need a tuple of non-negative integers')
1542
+ self.SIZES = sizes
1543
+ super().__init__(xtra_params=(self.SIZES,))
1544
+ else:
1545
+ raise ValueError('Need a tuple of non-negative integers')
1546
+
1547
+ def direct_function(self, i):
1548
+ return ecomb.get_cartesian_index(i, self.SIZES)
1549
+
1550
+ def inverse_function(self, val):
1551
+ return ecomb.get_cartesian_index_number(val, self.SIZES)
1552
+
1553
+ def stop_init(self):
1554
+ total = 1
1555
+ for size in self.SIZES:
1556
+ total *= size
1557
+ return total
1558
+
1559
+ def contains(self, val):
1560
+ if not isinstance(val, tuple) or len(val) != len(self.SIZES):
1561
+ return False
1562
+ if any(
1563
+ not isinstance(v, int) or v < 0 or v >= size
1564
+ for v, size in zip(val, self.SIZES)
1565
+ ):
1566
+ return False
1567
+ return self.slice_contains(val)
1568
+
1569
+
1570
+ Cartesian_ProductABCMixin = EABCMixinFactory.create_ABC_mixin(
1571
+ Natural_Cartesian_Product(())
1572
+ )
1573
+
1574
+
1575
+ class Cartesian_Product(Cartesian_ProductABCMixin):
1576
+ """An eset of every combination of one pick from each of several
1577
+ finite sequences, built via EABCMixinFactory on top of
1578
+ Natural_Cartesian_Product: that class enumerates index-tuples,
1579
+ this class only translates between an index-tuple and the actual
1580
+ pick from each source, one per source, in order.
1581
+
1582
+ Unlike Distinct_Permutator/Distinct_Combinator/Distinct_Arranger,
1583
+ which all translate positions within a single elements domain,
1584
+ this class combines several separate, independent sources -- the
1585
+ concrete capability gap this class fills. Each source must have
1586
+ unique elements internally (the same requirement every Distinct_*
1587
+ class has, for the same reason: index lookups need to be
1588
+ unambiguous), but different sources are free to share values with
1589
+ each other, since they're tracked by source position, not by a
1590
+ single shared elements domain.
1591
+ """
1592
+
1593
+ def __init__(self, *args, **kwargs):
1594
+ if 'xtra_params' in kwargs and len(kwargs['xtra_params']) != 0:
1595
+ eset_obj, self.sources = kwargs['xtra_params']
1596
+ super().__init__(xtra_params=(eset_obj,))
1597
+ elif len(args) == 1:
1598
+ sources = tuple(tuple(s) for s in args[0])
1599
+ for s in sources:
1600
+ if len(set(s)) != len(s):
1601
+ raise ValueError('Each source must have unique elements')
1602
+ self.sources = sources
1603
+ sizes = tuple(len(s) for s in sources)
1604
+ super().__init__(xtra_params=(Natural_Cartesian_Product(sizes),))
1605
+ else:
1606
+ raise ValueError('Need a sequence of sequences')
1607
+
1608
+ def init_check(self):
1609
+ return True
1610
+
1611
+ def _idx_to_vals(self, idx_tpl):
1612
+ return tuple(self.sources[j][idx_tpl[j]] for j in range(len(self.sources)))
1613
+
1614
+ def _vals_to_idx(self, val):
1615
+ return tuple(self.sources[j].index(val[j]) for j in range(len(self.sources)))
1616
+
1617
+ def direct_function(self, i):
1618
+ return self._idx_to_vals(self.eset_obj[i])
1619
+
1620
+ def inverse_function(self, val):
1621
+ return self.eset_obj.index(self._vals_to_idx(val))
1622
+
1623
+ def eset_obj_val(self, val):
1624
+ return self._vals_to_idx(val)
1625
+
1626
+ def get_mix_val(self, val):
1627
+ return self._idx_to_vals(val)
1628
+
1629
+ def contains_mixin_check(self, val):
1630
+ if not isinstance(val, tuple) or len(val) != len(self.sources):
1631
+ return False
1632
+ return all(v in s for v, s in zip(val, self.sources))
1633
+
1634
+ def __getitem__(self, key):
1635
+ if isinstance(key, slice):
1636
+ return type(self)(xtra_params=(self.eset_obj[key], self.sources))
1637
+ return super().__getitem__(key)