Repository navigation
Conversation
With a property as input, auto_bisc keeps every good and every bad permutation up to its current length as lists of Perm objects. At length 10 the bad ones are nearly all of S_10, about 0.9 GB as a list, and runs on sets that need length 10 grew past 8 GB. LabelledPerms keeps the labels as one bitmap per length, in the order of Perm.of_length (S_10 is 454 KB), made from a property, given as bitmaps, or both. Its views good and bad stand in for auto_bisc's dictionaries of lists: auto_bisc only iterates over a length, counts it, and asks whether it is there, and the views do that from the bitmaps, generating a length's permutations again when it is read, in the same order. - auto_bisc(LabelledPerms(...)) and bisc(LabelledPerms(...)) are new inputs; with given bitmaps and no property, auto_bisc stops when it needs a length it was not given, as for a tuple of dictionaries. - auto_bisc(prop, labels="bitmaps") wraps a property in LabelledPerms; labels="lists", the default, is the previous behaviour, unchanged. The existing inputs are untouched. On stack-sortable and West-2-stack- sortable permutations every route gives the same basis as before (tests/bisc/test_labelled_perms.py); auto_bisc's printed output is also identical on Av(231), Av(321), Av(2-31) and Av(132, 4321). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When auto_bisc needs a length it was not given, a LabelledPerms with a property labels it, and one with bitmaps alone stops. Derangements in Av(213, 231, 312), with one member at each even length, reach that step in a fraction of a second: with bitmaps up to length 8 and no property auto_bisc stops and says so; with a property, stopped at the first permutation of length 9, the lists and the bitmaps have printed the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's BiSC section starts with `from permuta.bisc import *`, which did not bring in LabelledPerms. It is now exported, and the section says that bisc and auto_bisc also accept a LabelledPerms, and how auto_bisc's labels="bitmaps" keeps a property's labels in less memory, with an example that runs as a doctest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bit i of length n is the label of Perm.unrank(i, n), so a permutation of length n is at its rank() less the number of shorter permutations. This is the order Perm.of_length(n) yields, which the views still use to iterate: it is the same order and about three times faster than unranking each index. A test pins the order to Perm.unrank and Perm.rank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewers of PermutaTriangle's pull requests ask for variable names that
say what they hold ("for rule_key in ..." rather than "for rk in ...").
In the code this pull request adds:
- LabelledPerms and its views: a length is `length`, as in
Perm.of_length, where it was `n`; a bitmap is `bitmap` where it was
`data` or `bits`; a byte is `byte` where it was `b`; the bit a view
wants is `wanted_bit` where it was `want`; the number of good
permutations is `good_count` where it was `good`. The attribute `bits`
is now `bitmaps`, as the constructor's keyword says.
- The tests: `length`, `bitmap`, `bitmaps`, `max_length`, `output`,
`entry`, `round_trip`, `too_short` and `shorter_count`, where they had
`n`, `bits`, `top`, `out`, `x`, `again`, `short` and `shorter`.
- The README's example: `length` where it had `n`.
The names bisc and auto_bisc already had (A, B, D, L, i, m and prop, the
BiSC paper's notation) are kept, and `i` stays the index of a bit. Nothing
else changes: the bisc tests and the README's examples pass as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 1, its readability and duplication suggestions: - auto_bisc's docstring opens with what it does and returns, names the inputs it takes, says when to use labels="bitmaps", and says that the lengths it labels are added to a LabelledPerms it is given. - LabelledPerms: its docstring says length where it said n; counts is now good_counts; one helper, _bitmap_size, gives a bitmap's size; add_length has a docstring and keeps bytes, as the constructor does; _PermsOfLength drops __bool__, which __len__ already gives. - auto_bisc uses prop where it had the alias labelled. - The tests use Perm.stack_sortable and Perm.west_2_stack_sortable instead of copies of their own, BASIS_231 for the basis they expect, and labelled_perms for a LabelledPerms, so that labels is only the option's name. The README's example is renamed the same way. - The README and CHANGELOG say when to use bitmaps. Nothing else changes: the bisc tests and the README's examples pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 1, blocking finding 2. LabelledPerms took any bitmap of the right size: - set padding bits, past a length's permutations, were counted: bitmaps={3: bytes([0xFF])} gave good[3] 6 permutations but a length of 8, and len(bad[3]) raised; - an int passed, since bytes(15) is 15 zero bytes: bitmaps={5: 15} said that no permutation of length 5 is good. A bitmap must now be bytes, a bytearray or a memoryview (TypeError otherwise), and a bit set past the length's permutations is a ValueError. The good permutations are counted with int.bit_count. The tests from the review fail on the previous commit and pass now, and a new one checks that any bytes-like bitmap is accepted and kept as bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 1, blocking findings 1 and 3: - No test checked that labels="bitmaps" keeps bitmaps: the results are the same in both modes, so deleting the line that wraps the property in LabelledPerms passed every test. test_labels_option_selects_the_ representation records the lengths added as bitmaps: none for lists, 0 to 8 for bitmaps. It fails when that line is deleted. - The docstring, README and CHANGELOG offer labels made from a property, given as bitmaps, or both, and no test built both. The new test gives bitmaps of lengths 0, 1, 2, 3, 5 and 8, made by an encoder of its own, and checks that auto_bisc labels only lengths 4, 6 and 7 with the property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…property From the review of #209, round 1, suggestions 1, 3 and 4: - bisc given a LabelledPerms did not use its property: with fewer lengths labelled than n it raised a bare KeyError from mine(), and with n left out on a new LabelledPerms a ValueError from max(). It now labels the lengths up to n that are missing with the property, adding them to the LabelledPerms as auto_bisc does. Without n it uses the longest length labelled, or 7, as for a property. It raises a ValueError that says what is missing when there is no property to label with. bisc has a docstring. - auto_bisc(prop, labels="bitmaps") silently ignored the option for a list, a tuple of dictionaries or a file name. It now raises a ValueError; a property or a LabelledPerms is accepted as before. - auto_bisc's message for an input it does not take lists LabelledPerms, as bisc's does. The three new tests fail on the previous commit and pass now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 2: - Blocking: no test checked that bisc chooses n from the bitmaps, or that it labels only the missing lengths; two faults made by hand passed every test. The review's two tests are added. They pass now and fail on both faults. - Recommended point 1: mine() reads the lengths up to m, so when m > n bisc raised a bare KeyError for a LabelledPerms, such as bisc(LabelledPerms(Perm.stack_sortable), 3, 0). bisc now labels up to max(m, n). Without a property it raises the ValueError that says which length is missing. A new test fails on the previous commit and passes now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 2, recommended points 2 and 3: - auto_bisc's docstring said that from length 10 on "the bitmaps [take] under a megabyte". A bitmap is 454 KB at length 10, but 5 MB at length 11 and 60 MB at length 12; the docstring now gives these. - The README, the CHANGELOG and LabelledPerms' docstring said the permutations of length 10 take about 0.9 GB as lists. Building list(Perm.of_length(10)) grows a process by 0.88 GB on PyPy 3.11, 1.26 GB on CPython 3.11 (measured here) and 1.8 GB on CPython 3.14 (the review), so they now say 0.9 to 1.8 GB, depending on the Python. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 2, its optional points, but one: - auto_bisc's message for an input it does not take says "a tuple of two dictionaries", which is what it takes, where it said "a dictionary". - Docstrings: LabelledPerms' says that bisc labels missing lengths too, and that bisc raises where auto_bisc stops; bisc's says what it returns and what report does; add_length's says that it needs a property and replaces a length's bitmap. - Every pytest.raises names its message with match=, so a changed message fails a test. - New tests: lengths 0 and 1 labelled bad, an empty bitmap (too short), and bisc given a LabelledPerms's view. Not taken: one method for the three loops that label missing lengths, which, like making _LengthViews a Mapping, is left for later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 3, Henning's point 2: "Labelled permutations doesn't feel like the right name for the bitmap." The class holds, for each length, one bit per permutation: whether it has the property. PropertyBitmaps, the review's suggestion, is Henning's choice. Renamed throughout, with no change in behavior: bisc.py, permuta.bisc's imports and __all__, the README, the CHANGELOG, and the tests, whose file is now tests/bisc/test_property_bitmaps.py. A variable holding one is property_bitmaps, as is the views' reference to it, and the tests' helper is property_bitmaps_up_to. Words about labels and labelling stay, as does labels="bitmaps". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 3, Henning's point 3: - The loop over given bitmaps read "(bitmaps or {}).items()", turning a missing bitmaps (None) into an empty dictionary so that the loop did nothing. It now runs under "if bitmaps is not None:". - The padding-bit check is needed, and a comment now says why: the size check leaves spare bits in the last byte only at lengths 0 to 3 (7, 7, 6 and 2 of them), since n! is a multiple of 8 from n = 4 on, and a spare bit set would be counted as a good permutation, so that len() of the good view would disagree with its iteration and len() of the bad view could go negative. No change in behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jaypantone
requested changes
Oct 23, 2026
From the review of #209, round 3, Henning's points 1 and 3 (what _LengthViews is doing): the views stood in for the dictionaries of lists that auto_bisc builds from its other inputs by duck typing, with only keys(), in and [length]. Now they are the standard abstractions, which a reader and mypy can follow: - _LengthViews is a collections.abc.Mapping[int, _PermsOfLength], with __getitem__, __iter__ and __len__; keys(), in and get() come from Mapping. Its keys are the lengths with a bitmap. - _PermsOfLength is a collections.abc.Collection[Perm], with a docstring. Its new __contains__ reads a permutation's bit, at perm.rank() less the number of shorter permutations, as the class docstring says; a permutation of another length is not in it. - bisc's branches for a dict and for a _LengthViews are one branch for a Mapping, so bisc takes any mapping of lengths to permutations. A new test checks the views are a Mapping of Collections and that in agrees with the property for every permutation up to length 6. It fails on three faulty versions of __contains__: without the offset of the shorter permutations, without the length check, and with the good bit wanted in both views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 3, Henning's point 1: "The pull request would be better with more Typing and mypy." bisc.py began with a file-level "# type: ignore", so mypy had never read the code #209 adds. - The ignore is gone, and mypy 2.4 passes on bisc.py, for Python 3.14 as in CI and for 3.10. - bisc_subfunctions.py keeps its errors ignored until a pull request of its own types it (Henning's choice), but as "# mypy: ignore-errors". A file-level "# type: ignore" makes mypy ignore the module's contents, so it reported that four of the functions bisc.py imports (run_clean_up, show_me, show_me_basis, to_sg_format) do not exist; with the new line mypy reads the module and reports none of its errors. - Type hints on every signature #209 adds or changes: the parameters of bisc and auto_bisc (labels: str too), PropertyBitmaps' __init__ and add_length, _bitmap_size, _LengthViews and _PermsOfLength. A given bitmap is typed bytes, bytearray or memoryview, as the check accepts. bisc and auto_bisc have no return types: what they return comes from bisc_subfunctions.py, untyped until its pull request. - The variables holding the permutations by length, bisc's D and auto_bisc's A and B, are typed Mapping[int, Collection[Perm]], which clears the six errors the review found (seven before the views became a Mapping). A list or a property is sorted into dictionaries of lists of their own, perm_lists in bisc, and good_perm_lists and bad_perm_lists in auto_bisc, which a property's new lengths are added to as L grows. - With the property typed Optional, add_length without one raises a ValueError that says so, where it failed with "'NoneType' object is not callable" (a follow-up from the round 2 review). bisc and auto_bisc check for a property before they call it, so they are unchanged. A new test covers it. What auto_bisc and bisc print and return is unchanged: compared, byte for byte, with develop (ad35732) and round 3's c54c542, for auto_bisc given a property (with and without growing past length 8), a list and a tuple of dictionaries, and bisc given a list, a property and a dictionary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4, Henning's question about what "bitmap[i >> 3] |= 1 << (i & 7)" and "(bitmap[i >> 3] >> (i & 7)) & 1" do: they set and read the label of the permutation at index i, bit i % 8 of byte i // 8. Two helpers now say so, _set_label and _label, written with // and %, which give the same for an index that is not negative. add_length, _PermsOfLength.__iter__ and __contains__ use them. The call costs little, timed on S_10 (best of 3, the bit work alone with a property as cheap as perm[0] < perm[-1]): - setting, as add_length: CPython 3.11 1.17 s inline, 1.20 s with the helper; PyPy 3.11 0.107 s either way; - reading, as __iter__: CPython 1.14 s and 1.22 s; PyPy 0.127 s and 0.133 s. With a real property, stack-sortability, setting S_10 took 17.2 s and 17.3 s on CPython, and 22.6 s and 20.1 s on PyPy (noise), so the property's cost hides the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4, Henning: "Lets keep accepting these different inputs in, but everything should be turned into a bitmap." He confirmed it in the BiSC agent's session. bisc and auto_bisc take what they took before. Each input is turned into a PropertyBitmaps at the start, and from there one code path reads the good and the bad permutations through its views. - labels= is gone, with its lists path. It was added by #209 and never released. - A list sets the bits of the permutations it lists. Every length up to the longest listed, and up to max(m, n) in bisc, gets a bitmap, all zeros where nothing is listed, as the defaultdict gave an empty list. - A property labels the lengths up to n in bisc, and the lengths past n, when m > n, are empty, as they were in the lists. That keeps develop's answer for that case. - A Mapping given to bisc is read for each length up to max(m, n). A length it lacks is a KeyError, as mine() raised before. - A tuple of two dictionaries, or a stored set, must give each permutation of each length, up to the shorter dictionary's longest, exactly once, as good or as bad. Otherwise it is a ValueError, where the complement used to be taken in silence. All 15 stored sets pass (4.2 s on CPython 3.11, 1.4 s on PyPy 3.11, all checked by a new test). - A permutation listed twice sets one bit. With the lists, (0, 1) listed twice among the increasing permutations made mine() take length 2 as complete, and bisc found nothing; now it finds 10, as it does without the duplicate. The CHANGELOG says so. - auto_bisc gives bisc its PropertyBitmaps. The reading of a stored set, written twice in auto_bisc, is one function, _read_stored_set, with the same logic. - The docstrings, messages, README and CHANGELOG describe the class in its own terms ("no bitmaps and no property", "You need to add bitmaps of longer permutations"), and the CHANGELOG records only the final behavior, under Added and Changed. Tests: - one for each input kind of bisc and of auto_bisc, which spies on mine() for the bitmaps it is given; - a list with a length missing, and with a duplicate; - a property with m > n; - the ValueError for a tuple missing a permutation and for one given twice; - every stored set. The tests of labels= are gone, and West-2 is compared with its basis. What auto_bisc and bisc print and return is byte for byte develop's (ad35732) in 15 runs, on CPython 3.11: - auto_bisc given stack-sortability, West-2-stack-sortability, a property that grows past length 9 (stopped at 10), a list, a shuffled list, a tuple, a tuple with its lists reversed, "smooth" and "SimSun"; - bisc given a list, a shuffled list, a property (with m <= n and m > n), a dictionary, and one with its lists reversed. The permutations are read in lexicographic order, the order of the lists that a property, a list's complement and the stored sets gave. So with a dictionary in another order, a message may name a different first permutation; no run here changed its result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4: with every input turned into bitmaps, reading a length through a view generated all of its permutations, even when few were good. That matters for a short list of long permutations given to bisc: every permutation of length 11 would be generated, about 14 s per read on CPython (40 million, at 0.35 us each), where the lists took microseconds. Henning chose to keep the speed (in the BiSC agent's session, 2026-10-23). A view now unranks the permutations it wants, in increasing index, which is the lexicographic order of Perm.of_length, when at most one in 16 is wanted (_SPARSE_FACTOR). Otherwise it generates the length as before. _wanted_indices skips each byte with nothing wanted, and stops at the last byte's spare bits. A length with nothing wanted is not read at all. The factor of 16: unranking a permutation costs 1.5 to 2.0 us on CPython 3.11 and 0.34 to 0.43 us on PyPy 3.11, at lengths 8 to 12, against 0.35 us and 0.03 us to generate one and test its bit. That is 5 and 13 times as much. With it, bisc given the identities up to length 3 and 20 random permutations of length 10, or of length 11, takes: - CPython 3.11: develop 0.71 s and 1.24 s, now 0.72 s and 1.30 s; - PyPy 3.11: develop 0.53 s and 0.83 s, now 0.58 s and 0.77 s. The bitmap of length 11 adds 5 MB to the peak. A new test reads, by unranking, the stack-sortable permutations of length 8 (1430 of 40320). By generating, it reads those of length 7 (429 of 5040). It also reads the bad views of a set of all but the identity, of length 6 with its first eight permutations bad (a byte of zeros), and of length 3 with none bad. It fails on three faulty versions of _wanted_indices: one that skips zero bytes in a bad view, one that yields in another order, and one that reads the spare bits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4: three faults that change what bisc returns passed all 60 tests in tests/bisc. The review wrote the tests that catch them, and these are its tests: - test_views_unrank_a_byte_of_good_permutations, beside its mirror for a byte of bad ones: the sparse read must not skip a byte whose eight permutations are all good (blocking finding 1). - tests/bisc/test_bisc_inputs.py, a module of its own, 24 tests of how bisc and auto_bisc take each kind of input (blocking findings 2 and 3, and older gaps): - the default n of a list, a mapping, a PropertyBitmaps and a property; - m > n, and m or n zero, for a list and a mapping; - the empty set; - report=True; - what auto_bisc prints for each input kind, and where it stops; - growth that would label a length it was given; - a duplicate on either side of a tuple; - the views as lengths are added; - where a view starts to unrank. - Three more in tests/bisc/test_property_bitmaps.py: - a list of tuples; - a permutation under another length's key; - a tuple that lacks a length in one dictionary. The review's notes for itself are reworded for Permuta (comments that named develop, the review or the faults). Its helper bitmaps_of looks permutations up in a set rather than a list. Checked: all 88 tests in tests/bisc pass. Each of the review's faults, planted in a copy, fails a new test: the sparse read skipping a byte of ones; a list, or a mapping, read only up to n; a list's longer permutations kept; and a PropertyBitmaps's default n one short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4, its statements to correct: 1. "Exactly once" was not what the code does: a permutation given twice on one side of a tuple, or of a stored set, is accepted and counted once. auto_bisc's and _from_good_and_bad's docstrings and the CHANGELOG now say that the good and the bad permutations of each length must not overlap and must together be all of that length's permutations. The ValueError says so, with how many permutations are in both and how many in neither. 2. auto_bisc's docstring said that a property labels each length when auto_bisc first needs it; lengths 0 to 8 are labeled at the start, and only the longer ones when needed. 3. The line "Populating the dictionary of bad perms of length" keeps what auto_bisc printed for a list, though nothing is populated now. A comment says so. 4. "As it was in the lists before", in bisc's docstring, a comment and the tests, is reworded for a reader who never saw the lists. The CHANGELOG also has a line for the ValueError given for a permutation under another length's key (new, from the review's untested behaviors). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4, its edge case 5: a tuple whose bad dictionary stops before the good one's length 8 was checked only on the good one. When the bad permutations stopped before length 7, the bitmaps, which hold only the lengths both dictionaries give, lacked a length that auto_bisc learns from before its sanity checks reach length 8. So bisc raised "bisc needs bitmaps up to length 7, and the PropertyBitmaps has none of length 7 and no property", naming an object the user never gave. develop printed "You need to add longer permutations to the dictionaries" and returned None. auto_bisc now checks, after turning the tuple into bitmaps, that they reach length L - 1, and otherwise prints that message and returns None. A bad dictionary that stops at length 7 still goes the way develop went, as the review's test checks. A new case in the same test, with the bad permutations stopping at length 6, raised the ValueError before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4, its edge case 7: in on a view took only a Perm of the length and read the bit at its rank without checking that it is a permutation. - (0, 1, 2) in good[3] was False, where the lists found it, since a tuple equals the Perm. - Perm((1, 2, 3)) in good[3] was True, its rank landing on another permutation's bit. - Perm((5, 5, 5)) in good[3] raised IndexError. Now in takes a Perm or any other sequence of the numbers 0 to length - 1, each once, and reads the bit of that permutation; anything else is in neither view. bisc and auto_bisc do not use in. _shorter_count, which in called for every permutation, is cached, as the review suggested. The views test checks a tuple and a list of the length, and a Perm that is no permutation of it, one with an entry repeated, a string and an int. It fails on the commit before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 4, its readability points and two of its smaller suggestions: - 9: the views' flag had three names (good, is_good, wanted_bit). Now it is wanted_bit throughout, passed by keyword: _LengthViews(self, wanted_bit=1) for good and wanted_bit=0 for bad. - 10: _SPARSE_FACTOR is now _UNRANK_WHEN_ONE_IN, and its comment says why 16: above the measured 5 (CPython) and 13 (PyPy). The early return for a length with nothing wanted has a comment. - 12: bisc returns _Patterns and auto_bisc Optional[_Patterns]. _Patterns is the forbidden patterns' type, length -> {pattern: [shadings]}. auto_bisc's bare returns say None, as mypy wants with the return type. permuta/bisc/__init__.py no longer starts with "# type: ignore". bisc_subfunctions.py's "# mypy: ignore-errors" has a comment saying why and that a pull request of its own will type the module. - 13: American English, "labeled", in a comment and in the tests' names and locals. - 14: a bitmap of the wrong size says how many bytes it has and must have, and a permutation under another length's key is named. _read_stored_set's en and spl are entry_name and name_parts. - 8 and 14: the class docstring gives a bitmap's size in bytes and the errors a bad bitmap raises, and says that bitmaps and good_counts are for reading, kept together by the constructor and add_length. Not taken: - the rename of prop, which is auto_bisc's and create_bisc_input's name for the same thing ("property" would shadow the builtin); - one method for the five loops that label missing lengths, which Henning left for later in rounds 2 and 3; - a guard for two bad bases in a row (6), which the review says cannot happen naturally and needs no action. Checked: mypy 2.4.0 finds nothing for Python 3.14 and 3.10, now reading permuta/bisc/__init__.py too. The 88 tests in tests/bisc pass on CPython 3.11, and with the README's examples on PyPy 3.11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6: sparse storage, which Henning asked for, by the review's design. A short list of long permutations cost a bitmap for every length up to its longest: three permutations of length 12 peaked at 211 MB on CPython 3.14, against develop's 45 MB, and the bitmap of length 14 alone would be 10.9 GB. Now each length keeps the smaller of two forms: its bitmap, (n! + 7) // 8 bytes, or the sorted indices of its good permutations in an array("Q"), 8 bytes each, which are the smaller when fewer than one permutation in 64 (_SPARSE_WHEN_ONE_IN) is good. The form is chosen length by length, by its count, since a property's share of good permutations can rise with the length, though a class's cannot. - Every way in chooses the form. A list or a mapping that gives few permutations of a length keeps their sorted, distinct indices; one that gives more sets a bitmap, and keeps the indices instead only when permutations given twice made them look many. add_length keeps the indices while they are few, and once they would take as much space as the bitmap, sets them in one and fills it from there, so that its peak is two bitmaps. A tuple's good side chooses as a list does, and the check of the good and the bad counts the overlap in whichever forms they have. A bitmap given is turned into indices when they are the smaller. - No int is made of a whole bitmap: bits are counted, and the overlap of two bitmaps found, a chunk of 64 KB at a time. The spare bits of a bitmap given are checked on its last byte only. - The views read both forms. The good permutations of a length kept as indices are unranked from them, and its bad ones are generated, with the good indices skipped in step; in is a binary search, and len() reads the count. The path of a bitmap is unchanged. - bitmaps is now a read-only Mapping, which builds the bitmap of a length kept as indices when asked for it, but not to answer in. good_counts is unchanged. bisc and auto_bisc no longer copy a bytearray they have just built; a bytearray given is still copied, so that changing it does not change the labels. The README and the changelog say how a length is kept. Checked: every existing test passes unchanged on CPython 3.11, and with the README's examples on PyPy 3.11. What bisc and auto_bisc print and return is develop's, on both Pythons, for every input kind of round 5's comparisons and for three permutations of length 12, and of length 14. At length 13, whose three are not in lexicographic order, the shadings come in the order develop gives for the three sorted, as on b4b27e3. mypy 2.4.0 finds nothing for Python 3.14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6, its checks, in tests/bisc/test_sparse_storage.py: - Both forms against brute force, for every view (iteration and its order, len(), in, the bad view), the count and the bitmap, and for every way in: a list or a mapping (shuffled, some permutations given twice), a tuple, bitmaps and a property. At lengths 0 to 8, with the threshold set to one in 2, so that both forms occur from length 2 on, and to one in 64, the default. The good sets have no permutation, all, as many as the threshold, one fewer, and a random number on each side of it. The bitmaps are read seven bytes at a time, so that counting them takes several chunks. - add_length switching to a bitmap in the middle of a length: early, late, at the last permutation, with the good spread out, and staying with indices one short of the threshold. - A permutation listed five times kept as one index. - A tuple's overlap counted in every pair of forms of its good and bad. - A length kept as indices builds no bitmap to answer in, to give its lengths or to read its good permutations, only when its bitmap is asked for. - bisc on three permutations of length 14 gives develop's answer, with tracemalloc's peak under 1 MB. tracemalloc is CPython's, so PyPy skips this one. Checked: the 645 tests pass on CPython 3.11, and the 114 in tests/bisc, with the README's 70 examples, on PyPy 3.11. Each of 23 faults planted in the new and changed functions of bisc.py fails at least one test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6, blocking 1. The indices of a length's good permutations were kept in an array("Q"), which holds integers to 2**64 - 1, but 21! is above 2**64: listing a permutation of length 21 or more raised OverflowError, where develop answers at once. An index of a length to 20 fits in 8 bytes, and those lengths keep their indices in the array, as before. Past length 20 they are kept in a list (_new_indices), and every place that read the array reads either. Such a length is always kept as indices, since its bitmap could not be made. The array's type code is named once (_INDEX_TYPECODE), and the threshold of one in 64 is derived from its size: an index takes 64 bits, where the bitmap takes one bit per permutation. The class docstring, the README and the changelog say so. Tests, the review's: the decreasing permutation of lengths 20, 21 and 25 listed, an index of length 21 from 2**63 to 2**64, and bisc on one permutation of length 21 or 25, against develop's answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… indices are many From the review of #209, round 6, blocking 2. On PyPy, auto_bisc(smooth) took 2.3 times develop's time: add_length's one loop called factorial (through _is_sparse) for every good permutation, and appended each to the array until the switch, then tested at each good one which form it was in. Now the limit is computed once (_most_indices, which _is_sparse uses too), and the loop is two. The first appends the indices of the good permutations while they are within it. Once one more is good, they are set in the bitmap, and the second loop labels the rest of the length, setting a bit for each good permutation and doing nothing more, as before the sparse storage. A length that stays within the limit keeps its indices, as before. PyPy 3.11, auto_bisc(smooth), eight runs of each, interleaved, each in its own process, with the Mac idle: median 1.02 s (1.01 to 1.46), develop 1.35 s, b4b27e3 2.50 s, 841c8dc 2.69 s; peak resident memory 106, 132, 168 and 172 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6, its suggestions 2 and 5. add_length and a list or a mapping kept a dense length's bitmap as a bytearray, so that bitmaps copied it to bytes on every read: a millisecond a read at length 12. Now a bitmap is kept as bytes once built, and bitmaps gives it as it is. add_length converts its bytearray once at the end: its peak stays two bitmaps, as at the switch from indices. The constructor and _labels_of both counted a bitmap and then chose its form; _labels_from_bitmap does that for both, copying a bytearray to bytes as the constructor did, so that changing a bytearray given still does not change the labels. _Labels no longer includes bytearray. Test: a dense length from a property, from a list and from a bytearray given is kept as bytes, and bitmaps gives the same object on every read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6, its suggestion 1: its 19 tests, as it wrote them, but in tests/bisc/test_sparse_storage.py, with that file's helpers (assert_views_match for their assert_reads_as, which it contains), docstrings for the numbered comments, and one local named byte. They cover: - a bitmap given as a bytearray or a memoryview, changed afterwards, and a memoryview of another format, shape or stride; - bitmaps giving bytes, equal on every read, and a round trip through it that keeps each length's form; - the first and the last permutation of a length kept as indices; - a property that raises in add_length, before and after the switch, and add_length replacing labels in either direction between the forms; - the lengths past n, with m > n, counted as empty and kept as no indices without a bitmap sized; a length of 14 kept as indices with no bitmap sized, which runs on PyPy as well, where tracemalloc does not; - a view iterated twice or in step with itself, bisc taking a view, and a repeated permutation in a list that looks sparse. They pass on 841c8dc too: they close gaps, which faults planted by the review in copies of it showed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6, its suggestions 3 to 5. - The texts say that the number of good permutations chooses a length's form, and _keep trusts its caller to have chosen it. A comment where _labels is made says that the storage is the module's: bitmaps, the views and the functions that make a PropertyBitmaps read _labels and keep a length's labels through _keep. - The class docstring says what reading bitmaps costs: the bitmap of a length kept as indices is built on each read, so items(), values(), dict() or bitmaps given to another PropertyBitmaps build every such length's, 10.9 GB for length 14; asking whether a length is there, iterating over the lengths and len() build none. - _count_labels, which counts bits, is _count_bits. It and the overlap of two bitmaps read them through one generator, _chunks. bitmaps and the views share their lengths through a base class, _Lengths. - A Perm whose index falls outside its length's, which only one whose values are not a permutation's can have, raises a ValueError naming it, whichever form its length is kept in. Before, a length kept as indices took it silently and failed later, and a bitmap raised IndexError. The changelog says so. - The tests of the two forms have docstrings, the threshold's comment says from which length a set with a good permutation is kept as indices (3 with one in 2, not 2), and the locals randomness, first_dense, good_8 and bad_8 are seeded_random, fewest_in_a_bitmap, good_of_length_8 and bad_of_length_8. A test checks the ValueError, in both forms. Not taken: one bitmap oracle for the three test files, which would change round 4's and 5's tests for no gain in what they check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 6, its point on PyPy's speed. A view's __iter__ was one generator holding three loops: the good of a length kept as indices, the bad of one, and a bitmap's. Round 6 put the first two in front of the bitmap's loop, which was round 5's, and on PyPy auto_bisc("smooth"), the stored set, then often ran about a third slower than on round 5. Now each loop is a generator of its own (_unranked, _skipping and _generated), and __iter__ only chooses one. In six interleaved runs of each, with the Mac idle, auto_bisc("smooth") took 1.27 to 1.33 s on PyPy, where the head before took 1.39 to 2.19 s and b4b27e3 1.44 to 1.77 s; the property smooth, West-2 and stack-sortable took the same as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 7, its blocking finding. Round 7 loosened is_kept_as_indices, the test helper that tells the two forms apart, to accept an array or a list at any length. A list kept where the array is meant, which takes five times the memory, then passed every test: two faults of the review's, add_length starting with a list and _labels_from_bitmap making one, passed all 141 tests of tests/bisc. The helper now asks for the container of each length: an array to length 20, and a list past it, as round 6's asked for the array. It is the review's, and all 53 tests of the file pass with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 7, its suggestion 1. The changelog, the description and a test said that a Perm whose values are not a permutation's raises a ValueError, but _indices_of checked only that its rank fell inside its length's. Most such sequences rank inside it, as another permutation: (0, 0, 0) and (0, 1, 1) as the identity, and (1, 2, 3), written from 1, as 201. At length 7, 4,166 of the 5,040 permutations written from 1 were taken so, silently. Develop mined such sequences as they were given, which made no sense of them either: for Av(231) written from 1, bisc(..., 3, 6) forbids all six patterns of length 3. _indices_of now compares each sequence's values with 0 to length - 1, as `in` does, and the range check, which this makes redundant, is gone. The check costs a set per permutation: about a tenth of rank()'s time on CPython 3.11 and about a third of it on PyPy 3.11, at lengths 8 and 10. The test of the ValueError covers sequences that rank as another permutation, and the review's that rank before the first or one past the last, through bisc and through _from_perms_by_length, in both forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y iterate From the review of #209, round 7, its suggestion 4. A view's __iter__ began with len(self), which Python limits to sys.maxsize, so iter() and bool() of the bad view of a length past 20, whose count is past it, raised OverflowError. The list form made these lengths reachable. __iter__ and a new __bool__ read the count from _wanted_count, an int of any size, which __len__ returns too, so only len() itself raises there. The class docstring says what stops past length 20: a length's bitmap, which cannot be built, and len() of its bad permutations, which can still be iterated over and tested with in. It also says that the indices take less than the bitmap but for the few percent an array allocates ahead. The review's test of a length kept in a list, with bool(), the first bad permutations and len()'s OverflowError added, fails on round 7's code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 7, its suggestions 2, 3 and 5, with the review's tests: - a permutation of length 100 or 200, whose n! / 64 a float cannot hold past length 171, kept as indices; - the bitmap of a length past 20, which cannot be built, and the overlap of labels kept as lists, which only such a length has; - add_length dropping its indices once it has the bitmap (CPython only), and a property true only on the first or the last permutation of a length, at lengths 0 to 8; - a view with nothing to yield, which reads no labels; - bisc keeping the lengths past n as empty arrays to length 20 and empty lists past it, with m = 25; an empty length 25, and bisc([], 3, 25); - the bad view of length 20, the longest kept in an array, read lazily. The test of three permutations of length 14 under tracemalloc now fails at once on a fault that would size a bitmap past length 10, rather than allocating 10.9 GB, through guard_bitmap_sizes, which the two tests that guarded the sizes each in their own words now share. The docstring of the test with an index above 2**63 says that it is kept in a list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #209, round 7, its suggestion 6. - One helper, _kept_as_bitmap, tells the two forms apart for every reader of the labels, where five places tested isinstance(labels, (array, list)), the kind of test round 6 blocked on. It is a TypeGuard for bytes: mypy narrows only where a TypeGuard holds, and the indices' branches read the labels as any sequence of ints, so no cast is needed. - add_length names its iterator indexed_perms, and says that the second loop goes on where the first stopped, that its else is the case with no break, and why it drops the indices: they take about as much as the bitmap, so dropping them keeps the peak at two bitmaps. - _most_indices is _largest_sparse_good_count, for what it holds, and its docstring says why it is (n! - 1) // 64 and not n! // 64, which would let exactly one in 64 through when 64 divides n!. - _keep's docstring, and the comment on _labels, say who reads the labels and who keeps them. - _Lengths, which gives two Mappings their keys, is _LengthKeys, and the view's generators are _unranked_perms, _perms_skipping and _perms_with_label. Their comment says what was measured, not why. - The comment on _INDEX_TYPECODE says why it is Final, and the changelog that 8 bytes an index holds to length 20. - In the tests, LENGTH_6, SPARSE_6, DENSE_6 and LENGTH_8 are named for what they hold, SPARSE_8 is extremes(8), and a docstring says "one good permutation". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Henning, through the review of #209 (2026-10-25): "We do not want to bloat the readme with technical details". The BiSC section's paragraph on PropertyBitmaps now ends after the memory comparison: the details of the sparse form, the lists past length 20, and how a PropertyBitmaps is made and reused are left to the class's docstring and the changelog. The example after the paragraph, a doctest, is unchanged, and the README's 70 examples pass on CPython 3.11 and PyPy 3.11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
With a property as input,
auto_bisckept every good and every bad permutation up to its current length as lists ofPermobjects. At length 10 the bad ones are nearly all of S_10, which take 0.9 to 1.8 GB as lists, depending on the Python. Runs on sets whose learning reaches length 10 grew past 8 GB. Three such sets, from Dong and Xu's paper on fixed and pixed points, still stopped at an 8 GB cap.What
PropertyBitmaps(inpermuta.bisc) holds a property's values on the permutations, length by length, in the smaller of two forms:Perm.unrank(i, n), so a permutation's bit is itsrank()less the number of shorter permutations. This is the order in whichPerm.of_length(n)yields them.array("Q"), when fewer than one permutation in 64 of the length is good, so that they take less than the bitmap, but for the few percent an array allocates ahead. Three permutations of length 14 take 24 bytes, where the bitmap would take 10.9 GB. Past length 20, where an index can need more than 8 bytes, the indices are kept in a list, so there is no longest length. There a length's bitmap cannot be built, and its bad permutations, more thansys.maxsize, have nolen(), though they can be iterated over and tested within.add_length(n)labels a length with the property, keeping the indices while they are few. Once there are more than a length keeps as indices, it sets them in the bitmap and labels the rest of the length in it, a bit for each good permutation, so that its peak is two bitmaps.bitmaps(length to bitmap) is a read-only Mapping. It gives a length kept as a bitmap as it is kept, as bytes. It builds the bitmap of a length kept as indices each time it is read, soitems(),values()ordict()build every such length's bitmap, 10.9 GB for length 14; it builds none to answerin, to iterate over the lengths, or forlen().good_countsgives the number of good permutations of each length. No int is made of a whole bitmap: its bits are counted a chunk at a time.biscandauto_bisctake what they took before, and turn each input into aPropertyBitmapsat the start. From there, one code path reads the good and the bad permutations through its views.biscorauto_biscreads: inauto_bisc, those up to 8 at the start, and each longer one when it is first needed.bisc, or any other Mapping, is read for the lengthsbiscreads.ValueErrorsays how many are in both and how many in neither. A permutation given twice on one side counts once. Every stored set inpermuta/resources/biscpasses.PropertyBitmapslabels the lengths it lacks with its property, and keeps them for a later run.goodandbad, are each aMappingfrom lengths toCollections of permutations.len()andbool()read the count kept with the labels.intakes any sequence that is a permutation of the length, and reads its bit, or looks for its index by binary search.bisc.pyno longer starts with# type: ignore, so mypy checks it, and its new code is typed.bisc_subfunctions.pykeeps its errors ignored, but now with# mypy: ignore-errors, so that mypy can see its functions.bisc_subfunctions.pyis left to a pull request of its own.biscandauto_bischave return types, andpermuta/bisc/__init__.pyis checked too.PropertyBitmapsis exported frompermuta.bisc. The README's BiSC section describes it, with an example that runs as a doctest.Results
What
biscandauto_biscprint and return is byte for byte the same as on develop, on CPython 3.11 and PyPy 3.11, for every input kind compared:auto_biscgiven three properties, one of them growing past length 9;auto_biscgiven a list, a shuffled list, a tuple of dictionaries (with its lists in order and reversed), and two stored sets;biscgiven a list, a shuffled list, a property (with m <= n and with m > n), and a dictionary (with its lists in order and reversed);biscgiven three permutations of length 12, and three of length 14, with every length up to them kept as indices;biscgiven one permutation of length 21, and one of length 25.Malformed input is treated differently:
(0, 1)listed twice among the increasing permutations, develop's lists took length 2 as complete and found nothing; nowbiscfinds 10.ValueError. Before, it was taken as it was.ValueErrorthat names it.Permwhose values are not those of a permutation of its length, such as one written from 1, raises aValueErrorthat names it. Before, it was mined as it was given, which made no sense of it: for Av(231) written from 1,bisc(..., 3, 6)forbade all six patterns of length 3.auto_biscat once, with the message develop printed at the end of its run: "You need to add longer permutations to the dictionaries".The permutations are read in lexicographic order. So with a dictionary in another order, a message may name a different first permutation, and a pattern's shadings may come in another order. Three permutations of length 13, not given in lexicographic order, give the same patterns and shadings as on develop, in the order develop gives for the same three sorted. For the same reason, a list's order no longer changes the time either.
Speed and memory
bisc(stack-sortable, 3, 8)bisc(list, 3, 8)bisc(dict, 3, 8)bisc, 20 permutations of length 10bisc, 20 permutations of length 11bisc, 3 permutations of length 12bisc, 3 permutations of length 13bisc, 3 permutations of length 14auto_bisc(stack-sortable)auto_bisc(West-2-stack-sortable)auto_bisc(smooth)auto_bisc(list to length 8)auto_bisc(tuple to length 8)auto_bisc("smooth")auto_bisc("SimSun")auto_bisc, a property growing to length 10Each figure is a median of runs, each run in its own process, with develop's and this branch's interleaved, on an idle Mac at normal priority: ten runs on PyPy 3.11 (8.0.1), five on CPython 3.11.11. The seconds are wall time, which the CPU time matched, and the memory is the peak resident. The last column is tracemalloc's peak on CPython, from a run of its own. CPython 3.14 was not run. The figures are for
b746f89, run with-I; the commits after it change the time only by the check of a listed permutation's values (below).auto_bisc(smooth),auto_bisc(West-2-stack-sortable)and a list given toauto_biscfaster.jit-summary), and the same code can run in a slower mode from one run or session to the next, and with-Ior without it. So its figures move more than CPython's. In ten runs,auto_bisc(smooth)took 1.09 to 1.37 s here, and 1.19 to 1.35 s on develop."SimSun", and 22% for"smooth". For a list or dictionary given tobisc, it shows only where the mining is short. For the Baxter permutations to length 9 with m = 3, which leaveminenothing to do, the review measured 0.006 s on develop and 0.14 s onb746f89, on CPython. With m = 4, develop and this branch are within the runs' noise of each other. Henning accepted these costs (2026-10-24).b746f89, costs about a tenth ofrank()'s time on CPython and a third of it on PyPy. Againstb746f89, in five runs each at background priority, it added about 5% to the tuple to length 8 on CPython, 1% to"smooth", and an eighth to the 0.65 s of the Baxter permutations with m = 3. On PyPy it added a third to those (0.27 s against 0.20 s), and the medians of the other cases were 0.79 to 1.07 ofb746f89's, within PyPy's noise.auto_bisctakes a fifth of develop's time on CPython and a half on PyPy, since develop took its complement withperm not ina list.auto_bisc(stack-sortable). A tuple or a stored set keeps its input's own lists, so its peak is develop's.Tests
tests/bisc/test_property_bitmaps.pycovers:Perm.unrankandPerm.rank;biscandauto_bisc, with a spy onmine()for the bitmaps it is given;ValueErrorfor a tuple, and every stored set;auto_biscgrowing past the labeled lengths.tests/bisc/test_bisc_inputs.pycovers howbiscandauto_bisctake each kind of input:report=True;auto_biscprints for each input kind, and where it stops;Its expected values are the answers from before this change, or follow from the mathematics.
tests/bisc/test_sparse_storage.pycovers the two forms of a length's labels:add_lengthswitching to a bitmap in the middle of a length, or staying with indices one short of the threshold, or at the very end of a length; dropping its indices once it has the bitmap (CPython only); a property that raises in it, before or after the switch, leaving the length as it was; and labels replaced from one form to the other;bitmapsgiving bytes, the same object for a length kept as a bitmap, and a round trip through it;bisc, and a view with nothing to yield, which reads no labels;biscon one permutation of length 21 or 25, against develop's answer; a length past 20 counted, iterated, tested withinand true, and its bitmap refused; the lengths past n kept as empty lists past 20, with m = 25; an empty length 25; and the bad view of length 20 read a few permutations at a time;biscand through aPropertyBitmaps: ones whose rank is another permutation's, such as(0, 0, 0)and ones written from 1, and ones whose rank is outside their length's;The existing tests pass unchanged, and so do all 70 examples in the README.
Review
Reviewed before submission, over nine rounds, by an automated reviewer. The last round changed only the README, and its checks ran on it; the rest ran on the commit before it, whose code and tests are the same:
permuta/bisc/bisc.pyandpermuta/bisc/__init__.py), pylint and black, and Permuta's tests on CPython 3.14 (695 passed) and PyPy 3.11 (693 passed, and the two CPython-only tests skipped). Tilings' tests, which do not importpermuta.bisc, passed against this branch on both in an earlier round (3719 each).biscandauto_biscreturn and print is the same, except for the malformed inputs listed above.PropertyBitmapseach, built every way in, lengths 0 to 8, on both Pythons.auto_bisctook 6 to 7% longer than on develop, and the inputs that are ranked cost what is stated above. On PyPy 3.11 every input took develop's time or less, but for those that are ranked;auto_bisc(smooth)took 1.36 s against develop's 1.60 s, over 40 paired runs.AI assistance: this change was written with an AI coding agent, and reviewed before submission, over nine rounds, by an automated reviewer (Claude Code, Opus 5.5, with Sonnet and Haiku subagents), which ran the checks above. Henning Ulfarsson checked it and opened it.