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/__init__.py +100 -0
- esets/besets.py +29 -0
- esets/cesets.py +1637 -0
- esets/ecombinatorics.py +810 -0
- esets/eset.py +479 -0
- esets/numeric.py +487 -0
- esets_lib-0.1.0.dist-info/METADATA +735 -0
- esets_lib-0.1.0.dist-info/RECORD +10 -0
- esets_lib-0.1.0.dist-info/WHEEL +4 -0
- esets_lib-0.1.0.dist-info/licenses/LICENSE +28 -0
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)
|