Skip to content

Add PropertyBitmaps: bisc and auto_bisc keep their input as bitmaps, or as indices where few are good - #209

Open
ulfarsson wants to merge 38 commits into
developfrom
bisc-labelled-perms
Open

ulfarsson wants to merge 38 commits into
developfrom
bisc-labelled-perms

Conversation

@ulfarsson

@ulfarsson ulfarsson commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Why

With a property as input, auto_bisc kept 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, 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 (in permuta.bisc) holds a property's values on the permutations, length by length, in the smaller of two forms:
    • a bitmap, (n! + 7) // 8 bytes (S_10's is 454 KB). Bit i of length n is the label of Perm.unrank(i, n), so a permutation's bit is its rank() less the number of shorter permutations. This is the order in which Perm.of_length(n) yields them.
    • the sorted indices of the good permutations, 8 bytes each in an 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 than sys.maxsize, have no len(), though they can be iterated over and tested with in.
    • The form is chosen length by length, by its count of good permutations, since a property's share of them can rise with the length. Every way in chooses it. 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.
    • It can be made from a property, from given bitmaps, or from both. A given bitmap must be bytes-like and of its length's size, with no bit set past the length's permutations, and it is turned into indices when they are the smaller.
    • 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, so items(), values() or dict() build every such length's bitmap, 10.9 GB for length 14; it builds none to answer in, to iterate over the lengths, or for len(). good_counts gives 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.
  • Every input becomes one. bisc and auto_bisc take what they took before, and turn each input into a PropertyBitmaps at the start. From there, one code path reads the good and the bad permutations through its views.
    • A list's permutations are the good ones, and a length with none listed is empty.
    • A property labels each length bisc or auto_bisc reads: in auto_bisc, those up to 8 at the start, and each longer one when it is first needed.
    • A dictionary given to bisc, or any other Mapping, is read for the lengths bisc reads.
    • For a tuple of two dictionaries, or a stored set, the good and the bad permutations of each length must not overlap and must together be all of that length's permutations. Otherwise a ValueError says how many are in both and how many in neither. A permutation given twice on one side counts once. Every stored set in permuta/resources/bisc passes.
    • A PropertyBitmaps labels the lengths it lacks with its property, and keeps them for a later run.
  • Its views, good and bad, are each a Mapping from lengths to Collections of permutations.
    • Iterating over a length yields its permutations in lexicographic order. For a length kept as indices, the good permutations are unranked from them, and the bad ones are generated with the good indices skipped in step. From a bitmap, when at most one in 16 is wanted, the view unranks them; otherwise it generates the length and tests each bit. Each of these is a small generator of its own.
    • len() and bool() read the count kept with the labels. in takes any sequence that is a permutation of the length, and reads its bit, or looks for its index by binary search.
  • Typing: bisc.py no longer starts with # type: ignore, so mypy checks it, and its new code is typed.
    • bisc_subfunctions.py keeps its errors ignored, but now with # mypy: ignore-errors, so that mypy can see its functions.
    • Typing bisc_subfunctions.py is left to a pull request of its own.
    • bisc and auto_bisc have return types, and permuta/bisc/__init__.py is checked too.

PropertyBitmaps is exported from permuta.bisc. The README's BiSC section describes it, with an example that runs as a doctest.

Results

What bisc and auto_bisc print 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_bisc given three properties, one of them growing past length 9;
  • auto_bisc given a list, a shuffled list, a tuple of dictionaries (with its lists in order and reversed), and two stored sets;
  • bisc given a list, a shuffled list, a property (with m <= n and with m > n), and a dictionary (with its lists in order and reversed);
  • bisc given three permutations of length 12, and three of length 14, with every length up to them kept as indices;
  • bisc given one permutation of length 21, and one of length 25.

Malformed input is treated differently:

  • A permutation listed twice counts once. With (0, 1) listed twice among the increasing permutations, develop's lists took length 2 as complete and found nothing; now bisc finds 10.
  • A tuple or stored set whose good and bad permutations overlap, or miss one, raises a ValueError. Before, it was taken as it was.
  • A permutation under another length's key raises a ValueError that names it.
  • A Perm whose values are not those of a permutation of its length, such as one written from 1, raises a ValueError that 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.
  • A tuple whose bad dictionary stops before length 7 stops auto_bisc at 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

Input CPython 3.11, develop CPython 3.11, #209 PyPy 3.11, develop PyPy 3.11, #209 CPython, tracemalloc's peak: develop, #209
bisc(stack-sortable, 3, 8) 0.49 s, 41 MB 0.50 s, 40 MB 0.66 s, 100 MB 0.70 s, 103 MB 0.6 MB, 0.2 MB
bisc(list, 3, 8) 0.50 s, 41 MB 0.50 s, 41 MB 0.64 s, 98 MB 0.66 s, 100 MB 0.6 MB, 0.8 MB
bisc(dict, 3, 8) 0.50 s, 41 MB 0.51 s, 42 MB 0.64 s, 98 MB 0.66 s, 100 MB 0.6 MB, 0.8 MB
bisc, 20 permutations of length 10 0.67 s, 41 MB 0.68 s, 42 MB 0.46 s, 90 MB 0.47 s, 90 MB 1.2 MB, 1.2 MB
bisc, 20 permutations of length 11 1.15 s, 42 MB 1.14 s, 42 MB 0.58 s, 92 MB 0.58 s, 92 MB 1.4 MB, 1.4 MB
bisc, 3 permutations of length 12 4.12 s, 42 MB 4.15 s, 42 MB 1.20 s, 100 MB 1.19 s, 102 MB 1.4 MB, 1.4 MB
bisc, 3 permutations of length 13 2.99 s, 42 MB 3.04 s, 42 MB 1.05 s, 99 MB 0.98 s, 97 MB 1.3 MB, 1.4 MB
bisc, 3 permutations of length 14 2.54 s, 42 MB 2.53 s, 42 MB 0.79 s, 90 MB 0.88 s, 95 MB 1.4 MB, 1.4 MB
auto_bisc(stack-sortable) 0.60 s, 55 MB 0.63 s, 41 MB 1.25 s, 132 MB 1.28 s, 119 MB 13.8 MB, 0.3 MB
auto_bisc(West-2-stack-sortable) 0.89 s, 55 MB 0.93 s, 41 MB 2.03 s, 150 MB 1.75 s, 113 MB 13.8 MB, 0.2 MB
auto_bisc(smooth) 0.85 s, 55 MB 0.89 s, 41 MB 1.31 s, 128 MB 1.10 s, 113 MB 13.9 MB, 0.3 MB
auto_bisc(list to length 8) 2.87 s, 55 MB 0.63 s, 42 MB 2.00 s, 125 MB 1.10 s, 115 MB 13.8 MB, 0.9 MB
auto_bisc(tuple to length 8) 0.89 s, 55 MB 1.02 s, 55 MB 1.88 s, 138 MB 1.78 s, 136 MB 13.8 MB, 13.9 MB
auto_bisc("smooth") 0.59 s, 62 MB 0.72 s, 62 MB 1.37 s, 144 MB 1.49 s, 142 MB 19.2 MB, 19.2 MB
auto_bisc("SimSun") 0.84 s, 62 MB 0.96 s, 62 MB 1.39 s, 130 MB 1.35 s, 136 MB 19.1 MB, 19.1 MB
auto_bisc, a property growing to length 10 2.94 s, 172 MB 2.78 s, 41 MB 1.44 s, 196 MB 1.41 s, 112 MB 125.1 MB, 0.5 MB

Each 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).

  • Against develop. On CPython every input is within 5% of develop, but for the tuple and the stored sets below, and two that are faster. On PyPy every input is within about 10%, with auto_bisc(smooth), auto_bisc(West-2-stack-sortable) and a list given to auto_bisc faster.
  • PyPy's slow mode. PyPy spends about 0.8 s of these one- to two-second runs tracing and compiling (its jit-summary), and the same code can run in a slower mode from one run or session to the next, and with -I or 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.
  • The cost. Every permutation given in a list, a dictionary, a tuple or a stored set is ranked, and its values checked, to make its bit or its index; a tuple and a stored set are also checked for overlap. On CPython that costs about 15% for the tuple to length 8 and for "SimSun", and 22% for "smooth". For a list or dictionary given to bisc, it shows only where the mining is short. For the Baxter permutations to length 9 with m = 3, which leave mine nothing to do, the review measured 0.006 s on develop and 0.14 s on b746f89, on CPython. With m = 4, develop and this branch are within the runs' noise of each other. Henning accepted these costs (2026-10-24).
  • The check of the values, added after b746f89, costs about a tenth of rank()'s time on CPython and a third of it on PyPy. Against b746f89, 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 of b746f89's, within PyPy's noise.
  • Faster. A list given to auto_bisc takes a fifth of develop's time on CPython and a half on PyPy, since develop took its complement with perm not in a list.
  • Memory. tracemalloc's peak falls where develop kept lists: from 125 MB to 0.5 MB for a property growing to length 10, and from 13.8 MB to 0.3 MB for auto_bisc(stack-sortable). A tuple or a stored set keeps its input's own lists, so its peak is develop's.
  • A short list of long permutations runs as on develop, in the time and memory of the mining, with each length up to them kept as indices.

Tests

  • tests/bisc/test_property_bitmaps.py covers:

    • the bit order, against Perm.unrank and Perm.rank;
    • the views, against lists and as a Mapping of Collections, both when they generate and when they unrank;
    • round trips through bitmaps, and the refusal of malformed ones;
    • each input kind of bisc and auto_bisc, with a spy on mine() for the bitmaps it is given;
    • a list with a length missing and with a duplicate, and a property with m > n;
    • the ValueError for a tuple, and every stored set;
    • auto_bisc growing past the labeled lengths.
  • tests/bisc/test_bisc_inputs.py covers how bisc and auto_bisc take each kind of input:

    • the default n of each, m > n, and m or n zero;
    • the empty set, and report=True;
    • what auto_bisc prints for each input kind, and where it stops;
    • growth that would label a length it was given;
    • where a view starts to unrank.

    Its expected values are the answers from before this change, or follow from the mathematics.

  • tests/bisc/test_sparse_storage.py covers the two forms of a length's labels:

    • both forms against brute force, for every view, the count and the bitmap, and for every way in (a list or a mapping, shuffled and with permutations given twice, a tuple, bitmaps and a property), at lengths 0 to 8. The threshold is set to one in 2 as well as the default one in 64, and the good sets have no permutation, all, as many as the threshold, one fewer, and a random number on each side of it;
    • add_length switching 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;
    • a bitmap given as a bytearray or a memoryview, of any format, shape or stride, and changed afterwards; bitmaps giving bytes, the same object for a length kept as a bitmap, and a round trip through it;
    • a permutation listed many times, and a tuple's overlap counted in every pair of forms, lists of indices included; the first and the last permutation of a length kept as indices, a view of one iterated twice or given to bisc, and a view with nothing to yield, which reads no labels;
    • no bitmap built for a length kept as indices unless its bitmap is asked for, and none sized at all for a length of 14 or for the lengths past n, checked through the code's structure, so that this runs on PyPy too;
    • the last permutation of lengths 20, 21, 25, 100 and 200, an index of length 21 above 2**63, and bisc on one permutation of length 21 or 25, against develop's answer; a length past 20 counted, iterated, tested with in and 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;
    • sequences that are not permutations, refused in both forms, through bisc and through a PropertyBitmaps: 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;
    • three permutations of length 14 giving develop's answer, with tracemalloc's peak under 1 MB (CPython only). A fault that would size a bitmap past length 10 fails it at once, rather than allocating one.
  • 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:

  • flake8, mypy (which now reads permuta/bisc/bisc.py and permuta/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 import permuta.bisc, passed against this branch on both in an earlier round (3719 each).
  • A comparison with develop on CPython 3.14 and PyPy 3.11 for every input kind, permutations of lengths 21 to 30 among them: what bisc and auto_bisc return and print is the same, except for the malformed inputs listed above.
  • A brute-force check of both forms and their views, at four thresholds and with the list form forced at short lengths: 900 PropertyBitmaps each, built every way in, lengths 0 to 8, on both Pythons.
  • Mutation testing of the new and changed functions, and faults made by hand in each new branch: every fault that changes a result fails a test.
  • Timing against develop, interleaved, on both Pythons, with PyPy's runs paired by hash seed. On CPython 3.14 a property given to auto_bisc took 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.

ulfarsson and others added 3 commits October 18, 2026 18:45
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>
@ulfarsson
ulfarsson requested a review from jaypantone October 18, 2026 19:44
ulfarsson and others added 11 commits October 18, 2026 19:50
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>
Comment thread permuta/bisc/bisc.py Outdated
Comment thread permuta/bisc/bisc.py Outdated
Comment thread CHANGELOG.md
Comment thread README.rst Outdated
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>
Comment thread permuta/bisc/bisc.py Outdated
ulfarsson and others added 12 commits October 23, 2026 15:13
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>
ulfarsson and others added 10 commits October 24, 2026 17:57
… 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>
@ulfarsson ulfarsson changed the title Bisc labelled perms Add PropertyBitmaps: bisc and auto_bisc keep their input as bitmaps, or as indices where few are good Oct 25, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants