From 348a27c366e2bbd20f53f84592752f2694f1d70f Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 18 Oct 2026 16:43:20 +0000 Subject: [PATCH 01/12] Add LabelledPerms: auto_bisc input with labels kept as bitmaps 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 --- CHANGELOG.md | 8 ++ permuta/bisc/bisc.py | 132 +++++++++++++++++++++++++++++- tests/bisc/test_labelled_perms.py | 81 ++++++++++++++++++ 3 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 tests/bisc/test_labelled_perms.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 50ecb90..e2cd282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +### Added +- `LabelledPerms` in `permuta.bisc.bisc`: labels of permutations kept as one bitmap + per length (lexicographic order), made from a property, given as bitmaps, or both. + `auto_bisc` and `bisc` accept it as input. +- `auto_bisc(prop, labels="bitmaps")`: keeps a property's labels as bitmaps instead + of lists of permutations, for the same result in far less memory at longer + lengths. `labels="lists"`, the default, is the previous behaviour. + ### Fixed - Fixed bug in `Perm.rtlmax_ltrmin_decomposition` and fixed associated tests. diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index e2b6549..28fe58d 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -4,6 +4,7 @@ import os import types from collections import defaultdict +from math import factorial from permuta.bisc.bisc_subfunctions import ( forb, @@ -36,12 +37,19 @@ def bisc(A, m, n=None, report=False): elif isinstance(A, dict): D = A + elif isinstance(A, LabelledPerms): + D = A.good + + elif isinstance(A, _LengthViews): + D = A + else: print( """BiSC can accept inputs: 1. A list of permutations 2. A property of permutations - 3. A dictionary of lengths pointing to permutations""" + 3. A dictionary of lengths pointing to permutations + 4. A LabelledPerms (labels kept as bitmaps)""" ) assert False @@ -54,7 +62,20 @@ def bisc(A, m, n=None, report=False): return SG -def auto_bisc(prop): +def auto_bisc(prop, labels="lists"): + """ + labels says how the labels of a property (a function) are kept: + "lists" (the default, as before): the good and the bad permutations of + each length as lists of Perm objects; + "bitmaps": one bitmap per length (LabelledPerms), for the same result in + far less memory, at the cost of generating a length's permutations again + each time it is read. + A LabelledPerms can also be given as prop directly. + """ + if labels not in ("lists", "bitmaps"): + raise ValueError('labels must be "lists" or "bitmaps"') + if labels == "bitmaps" and isinstance(prop, types.FunctionType): + prop = LabelledPerms(prop) L = 8 # Want to sanity check on at least S8, and one above n n = 4 m = 2 @@ -88,6 +109,20 @@ def auto_bisc(prop): else: B[i].append(perm) + elif isinstance(prop, LabelledPerms): + # Labels kept as one bitmap per length: A and B are views of the good + # and the bad permutations, generated length by length when they are + # read, instead of lists. Lengths not given are labelled with the + # LabelledPerms's property, if it has one + labelled = prop + for i in range(L + 1): + if i not in labelled.bits: + if labelled.prop is None: + print("You should have labels up to length at least 8") + return + labelled.add_length(i) + A, B = labelled.good, labelled.bad + elif ( isinstance(prop, tuple) and isinstance(prop[0], dict) @@ -229,6 +264,15 @@ def auto_bisc(prop): else: B[i].append(perm) + elif isinstance(prop, LabelledPerms): + for i in range(oldL + 1, L + 1): + if i not in labelled.bits: + if labelled.prop is None: + print("You need to add labels of longer permutations") + return + print("Adding perms of length {}".format(i)) + labelled.add_length(i) + elif isinstance(prop, tuple) and L > min(max(A.keys()), max(B.keys())): print("You need to add longer permutations to the dictionaries") return @@ -269,6 +313,90 @@ def auto_bisc(prop): return +class LabelledPerms: + """Labels of permutations, one bitmap per length, as input to auto_bisc + and bisc: which permutations of each length satisfy a property (good) and + which do not (bad). + + Bit i of length n is the i-th permutation of Perm.of_length(n) + (lexicographic order), in byte i // 8, from the low bit. The bitmaps can + be given (bitmaps={n: bytes}), made from a property (add_length), or both: + auto_bisc labels the lengths it needs and is not given with prop, and + stops, as for a tuple of dictionaries, when there is no prop. + + auto_bisc reads its good and bad permutations only length by length: + iterating over a length, counting it, asking whether a length is there. + The views good and bad do that from the bitmaps, generating a length's + permutations again each time it is iterated, in the same order as the + lists a property input makes. A bitmap of S_10 is 454 KB; S_10 as a list + of Perm objects is about 0.9 GB. + """ + + def __init__(self, prop=None, bitmaps=None): + self.prop = prop + self.bits = {} + self.counts = {} + for n, data in (bitmaps or {}).items(): + data = bytes(data) + if len(data) != (factorial(n) + 7) // 8: + raise ValueError("A bitmap of length {} has the wrong size".format(n)) + self.bits[n] = data + self.counts[n] = sum(bin(b).count("1") for b in data) + self.good = _LengthViews(self, True) + self.bad = _LengthViews(self, False) + + def add_length(self, n): + bits = bytearray((factorial(n) + 7) // 8) + count = 0 + for i, perm in enumerate(Perm.of_length(n)): + if self.prop(perm): + bits[i >> 3] |= 1 << (i & 7) + count += 1 + self.bits[n] = bits + self.counts[n] = count + + +class _LengthViews: + """length -> the good (or bad) permutations of that length, read-only, as + the dictionaries of lists auto_bisc builds from the other inputs.""" + + def __init__(self, labelled, good): + self.labelled = labelled + self.is_good = good + + def keys(self): + return self.labelled.bits.keys() + + def __contains__(self, n): + return n in self.labelled.bits + + def __getitem__(self, n): + if n not in self.labelled.bits: + raise KeyError(n) + return _PermsOfLength(self.labelled, n, self.is_good) + + +class _PermsOfLength: + def __init__(self, labelled, n, good): + self.labelled = labelled + self.n = n + self.want = 1 if good else 0 + + def __iter__(self): + bits = self.labelled.bits[self.n] + want = self.want + for i, perm in enumerate(Perm.of_length(self.n)): + if (bits[i >> 3] >> (i & 7)) & 1 == want: + yield perm + + def __len__(self): + good = self.labelled.counts[self.n] + return good if self.want else factorial(self.n) - good + + def __bool__(self): + return len(self) > 0 + + def create_bisc_input(N, prop): """ Create a dictionary, D, containing keys 1, 2, 3, ..., N. Each key points to diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py new file mode 100644 index 0000000..0ab7a6a --- /dev/null +++ b/tests/bisc/test_labelled_perms.py @@ -0,0 +1,81 @@ +import pytest + +from permuta.bisc.bisc import LabelledPerms, auto_bisc, bisc +from permuta.patterns.perm import Perm + + +def stack_sortable(perm): + return perm.avoids(Perm((1, 2, 0))) + + +def labelled(prop, top): + labels = LabelledPerms(prop) + for n in range(top + 1): + labels.add_length(n) + return labels + + +def test_views_match_lists(): + labels = labelled(stack_sortable, 6) + for n in range(7): + good = [perm for perm in Perm.of_length(n) if stack_sortable(perm)] + bad = [perm for perm in Perm.of_length(n) if not stack_sortable(perm)] + assert list(labels.good[n]) == good + assert list(labels.bad[n]) == bad + assert len(labels.good[n]) == len(good) + assert len(labels.bad[n]) == len(bad) + assert bool(labels.bad[n]) == bool(bad) + assert sorted(labels.good.keys()) == list(range(7)) + assert 6 in labels.bad and 7 not in labels.bad + with pytest.raises(KeyError): + _ = labels.good[7] + + +def test_bitmaps_round_trip(): + labels = labelled(stack_sortable, 6) + again = LabelledPerms(bitmaps=labels.bits) + for n in range(7): + assert list(again.good[n]) == list(labels.good[n]) + assert list(again.bad[n]) == list(labels.bad[n]) + with pytest.raises(ValueError): + LabelledPerms(bitmaps={3: b"\x00\x00"}) # 3! bits fit in one byte + + +def test_bisc_accepts_labelled_perms(): + expected = {3: {Perm((1, 2, 0)): [set()]}} + assert bisc(stack_sortable, 3, 5) == expected + assert bisc(labelled(stack_sortable, 5), 3, 5) == expected + + +def test_auto_bisc_labels_option(): + expected = {3: {Perm((1, 2, 0)): [set()]}} + assert auto_bisc(stack_sortable) == expected + assert auto_bisc(stack_sortable, labels="lists") == expected + assert auto_bisc(stack_sortable, labels="bitmaps") == expected + with pytest.raises(ValueError): + auto_bisc(stack_sortable, labels="sets") + + +def test_auto_bisc_labelled_perms_input(): + expected = {3: {Perm((1, 2, 0)): [set()]}} + assert auto_bisc(labelled(stack_sortable, 8)) == expected + # Given bitmaps and no property: the lengths auto_bisc needs must be there + bits = labelled(stack_sortable, 8).bits + assert auto_bisc(LabelledPerms(bitmaps=bits)) == expected + short = {n: bits[n] for n in range(6)} + assert auto_bisc(LabelledPerms(bitmaps=short)) is None + + +def test_west_2_stack_sortable_same_as_lists(): + def west_2(perm): + def stack_sort(word): + out, stack = [], [] + for x in word: + while stack and stack[-1] < x: + out.append(stack.pop()) + stack.append(x) + return out + stack[::-1] + + return stack_sort(stack_sort(list(perm))) == sorted(perm) + + assert auto_bisc(west_2, labels="bitmaps") == auto_bisc(west_2) From de4fa241f9952d9653ca11cd2f3fd760bf3d3ced Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 18 Oct 2026 18:48:36 +0000 Subject: [PATCH 02/12] Test auto_bisc's growth past the labelled lengths with LabelledPerms 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 --- tests/bisc/test_labelled_perms.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 0ab7a6a..6eb8347 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -1,3 +1,6 @@ +import contextlib +import io + import pytest from permuta.bisc.bisc import LabelledPerms, auto_bisc, bisc @@ -79,3 +82,43 @@ def stack_sort(word): return stack_sort(stack_sort(list(perm))) == sorted(perm) assert auto_bisc(west_2, labels="bitmaps") == auto_bisc(west_2) + + +def derangement_in_av_213_231_312(perm): + """One member at each even length, none at odd: auto_bisc keeps learning longer + patterns while each step stays cheap, so it soon needs more labels.""" + return perm.avoids(Perm((1, 0, 2)), Perm((1, 2, 0)), Perm((2, 0, 1))) and all( + value != index for index, value in enumerate(perm) + ) + + +def test_auto_bisc_stops_without_labels_it_needs(): + # Bitmaps up to length 8 and no property: auto_bisc needs length 9 and stops + bits = labelled(derangement_in_av_213_231_312, 8).bits + output = io.StringIO() + with contextlib.redirect_stdout(output): + assert auto_bisc(LabelledPerms(bitmaps=bits)) is None + assert "You need to add labels of longer permutations" in output.getvalue() + + +class ReachedLength9(Exception): + pass + + +def test_auto_bisc_labels_new_lengths_from_the_property(): + # When auto_bisc grows past the labelled lengths, it labels the new ones with + # the property; stopped at the first permutation of length 9, both approaches + # have printed the same + def prop(perm): + if len(perm) == 9: + raise ReachedLength9 + return derangement_in_av_213_231_312(perm) + + outputs = [] + for labels in ("lists", "bitmaps"): + output = io.StringIO() + with contextlib.redirect_stdout(output), pytest.raises(ReachedLength9): + auto_bisc(prop, labels=labels) + outputs.append(output.getvalue()) + assert outputs[0] == outputs[1] + assert "Adding perms of length 9" in outputs[1] From b2077689815f7eb6c1f26effcaae03eea8999738 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 18 Oct 2026 19:05:12 +0000 Subject: [PATCH 03/12] Export LabelledPerms from permuta.bisc and document it in the README 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 --- CHANGELOG.md | 2 +- README.rst | 19 ++++++++++++++++++- permuta/bisc/__init__.py | 10 +++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cd282..5ae259f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased ### Added -- `LabelledPerms` in `permuta.bisc.bisc`: labels of permutations kept as one bitmap +- `LabelledPerms` in `permuta.bisc`: labels of permutations kept as one bitmap per length (lexicographic order), made from a property, given as bitmaps, or both. `auto_bisc` and `bisc` accept it as input. - `auto_bisc(prop, labels="bitmaps")`: keeps a property's labels as bitmaps instead diff --git a/README.rst b/README.rst index 1277f39..2988a30 100644 --- a/README.rst +++ b/README.rst @@ -516,7 +516,24 @@ permutations that can be imported from ``permuta.bisc.perm_properties``, such as ``smooth``, ``forest-like``, ``baxter``, ``simsun``, ``quick_sortable``, etc. Both ``bisc`` and ``auto_bisc`` can accept input in the form of a property, -or a list of permutations (satisfying some property). +a list of permutations (satisfying some property), or a ``LabelledPerms``. + +``auto_bisc`` keeps the permutations satisfying a property, and those that do +not, as lists. For long runs it can keep them as one bitmap per length instead, +with the same result in far less memory: +``auto_bisc(Perm.stack_sortable, labels="bitmaps")``. Such labels can also be +given directly as a ``LabelledPerms``, made from a property, from bitmaps, or +both, and ``bisc`` accepts it too. + +.. code-block:: python + + >>> labels = LabelledPerms(Perm.stack_sortable) + >>> for n in range(6): + ... labels.add_length(n) + >>> len(labels.good[5]), len(labels.bad[5]) + (42, 78) + >>> bisc(labels, 3, 5) + {3: {Perm((1, 2, 0)): [set()]}} License ####### diff --git a/permuta/bisc/__init__.py b/permuta/bisc/__init__.py index 34bcdf8..acb5b85 100644 --- a/permuta/bisc/__init__.py +++ b/permuta/bisc/__init__.py @@ -1,6 +1,13 @@ # type: ignore -from .bisc import auto_bisc, bisc, create_bisc_input, read_bisc_file, write_bisc_files +from .bisc import ( + LabelledPerms, + auto_bisc, + bisc, + create_bisc_input, + read_bisc_file, + write_bisc_files, +) from .bisc_subfunctions import ( patterns_suffice_for_bad, patterns_suffice_for_good, @@ -10,6 +17,7 @@ ) __all__ = [ + "LabelledPerms", "auto_bisc", "bisc", "create_bisc_input", From f449beb2268fc67133ec0f76cb65aded2cc9b06e Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Sun, 18 Oct 2026 19:48:22 +0000 Subject: [PATCH 04/12] Define LabelledPerms' bit order by Perm.unrank 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 --- permuta/bisc/bisc.py | 13 ++++++++----- tests/bisc/test_labelled_perms.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index 28fe58d..088266e 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -318,11 +318,14 @@ class LabelledPerms: and bisc: which permutations of each length satisfy a property (good) and which do not (bad). - Bit i of length n is the i-th permutation of Perm.of_length(n) - (lexicographic order), in byte i // 8, from the low bit. The bitmaps can - be given (bitmaps={n: bytes}), made from a property (add_length), or both: - auto_bisc labels the lengths it needs and is not given with prop, and - stops, as for a tuple of dictionaries, when there is no prop. + Bit i of length n is the label of Perm.unrank(i, n), in byte i // 8, from + the low bit; so a permutation perm of length n is at bit + perm.rank() - sum(factorial(k) for k in range(n)). This is the order in + which Perm.of_length(n) yields the permutations (lexicographic), which the + views use to iterate. The bitmaps can be given (bitmaps={n: bytes}), made + from a property (add_length), or both: auto_bisc labels the lengths it + needs and is not given with prop, and stops, as for a tuple of + dictionaries, when there is no prop. auto_bisc reads its good and bad permutations only length by length: iterating over a length, counting it, asking whether a length is there. diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 6eb8347..0a7a2f9 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -1,5 +1,6 @@ import contextlib import io +from math import factorial import pytest @@ -34,6 +35,20 @@ def test_views_match_lists(): _ = labels.good[7] +def test_bit_order_is_perm_unrank(): + # Bit i of length n is the label of Perm.unrank(i, n), and the permutation + # perm is at bit perm.rank() minus the number of shorter permutations + labels = labelled(stack_sortable, 6) + shorter = 0 + for n in range(7): + bits = labels.bits[n] + for i in range(factorial(n)): + perm = Perm.unrank(i, n) + assert perm.rank() - shorter == i + assert bool(bits[i >> 3] >> (i & 7) & 1) == stack_sortable(perm) + shorter += factorial(n) + + def test_bitmaps_round_trip(): labels = labelled(stack_sortable, 6) again = LabelledPerms(bitmaps=labels.bits) From 9332c71d6051d8930807cbbb0da5c93b137d5cd5 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Wed, 21 Oct 2026 13:35:14 +0000 Subject: [PATCH 05/12] Name LabelledPerms' variables by what they hold 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 --- README.rst | 4 +- permuta/bisc/bisc.py | 64 ++++++++++++++-------------- tests/bisc/test_labelled_perms.py | 70 +++++++++++++++---------------- 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/README.rst b/README.rst index 2988a30..023a798 100644 --- a/README.rst +++ b/README.rst @@ -528,8 +528,8 @@ both, and ``bisc`` accepts it too. .. code-block:: python >>> labels = LabelledPerms(Perm.stack_sortable) - >>> for n in range(6): - ... labels.add_length(n) + >>> for length in range(6): + ... labels.add_length(length) >>> len(labels.good[5]), len(labels.bad[5]) (42, 78) >>> bisc(labels, 3, 5) diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index 088266e..4e4cc69 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -116,7 +116,7 @@ def auto_bisc(prop, labels="lists"): # LabelledPerms's property, if it has one labelled = prop for i in range(L + 1): - if i not in labelled.bits: + if i not in labelled.bitmaps: if labelled.prop is None: print("You should have labels up to length at least 8") return @@ -266,7 +266,7 @@ def auto_bisc(prop, labels="lists"): elif isinstance(prop, LabelledPerms): for i in range(oldL + 1, L + 1): - if i not in labelled.bits: + if i not in labelled.bitmaps: if labelled.prop is None: print("You need to add labels of longer permutations") return @@ -337,26 +337,28 @@ class LabelledPerms: def __init__(self, prop=None, bitmaps=None): self.prop = prop - self.bits = {} + self.bitmaps = {} self.counts = {} - for n, data in (bitmaps or {}).items(): - data = bytes(data) - if len(data) != (factorial(n) + 7) // 8: - raise ValueError("A bitmap of length {} has the wrong size".format(n)) - self.bits[n] = data - self.counts[n] = sum(bin(b).count("1") for b in data) + for length, bitmap in (bitmaps or {}).items(): + bitmap = bytes(bitmap) + if len(bitmap) != (factorial(length) + 7) // 8: + raise ValueError( + "A bitmap of length {} has the wrong size".format(length) + ) + self.bitmaps[length] = bitmap + self.counts[length] = sum(bin(byte).count("1") for byte in bitmap) self.good = _LengthViews(self, True) self.bad = _LengthViews(self, False) - def add_length(self, n): - bits = bytearray((factorial(n) + 7) // 8) + def add_length(self, length): + bitmap = bytearray((factorial(length) + 7) // 8) count = 0 - for i, perm in enumerate(Perm.of_length(n)): + for i, perm in enumerate(Perm.of_length(length)): if self.prop(perm): - bits[i >> 3] |= 1 << (i & 7) + bitmap[i >> 3] |= 1 << (i & 7) count += 1 - self.bits[n] = bits - self.counts[n] = count + self.bitmaps[length] = bitmap + self.counts[length] = count class _LengthViews: @@ -368,33 +370,33 @@ def __init__(self, labelled, good): self.is_good = good def keys(self): - return self.labelled.bits.keys() + return self.labelled.bitmaps.keys() - def __contains__(self, n): - return n in self.labelled.bits + def __contains__(self, length): + return length in self.labelled.bitmaps - def __getitem__(self, n): - if n not in self.labelled.bits: - raise KeyError(n) - return _PermsOfLength(self.labelled, n, self.is_good) + def __getitem__(self, length): + if length not in self.labelled.bitmaps: + raise KeyError(length) + return _PermsOfLength(self.labelled, length, self.is_good) class _PermsOfLength: - def __init__(self, labelled, n, good): + def __init__(self, labelled, length, good): self.labelled = labelled - self.n = n - self.want = 1 if good else 0 + self.length = length + self.wanted_bit = 1 if good else 0 def __iter__(self): - bits = self.labelled.bits[self.n] - want = self.want - for i, perm in enumerate(Perm.of_length(self.n)): - if (bits[i >> 3] >> (i & 7)) & 1 == want: + bitmap = self.labelled.bitmaps[self.length] + wanted_bit = self.wanted_bit + for i, perm in enumerate(Perm.of_length(self.length)): + if (bitmap[i >> 3] >> (i & 7)) & 1 == wanted_bit: yield perm def __len__(self): - good = self.labelled.counts[self.n] - return good if self.want else factorial(self.n) - good + good_count = self.labelled.counts[self.length] + return good_count if self.wanted_bit else factorial(self.length) - good_count def __bool__(self): return len(self) > 0 diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 0a7a2f9..4e03f06 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -12,23 +12,23 @@ def stack_sortable(perm): return perm.avoids(Perm((1, 2, 0))) -def labelled(prop, top): +def labelled(prop, max_length): labels = LabelledPerms(prop) - for n in range(top + 1): - labels.add_length(n) + for length in range(max_length + 1): + labels.add_length(length) return labels def test_views_match_lists(): labels = labelled(stack_sortable, 6) - for n in range(7): - good = [perm for perm in Perm.of_length(n) if stack_sortable(perm)] - bad = [perm for perm in Perm.of_length(n) if not stack_sortable(perm)] - assert list(labels.good[n]) == good - assert list(labels.bad[n]) == bad - assert len(labels.good[n]) == len(good) - assert len(labels.bad[n]) == len(bad) - assert bool(labels.bad[n]) == bool(bad) + for length in range(7): + good = [perm for perm in Perm.of_length(length) if stack_sortable(perm)] + bad = [perm for perm in Perm.of_length(length) if not stack_sortable(perm)] + assert list(labels.good[length]) == good + assert list(labels.bad[length]) == bad + assert len(labels.good[length]) == len(good) + assert len(labels.bad[length]) == len(bad) + assert bool(labels.bad[length]) == bool(bad) assert sorted(labels.good.keys()) == list(range(7)) assert 6 in labels.bad and 7 not in labels.bad with pytest.raises(KeyError): @@ -39,22 +39,22 @@ def test_bit_order_is_perm_unrank(): # Bit i of length n is the label of Perm.unrank(i, n), and the permutation # perm is at bit perm.rank() minus the number of shorter permutations labels = labelled(stack_sortable, 6) - shorter = 0 - for n in range(7): - bits = labels.bits[n] - for i in range(factorial(n)): - perm = Perm.unrank(i, n) - assert perm.rank() - shorter == i - assert bool(bits[i >> 3] >> (i & 7) & 1) == stack_sortable(perm) - shorter += factorial(n) + shorter_count = 0 + for length in range(7): + bitmap = labels.bitmaps[length] + for i in range(factorial(length)): + perm = Perm.unrank(i, length) + assert perm.rank() - shorter_count == i + assert bool(bitmap[i >> 3] >> (i & 7) & 1) == stack_sortable(perm) + shorter_count += factorial(length) def test_bitmaps_round_trip(): labels = labelled(stack_sortable, 6) - again = LabelledPerms(bitmaps=labels.bits) - for n in range(7): - assert list(again.good[n]) == list(labels.good[n]) - assert list(again.bad[n]) == list(labels.bad[n]) + round_trip = LabelledPerms(bitmaps=labels.bitmaps) + for length in range(7): + assert list(round_trip.good[length]) == list(labels.good[length]) + assert list(round_trip.bad[length]) == list(labels.bad[length]) with pytest.raises(ValueError): LabelledPerms(bitmaps={3: b"\x00\x00"}) # 3! bits fit in one byte @@ -78,21 +78,21 @@ def test_auto_bisc_labelled_perms_input(): expected = {3: {Perm((1, 2, 0)): [set()]}} assert auto_bisc(labelled(stack_sortable, 8)) == expected # Given bitmaps and no property: the lengths auto_bisc needs must be there - bits = labelled(stack_sortable, 8).bits - assert auto_bisc(LabelledPerms(bitmaps=bits)) == expected - short = {n: bits[n] for n in range(6)} - assert auto_bisc(LabelledPerms(bitmaps=short)) is None + bitmaps = labelled(stack_sortable, 8).bitmaps + assert auto_bisc(LabelledPerms(bitmaps=bitmaps)) == expected + too_short = {length: bitmaps[length] for length in range(6)} + assert auto_bisc(LabelledPerms(bitmaps=too_short)) is None def test_west_2_stack_sortable_same_as_lists(): def west_2(perm): def stack_sort(word): - out, stack = [], [] - for x in word: - while stack and stack[-1] < x: - out.append(stack.pop()) - stack.append(x) - return out + stack[::-1] + output, stack = [], [] + for entry in word: + while stack and stack[-1] < entry: + output.append(stack.pop()) + stack.append(entry) + return output + stack[::-1] return stack_sort(stack_sort(list(perm))) == sorted(perm) @@ -109,10 +109,10 @@ def derangement_in_av_213_231_312(perm): def test_auto_bisc_stops_without_labels_it_needs(): # Bitmaps up to length 8 and no property: auto_bisc needs length 9 and stops - bits = labelled(derangement_in_av_213_231_312, 8).bits + bitmaps = labelled(derangement_in_av_213_231_312, 8).bitmaps output = io.StringIO() with contextlib.redirect_stdout(output): - assert auto_bisc(LabelledPerms(bitmaps=bits)) is None + assert auto_bisc(LabelledPerms(bitmaps=bitmaps)) is None assert "You need to add labels of longer permutations" in output.getvalue() From 0099214e8538b6b9b6a9ce15139df51dfe16cd77 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:16:16 +0000 Subject: [PATCH 06/12] Make LabelledPerms and its tests easier to read 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 --- CHANGELOG.md | 3 +- README.rst | 16 +++--- permuta/bisc/bisc.py | 81 ++++++++++++++++----------- tests/bisc/test_labelled_perms.py | 91 +++++++++++++------------------ 4 files changed, 100 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae259f..db41f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. `auto_bisc` and `bisc` accept it as input. - `auto_bisc(prop, labels="bitmaps")`: keeps a property's labels as bitmaps instead of lists of permutations, for the same result in far less memory at longer - lengths. `labels="lists"`, the default, is the previous behaviour. + lengths (the permutations of length 10 take about 0.9 GB as lists, 454 KB as a + bitmap). `labels="lists"`, the default, is the previous behaviour. ### Fixed - Fixed bug in `Perm.rtlmax_ltrmin_decomposition` and fixed associated tests. diff --git a/README.rst b/README.rst index 023a798..7b934c2 100644 --- a/README.rst +++ b/README.rst @@ -521,18 +521,20 @@ a list of permutations (satisfying some property), or a ``LabelledPerms``. ``auto_bisc`` keeps the permutations satisfying a property, and those that do not, as lists. For long runs it can keep them as one bitmap per length instead, with the same result in far less memory: -``auto_bisc(Perm.stack_sortable, labels="bitmaps")``. Such labels can also be -given directly as a ``LabelledPerms``, made from a property, from bitmaps, or -both, and ``bisc`` accepts it too. +``auto_bisc(Perm.stack_sortable, labels="bitmaps")``. Use it when ``auto_bisc`` +has to check long permutations: the permutations of length 10 take about 0.9 GB +as lists, and 454 KB as a bitmap. Such labels can also be given directly as a +``LabelledPerms``, made from a property, from bitmaps, or both, and ``bisc`` +accepts it too. .. code-block:: python - >>> labels = LabelledPerms(Perm.stack_sortable) + >>> labelled_perms = LabelledPerms(Perm.stack_sortable) >>> for length in range(6): - ... labels.add_length(length) - >>> len(labels.good[5]), len(labels.bad[5]) + ... labelled_perms.add_length(length) + >>> len(labelled_perms.good[5]), len(labelled_perms.bad[5]) (42, 78) - >>> bisc(labels, 3, 5) + >>> bisc(labelled_perms, 3, 5) {3: {Perm((1, 2, 0)): [set()]}} License diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index 4e4cc69..9008293 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -64,13 +64,27 @@ def bisc(A, m, n=None, report=False): def auto_bisc(prop, labels="lists"): """ - labels says how the labels of a property (a function) are kept: + Learn a basis of forbidden patterns for a set of permutations, growing + the length of the patterns and of the permutations they are learned from + until the patterns found describe the set on every permutation up to + length L, at least 8. Returns the basis, in the form bisc returns its + patterns, or None when the input is too short to check a basis. + + prop is the set: a property of permutations (a function), a list of + permutations, a tuple of two dictionaries (length -> the permutations in + the set, and those not in it), the name of a set in + permuta/resources/bisc, or a LabelledPerms. + + labels says how the labels of a property are kept: "lists" (the default, as before): the good and the bad permutations of each length as lists of Perm objects; "bitmaps": one bitmap per length (LabelledPerms), for the same result in far less memory, at the cost of generating a length's permutations again - each time it is read. - A LabelledPerms can also be given as prop directly. + each time it is read. Use it when the permutations checked are long: from + length 10 on the lists take gigabytes, the bitmaps under a megabyte. + + A LabelledPerms can also be given as prop directly. The lengths auto_bisc + labels with its property are added to it, so a later run reuses them. """ if labels not in ("lists", "bitmaps"): raise ValueError('labels must be "lists" or "bitmaps"') @@ -114,14 +128,13 @@ def auto_bisc(prop, labels="lists"): # and the bad permutations, generated length by length when they are # read, instead of lists. Lengths not given are labelled with the # LabelledPerms's property, if it has one - labelled = prop for i in range(L + 1): - if i not in labelled.bitmaps: - if labelled.prop is None: + if i not in prop.bitmaps: + if prop.prop is None: print("You should have labels up to length at least 8") return - labelled.add_length(i) - A, B = labelled.good, labelled.bad + prop.add_length(i) + A, B = prop.good, prop.bad elif ( isinstance(prop, tuple) @@ -266,12 +279,12 @@ def auto_bisc(prop, labels="lists"): elif isinstance(prop, LabelledPerms): for i in range(oldL + 1, L + 1): - if i not in labelled.bitmaps: - if labelled.prop is None: + if i not in prop.bitmaps: + if prop.prop is None: print("You need to add labels of longer permutations") return print("Adding perms of length {}".format(i)) - labelled.add_length(i) + prop.add_length(i) elif isinstance(prop, tuple) and L > min(max(A.keys()), max(B.keys())): print("You need to add longer permutations to the dictionaries") @@ -313,19 +326,26 @@ def auto_bisc(prop, labels="lists"): return +def _bitmap_size(length): + """The number of bytes in the bitmap of a length: a bit for each of its + permutations.""" + return (factorial(length) + 7) // 8 + + class LabelledPerms: """Labels of permutations, one bitmap per length, as input to auto_bisc and bisc: which permutations of each length satisfy a property (good) and which do not (bad). - Bit i of length n is the label of Perm.unrank(i, n), in byte i // 8, from - the low bit; so a permutation perm of length n is at bit - perm.rank() - sum(factorial(k) for k in range(n)). This is the order in - which Perm.of_length(n) yields the permutations (lexicographic), which the - views use to iterate. The bitmaps can be given (bitmaps={n: bytes}), made - from a property (add_length), or both: auto_bisc labels the lengths it - needs and is not given with prop, and stops, as for a tuple of - dictionaries, when there is no prop. + Bit i of a length's bitmap is the label of Perm.unrank(i, length), in + byte i // 8, from the low bit; so a permutation perm is at bit + perm.rank() - sum(factorial(k) for k in range(len(perm))). This is the + order in which Perm.of_length(length) yields the permutations + (lexicographic), which the views use to iterate. The bitmaps can be given + (bitmaps={length: bytes}), made from a property (add_length), or both: + auto_bisc labels the lengths it needs and is not given with prop, adding + them to this object, and stops, as for a tuple of dictionaries, when + there is no prop. auto_bisc reads its good and bad permutations only length by length: iterating over a length, counting it, asking whether a length is there. @@ -338,27 +358,29 @@ class LabelledPerms: def __init__(self, prop=None, bitmaps=None): self.prop = prop self.bitmaps = {} - self.counts = {} + self.good_counts = {} for length, bitmap in (bitmaps or {}).items(): bitmap = bytes(bitmap) - if len(bitmap) != (factorial(length) + 7) // 8: + if len(bitmap) != _bitmap_size(length): raise ValueError( "A bitmap of length {} has the wrong size".format(length) ) self.bitmaps[length] = bitmap - self.counts[length] = sum(bin(byte).count("1") for byte in bitmap) + self.good_counts[length] = sum(bin(byte).count("1") for byte in bitmap) self.good = _LengthViews(self, True) self.bad = _LengthViews(self, False) def add_length(self, length): - bitmap = bytearray((factorial(length) + 7) // 8) - count = 0 + """Label the permutations of a length with the property, and keep + their labels as the length's bitmap.""" + bitmap = bytearray(_bitmap_size(length)) + good_count = 0 for i, perm in enumerate(Perm.of_length(length)): if self.prop(perm): bitmap[i >> 3] |= 1 << (i & 7) - count += 1 - self.bitmaps[length] = bitmap - self.counts[length] = count + good_count += 1 + self.bitmaps[length] = bytes(bitmap) + self.good_counts[length] = good_count class _LengthViews: @@ -395,12 +417,9 @@ def __iter__(self): yield perm def __len__(self): - good_count = self.labelled.counts[self.length] + good_count = self.labelled.good_counts[self.length] return good_count if self.wanted_bit else factorial(self.length) - good_count - def __bool__(self): - return len(self) > 0 - def create_bisc_input(N, prop): """ diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 4e03f06..235e69f 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -7,96 +7,83 @@ from permuta.bisc.bisc import LabelledPerms, auto_bisc, bisc from permuta.patterns.perm import Perm +BASIS_231 = {3: {Perm((1, 2, 0)): [set()]}} -def stack_sortable(perm): - return perm.avoids(Perm((1, 2, 0))) - -def labelled(prop, max_length): - labels = LabelledPerms(prop) +def labelled_up_to(prop, max_length): + labelled_perms = LabelledPerms(prop) for length in range(max_length + 1): - labels.add_length(length) - return labels + labelled_perms.add_length(length) + return labelled_perms def test_views_match_lists(): - labels = labelled(stack_sortable, 6) + labelled_perms = labelled_up_to(Perm.stack_sortable, 6) for length in range(7): - good = [perm for perm in Perm.of_length(length) if stack_sortable(perm)] - bad = [perm for perm in Perm.of_length(length) if not stack_sortable(perm)] - assert list(labels.good[length]) == good - assert list(labels.bad[length]) == bad - assert len(labels.good[length]) == len(good) - assert len(labels.bad[length]) == len(bad) - assert bool(labels.bad[length]) == bool(bad) - assert sorted(labels.good.keys()) == list(range(7)) - assert 6 in labels.bad and 7 not in labels.bad + good = [perm for perm in Perm.of_length(length) if perm.stack_sortable()] + bad = [perm for perm in Perm.of_length(length) if not perm.stack_sortable()] + assert list(labelled_perms.good[length]) == good + assert list(labelled_perms.bad[length]) == bad + assert len(labelled_perms.good[length]) == len(good) + assert len(labelled_perms.bad[length]) == len(bad) + assert bool(labelled_perms.bad[length]) == bool(bad) + assert sorted(labelled_perms.good.keys()) == list(range(7)) + assert 6 in labelled_perms.bad and 7 not in labelled_perms.bad with pytest.raises(KeyError): - _ = labels.good[7] + _ = labelled_perms.good[7] def test_bit_order_is_perm_unrank(): - # Bit i of length n is the label of Perm.unrank(i, n), and the permutation - # perm is at bit perm.rank() minus the number of shorter permutations - labels = labelled(stack_sortable, 6) + # Bit i of a length's bitmap is the label of Perm.unrank(i, length), and the + # permutation perm is at bit perm.rank() minus the number of shorter + # permutations + labelled_perms = labelled_up_to(Perm.stack_sortable, 6) shorter_count = 0 for length in range(7): - bitmap = labels.bitmaps[length] + bitmap = labelled_perms.bitmaps[length] for i in range(factorial(length)): perm = Perm.unrank(i, length) assert perm.rank() - shorter_count == i - assert bool(bitmap[i >> 3] >> (i & 7) & 1) == stack_sortable(perm) + assert bool(bitmap[i >> 3] >> (i & 7) & 1) == perm.stack_sortable() shorter_count += factorial(length) def test_bitmaps_round_trip(): - labels = labelled(stack_sortable, 6) - round_trip = LabelledPerms(bitmaps=labels.bitmaps) + labelled_perms = labelled_up_to(Perm.stack_sortable, 6) + round_trip = LabelledPerms(bitmaps=labelled_perms.bitmaps) for length in range(7): - assert list(round_trip.good[length]) == list(labels.good[length]) - assert list(round_trip.bad[length]) == list(labels.bad[length]) + assert list(round_trip.good[length]) == list(labelled_perms.good[length]) + assert list(round_trip.bad[length]) == list(labelled_perms.bad[length]) with pytest.raises(ValueError): LabelledPerms(bitmaps={3: b"\x00\x00"}) # 3! bits fit in one byte def test_bisc_accepts_labelled_perms(): - expected = {3: {Perm((1, 2, 0)): [set()]}} - assert bisc(stack_sortable, 3, 5) == expected - assert bisc(labelled(stack_sortable, 5), 3, 5) == expected + assert bisc(Perm.stack_sortable, 3, 5) == BASIS_231 + assert bisc(labelled_up_to(Perm.stack_sortable, 5), 3, 5) == BASIS_231 def test_auto_bisc_labels_option(): - expected = {3: {Perm((1, 2, 0)): [set()]}} - assert auto_bisc(stack_sortable) == expected - assert auto_bisc(stack_sortable, labels="lists") == expected - assert auto_bisc(stack_sortable, labels="bitmaps") == expected + assert auto_bisc(Perm.stack_sortable) == BASIS_231 + assert auto_bisc(Perm.stack_sortable, labels="lists") == BASIS_231 + assert auto_bisc(Perm.stack_sortable, labels="bitmaps") == BASIS_231 with pytest.raises(ValueError): - auto_bisc(stack_sortable, labels="sets") + auto_bisc(Perm.stack_sortable, labels="sets") def test_auto_bisc_labelled_perms_input(): - expected = {3: {Perm((1, 2, 0)): [set()]}} - assert auto_bisc(labelled(stack_sortable, 8)) == expected + assert auto_bisc(labelled_up_to(Perm.stack_sortable, 8)) == BASIS_231 # Given bitmaps and no property: the lengths auto_bisc needs must be there - bitmaps = labelled(stack_sortable, 8).bitmaps - assert auto_bisc(LabelledPerms(bitmaps=bitmaps)) == expected + bitmaps = labelled_up_to(Perm.stack_sortable, 8).bitmaps + assert auto_bisc(LabelledPerms(bitmaps=bitmaps)) == BASIS_231 too_short = {length: bitmaps[length] for length in range(6)} assert auto_bisc(LabelledPerms(bitmaps=too_short)) is None def test_west_2_stack_sortable_same_as_lists(): - def west_2(perm): - def stack_sort(word): - output, stack = [], [] - for entry in word: - while stack and stack[-1] < entry: - output.append(stack.pop()) - stack.append(entry) - return output + stack[::-1] - - return stack_sort(stack_sort(list(perm))) == sorted(perm) - - assert auto_bisc(west_2, labels="bitmaps") == auto_bisc(west_2) + assert auto_bisc(Perm.west_2_stack_sortable, labels="bitmaps") == auto_bisc( + Perm.west_2_stack_sortable + ) def derangement_in_av_213_231_312(perm): @@ -109,7 +96,7 @@ def derangement_in_av_213_231_312(perm): def test_auto_bisc_stops_without_labels_it_needs(): # Bitmaps up to length 8 and no property: auto_bisc needs length 9 and stops - bitmaps = labelled(derangement_in_av_213_231_312, 8).bitmaps + bitmaps = labelled_up_to(derangement_in_av_213_231_312, 8).bitmaps output = io.StringIO() with contextlib.redirect_stdout(output): assert auto_bisc(LabelledPerms(bitmaps=bitmaps)) is None From 38c9dfab7b5a4579c551cd4cc76bc2fda78ccf4a Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:17:24 +0000 Subject: [PATCH 07/12] Reject malformed bitmaps in LabelledPerms 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 --- permuta/bisc/bisc.py | 17 +++++++++++++++-- tests/bisc/test_labelled_perms.py | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index 9008293..6e3fae5 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -345,7 +345,9 @@ class LabelledPerms: (bitmaps={length: bytes}), made from a property (add_length), or both: auto_bisc labels the lengths it needs and is not given with prop, adding them to this object, and stops, as for a tuple of dictionaries, when - there is no prop. + there is no prop. A bitmap given must be bytes-like (bytes, a bytearray + or a memoryview), of its length's size, with no bit set past the length's + permutations. auto_bisc reads its good and bad permutations only length by length: iterating over a length, counting it, asking whether a length is there. @@ -360,13 +362,24 @@ def __init__(self, prop=None, bitmaps=None): self.bitmaps = {} self.good_counts = {} for length, bitmap in (bitmaps or {}).items(): + if not isinstance(bitmap, (bytes, bytearray, memoryview)): + raise TypeError( + "A bitmap must be bytes, a bytearray or a memoryview, " + "not {}".format(type(bitmap).__name__) + ) bitmap = bytes(bitmap) if len(bitmap) != _bitmap_size(length): raise ValueError( "A bitmap of length {} has the wrong size".format(length) ) + label_bits = int.from_bytes(bitmap, "little") + if label_bits >> factorial(length): + raise ValueError( + "A bitmap of length {} has bits set past its {} " + "permutations".format(length, factorial(length)) + ) self.bitmaps[length] = bitmap - self.good_counts[length] = sum(bin(byte).count("1") for byte in bitmap) + self.good_counts[length] = label_bits.bit_count() self.good = _LengthViews(self, True) self.bad = _LengthViews(self, False) diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 235e69f..56761ad 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -58,6 +58,30 @@ def test_bitmaps_round_trip(): LabelledPerms(bitmaps={3: b"\x00\x00"}) # 3! bits fit in one byte +@pytest.mark.parametrize( + "length, bitmap", + [(0, b"\x03"), (1, b"\x81"), (2, b"\x04"), (3, b"\xff"), (3, b"\x40")], +) +def test_set_padding_bits_are_rejected(length, bitmap): + with pytest.raises(ValueError): + LabelledPerms(bitmaps={length: bitmap}) + + +@pytest.mark.parametrize("length, bitmap", [(0, 1), (3, 1), (4, 3), (5, 15)]) +def test_an_int_is_not_a_bitmap(length, bitmap): + with pytest.raises(TypeError): + LabelledPerms(bitmaps={length: bitmap}) + + +def test_bitmaps_may_be_any_bytes_like(): + # All six permutations of length 3 are good, and the first of length 2 + for bitmap in (b"\x3f", bytearray(b"\x3f"), memoryview(b"\x3f")): + labelled_perms = LabelledPerms(bitmaps={3: bitmap, 2: b"\x01"}) + assert len(labelled_perms.good[3]) == 6 and not labelled_perms.bad[3] + assert list(labelled_perms.good[2]) == [Perm((0, 1))] + assert isinstance(labelled_perms.bitmaps[3], bytes) + + def test_bisc_accepts_labelled_perms(): assert bisc(Perm.stack_sortable, 3, 5) == BASIS_231 assert bisc(labelled_up_to(Perm.stack_sortable, 5), 3, 5) == BASIS_231 From 58e57db92f9d504ca8601ea00b8827878f557f91 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:18:09 +0000 Subject: [PATCH 08/12] Test the labels option and LabelledPerms given a property and bitmaps 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 --- tests/bisc/test_labelled_perms.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 56761ad..3ae8086 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -95,6 +95,24 @@ def test_auto_bisc_labels_option(): auto_bisc(Perm.stack_sortable, labels="sets") +def test_labels_option_selects_the_representation(monkeypatch): + # The results are the same either way, so record which lengths are kept as + # bitmaps: none for lists, every length checked for bitmaps + added_lengths = [] + real_add_length = LabelledPerms.add_length + + def recording_add_length(self, length): + added_lengths.append(length) + return real_add_length(self, length) + + monkeypatch.setattr(LabelledPerms, "add_length", recording_add_length) + assert auto_bisc(Perm.stack_sortable) == BASIS_231 + assert auto_bisc(Perm.stack_sortable, labels="lists") == BASIS_231 + assert added_lengths == [] + assert auto_bisc(Perm.stack_sortable, labels="bitmaps") == BASIS_231 + assert added_lengths == list(range(9)) + + def test_auto_bisc_labelled_perms_input(): assert auto_bisc(labelled_up_to(Perm.stack_sortable, 8)) == BASIS_231 # Given bitmaps and no property: the lengths auto_bisc needs must be there @@ -104,6 +122,34 @@ def test_auto_bisc_labelled_perms_input(): assert auto_bisc(LabelledPerms(bitmaps=too_short)) is None +def encode(prop, length): + """A length's bitmap made without LabelledPerms: bit i of the little-endian + integer is the label of Perm.unrank(i, length).""" + label_bits = sum( + 1 << i for i in range(factorial(length)) if prop(Perm.unrank(i, length)) + ) + return label_bits.to_bytes((factorial(length) + 7) // 8, "little") + + +def test_auto_bisc_labels_only_missing_lengths_with_the_property(): + labelled_lengths = [] + + def prop(perm): + labelled_lengths.append(len(perm)) + return perm.stack_sortable() + + given_lengths = (0, 1, 2, 3, 5, 8) + labelled_perms = LabelledPerms( + prop, + bitmaps={ + length: encode(Perm.stack_sortable, length) for length in given_lengths + }, + ) + assert auto_bisc(labelled_perms) == BASIS_231 + assert set(labelled_lengths) == {4, 6, 7} + assert sorted(labelled_perms.bitmaps) == list(range(9)) + + def test_west_2_stack_sortable_same_as_lists(): assert auto_bisc(Perm.west_2_stack_sortable, labels="bitmaps") == auto_bisc( Perm.west_2_stack_sortable From a5268a9d00fb5a856849ef4c47a71fc87a9ef1b8 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:19:39 +0000 Subject: [PATCH 09/12] bisc labels what a LabelledPerms lacks, and labels="bitmaps" needs a 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 --- CHANGELOG.md | 3 ++- permuta/bisc/bisc.py | 36 ++++++++++++++++++++++++++++--- tests/bisc/test_labelled_perms.py | 27 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db41f0b..a7ceeb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - `LabelledPerms` in `permuta.bisc`: labels of permutations kept as one bitmap per length (lexicographic order), made from a property, given as bitmaps, or both. - `auto_bisc` and `bisc` accept it as input. + `auto_bisc` and `bisc` accept it as input, and label the lengths it lacks with + its property. - `auto_bisc(prop, labels="bitmaps")`: keeps a property's labels as bitmaps instead of lists of permutations, for the same result in far less memory at longer lengths (the permutations of length 10 take about 0.9 GB as lists, 454 KB as a diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index 6e3fae5..e00914e 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -20,6 +20,14 @@ def bisc(A, m, n=None, report=False): + """ + Mine the forbidden patterns of length up to m from the permutations up to + length n of a set A: a list of permutations, a property (a function), a + dictionary (length -> the permutations of that length in the set), or a + LabelledPerms. Without n, a property uses length 7, and the others their + longest length. A LabelledPerms's lengths up to n that are not labelled + are labelled with its property, and added to it. + """ if isinstance(A, list): D = defaultdict(list) for perm in A: @@ -38,6 +46,22 @@ def bisc(A, m, n=None, report=False): D = A elif isinstance(A, LabelledPerms): + if n is None: + if A.bitmaps: + n = max(A.bitmaps) + elif A.prop is not None: + print("I will use permutations up to length 7") + n = 7 + else: + raise ValueError("The LabelledPerms has no labels and no property") + for length in range(n + 1): + if length not in A.bitmaps: + if A.prop is None: + raise ValueError( + "bisc needs labels up to length {}, and the LabelledPerms " + "has none of length {} and no property".format(n, length) + ) + A.add_length(length) D = A.good elif isinstance(A, _LengthViews): @@ -88,8 +112,13 @@ def auto_bisc(prop, labels="lists"): """ if labels not in ("lists", "bitmaps"): raise ValueError('labels must be "lists" or "bitmaps"') - if labels == "bitmaps" and isinstance(prop, types.FunctionType): - prop = LabelledPerms(prop) + if labels == "bitmaps": + if isinstance(prop, types.FunctionType): + prop = LabelledPerms(prop) + elif not isinstance(prop, LabelledPerms): + raise ValueError( + 'labels="bitmaps" needs a property (a function) or a LabelledPerms' + ) L = 8 # Want to sanity check on at least S8, and one above n n = 4 m = 2 @@ -188,7 +217,8 @@ def auto_bisc(prop, labels="lists"): 1. A list of permutations 2. A property of permutations 3. A dictionary of lengths pointing to permutations - 4. A string """ + 4. A string + 5. A LabelledPerms (labels kept as bitmaps)""" ) assert False diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 3ae8086..bb537cf 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -150,6 +150,33 @@ def prop(perm): assert sorted(labelled_perms.bitmaps) == list(range(9)) +def test_bisc_labels_missing_lengths_with_the_property(): + # Without n, bisc uses the longest length labelled, or 7 when there is none + assert bisc(labelled_up_to(Perm.stack_sortable, 5), 3) == BASIS_231 + labelled_perms = LabelledPerms(Perm.stack_sortable) + assert bisc(labelled_perms, 3, 5) == BASIS_231 + assert sorted(labelled_perms.bitmaps) == list(range(6)) + labelled_perms = LabelledPerms(Perm.stack_sortable) + assert bisc(labelled_perms, 3) == BASIS_231 + assert sorted(labelled_perms.bitmaps) == list(range(8)) + + +def test_bisc_needs_labels_or_a_property(): + bitmaps = {length: encode(Perm.stack_sortable, length) for length in range(4)} + with pytest.raises(ValueError): + bisc(LabelledPerms(bitmaps=bitmaps), 3, 5) + with pytest.raises(ValueError): + bisc(LabelledPerms(), 3) + + +def test_bitmaps_need_a_property(): + for prop in ([Perm((0,))], ({}, {}), "yt_perm_avoids_22"): + with pytest.raises(ValueError): + auto_bisc(prop, labels="bitmaps") + labelled_perms = labelled_up_to(Perm.stack_sortable, 8) + assert auto_bisc(labelled_perms, labels="bitmaps") == BASIS_231 + + def test_west_2_stack_sortable_same_as_lists(): assert auto_bisc(Perm.west_2_stack_sortable, labels="bitmaps") == auto_bisc( Perm.west_2_stack_sortable From a8b80bf290a3da1f9e4dd394d5f54c3cac7347e0 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:46:46 +0000 Subject: [PATCH 10/12] Test how bisc treats a LabelledPerms, and label up to max(m, n) 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 --- permuta/bisc/bisc.py | 9 +++++--- tests/bisc/test_labelled_perms.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index e00914e..e03dfca 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -25,7 +25,8 @@ def bisc(A, m, n=None, report=False): length n of a set A: a list of permutations, a property (a function), a dictionary (length -> the permutations of that length in the set), or a LabelledPerms. Without n, a property uses length 7, and the others their - longest length. A LabelledPerms's lengths up to n that are not labelled + longest length. mine() reads the lengths up to m and the permutations up + to n, so a LabelledPerms's lengths up to max(m, n) that are not labelled are labelled with its property, and added to it. """ if isinstance(A, list): @@ -54,12 +55,14 @@ def bisc(A, m, n=None, report=False): n = 7 else: raise ValueError("The LabelledPerms has no labels and no property") - for length in range(n + 1): + for length in range(max(m, n) + 1): if length not in A.bitmaps: if A.prop is None: raise ValueError( "bisc needs labels up to length {}, and the LabelledPerms " - "has none of length {} and no property".format(n, length) + "has none of length {} and no property".format( + max(m, n), length + ) ) A.add_length(length) D = A.good diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index bb537cf..664c84e 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -169,6 +169,44 @@ def test_bisc_needs_labels_or_a_property(): bisc(LabelledPerms(), 3) +def test_bisc_without_n_uses_the_longest_labelled_length_with_no_property(): + bitmaps = {length: encode(Perm.stack_sortable, length) for length in range(6)} + assert bisc(LabelledPerms(bitmaps=bitmaps), 3) == BASIS_231 + + +def test_bisc_labels_only_the_missing_lengths(): + labelled_lengths = [] + + def prop(perm): + labelled_lengths.append(len(perm)) + return perm.stack_sortable() + + given_lengths = (0, 1, 2, 3, 5) + labelled_perms = LabelledPerms( + prop, + bitmaps={ + length: encode(Perm.stack_sortable, length) for length in given_lengths + }, + ) + assert bisc(labelled_perms, 3) == BASIS_231 + assert labelled_lengths == [4] * factorial(4) + assert sorted(labelled_perms.bitmaps) == list(range(6)) + assert bisc(labelled_perms, 3, 5) == BASIS_231 + assert labelled_lengths == [4] * factorial(4) + + +def test_bisc_labels_up_to_the_pattern_length(): + # mine() reads the lengths up to m, so bisc labels up to m when n < m + labelled_perms = LabelledPerms(Perm.stack_sortable) + bisc(labelled_perms, 3, 0) + assert sorted(labelled_perms.bitmaps) == [0, 1, 2, 3] + labelled_perms = LabelledPerms(Perm.stack_sortable, bitmaps={0: b"\x01"}) + bisc(labelled_perms, 3) + assert sorted(labelled_perms.bitmaps) == [0, 1, 2, 3] + with pytest.raises(ValueError): + bisc(LabelledPerms(bitmaps={0: b"\x01"}), 3) + + def test_bitmaps_need_a_property(): for prop in ([Perm((0,))], ({}, {}), "yt_perm_avoids_22"): with pytest.raises(ValueError): From 69057c8fc4b7e94f9439e726a4598c90cfeeffe1 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:47:23 +0000 Subject: [PATCH 11/12] Correct the memory figures in the docs 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 --- CHANGELOG.md | 5 +++-- README.rst | 8 ++++---- permuta/bisc/bisc.py | 8 +++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ceeb3..530c085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. its property. - `auto_bisc(prop, labels="bitmaps")`: keeps a property's labels as bitmaps instead of lists of permutations, for the same result in far less memory at longer - lengths (the permutations of length 10 take about 0.9 GB as lists, 454 KB as a - bitmap). `labels="lists"`, the default, is the previous behaviour. + lengths (the permutations of length 10 take 0.9 to 1.8 GB as lists of `Perm` + objects, depending on the Python, and 454 KB as a bitmap). `labels="lists"`, + the default, is the previous behaviour. ### Fixed - Fixed bug in `Perm.rtlmax_ltrmin_decomposition` and fixed associated tests. diff --git a/README.rst b/README.rst index 7b934c2..bc3ab2b 100644 --- a/README.rst +++ b/README.rst @@ -522,10 +522,10 @@ a list of permutations (satisfying some property), or a ``LabelledPerms``. not, as lists. For long runs it can keep them as one bitmap per length instead, with the same result in far less memory: ``auto_bisc(Perm.stack_sortable, labels="bitmaps")``. Use it when ``auto_bisc`` -has to check long permutations: the permutations of length 10 take about 0.9 GB -as lists, and 454 KB as a bitmap. Such labels can also be given directly as a -``LabelledPerms``, made from a property, from bitmaps, or both, and ``bisc`` -accepts it too. +has to check long permutations: the permutations of length 10 take 0.9 to 1.8 GB +as lists of ``Perm`` objects, depending on the Python, and 454 KB as a bitmap. +Such labels can also be given directly as a ``LabelledPerms``, made from a +property, from bitmaps, or both, and ``bisc`` accepts it too. .. code-block:: python diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index e03dfca..7bb93de 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -107,8 +107,10 @@ def auto_bisc(prop, labels="lists"): each length as lists of Perm objects; "bitmaps": one bitmap per length (LabelledPerms), for the same result in far less memory, at the cost of generating a length's permutations again - each time it is read. Use it when the permutations checked are long: from - length 10 on the lists take gigabytes, the bitmaps under a megabyte. + each time it is read. Use it when the permutations checked are long: those + of length 10 take 0.9 to 1.8 GB as lists of Perm objects, depending on the + Python, and 454 KB as a bitmap, a bit for each permutation (5 MB at length + 11, 60 MB at length 12). A LabelledPerms can also be given as prop directly. The lengths auto_bisc labels with its property are added to it, so a later run reuses them. @@ -387,7 +389,7 @@ class LabelledPerms: The views good and bad do that from the bitmaps, generating a length's permutations again each time it is iterated, in the same order as the lists a property input makes. A bitmap of S_10 is 454 KB; S_10 as a list - of Perm objects is about 0.9 GB. + of Perm objects takes 0.9 GB on PyPy 3.11 and 1.3 to 1.8 GB on CPython. """ def __init__(self, prop=None, bitmaps=None): From c54c54200bf2b386c4894287a8f3527cc9538c56 Mon Sep 17 00:00:00 2001 From: Henning Ulfarsson Date: Thu, 22 Oct 2026 21:50:15 +0000 Subject: [PATCH 12/12] Docstrings, messages and edge-case tests for LabelledPerms 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 --- permuta/bisc/bisc.py | 22 +++++++++++++-------- tests/bisc/test_labelled_perms.py | 33 ++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/permuta/bisc/bisc.py b/permuta/bisc/bisc.py index 7bb93de..0b0495c 100644 --- a/permuta/bisc/bisc.py +++ b/permuta/bisc/bisc.py @@ -27,7 +27,12 @@ def bisc(A, m, n=None, report=False): LabelledPerms. Without n, a property uses length 7, and the others their longest length. mine() reads the lengths up to m and the permutations up to n, so a LabelledPerms's lengths up to max(m, n) that are not labelled - are labelled with its property, and added to it. + are labelled with its property, and added to it. report prints the + progress of the search. + + Returns the forbidden patterns found: a dictionary from length to + {pattern: [shadings]}, where each shading is a set of boxes, and an empty + one makes the pattern classical. """ if isinstance(A, list): D = defaultdict(list) @@ -221,7 +226,7 @@ def auto_bisc(prop, labels="lists"): """BiSC can accept inputs: 1. A list of permutations 2. A property of permutations - 3. A dictionary of lengths pointing to permutations + 3. A tuple of two dictionaries of lengths pointing to permutations 4. A string 5. A LabelledPerms (labels kept as bitmaps)""" ) @@ -378,11 +383,11 @@ class LabelledPerms: order in which Perm.of_length(length) yields the permutations (lexicographic), which the views use to iterate. The bitmaps can be given (bitmaps={length: bytes}), made from a property (add_length), or both: - auto_bisc labels the lengths it needs and is not given with prop, adding - them to this object, and stops, as for a tuple of dictionaries, when - there is no prop. A bitmap given must be bytes-like (bytes, a bytearray - or a memoryview), of its length's size, with no bit set past the length's - permutations. + auto_bisc and bisc label the lengths they need and are not given with + prop, adding them to this object. Without a prop, auto_bisc stops, as for + a tuple of dictionaries, and bisc raises a ValueError. A bitmap given must + be bytes-like (bytes, a bytearray or a memoryview), of its length's size, + with no bit set past the length's permutations. auto_bisc reads its good and bad permutations only length by length: iterating over a length, counting it, asking whether a length is there. @@ -420,7 +425,8 @@ def __init__(self, prop=None, bitmaps=None): def add_length(self, length): """Label the permutations of a length with the property, and keep - their labels as the length's bitmap.""" + their labels as the length's bitmap. It needs a property, and replaces + the length's bitmap if there is one.""" bitmap = bytearray(_bitmap_size(length)) good_count = 0 for i, perm in enumerate(Perm.of_length(length)): diff --git a/tests/bisc/test_labelled_perms.py b/tests/bisc/test_labelled_perms.py index 664c84e..b3a7380 100644 --- a/tests/bisc/test_labelled_perms.py +++ b/tests/bisc/test_labelled_perms.py @@ -29,7 +29,7 @@ def test_views_match_lists(): assert bool(labelled_perms.bad[length]) == bool(bad) assert sorted(labelled_perms.good.keys()) == list(range(7)) assert 6 in labelled_perms.bad and 7 not in labelled_perms.bad - with pytest.raises(KeyError): + with pytest.raises(KeyError, match="7"): _ = labelled_perms.good[7] @@ -54,8 +54,19 @@ def test_bitmaps_round_trip(): for length in range(7): assert list(round_trip.good[length]) == list(labelled_perms.good[length]) assert list(round_trip.bad[length]) == list(labelled_perms.bad[length]) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="wrong size"): LabelledPerms(bitmaps={3: b"\x00\x00"}) # 3! bits fit in one byte + with pytest.raises(ValueError, match="wrong size"): + LabelledPerms(bitmaps={3: b""}) + + +def test_lengths_0_and_1_labelled_bad(): + labelled_perms = LabelledPerms(bitmaps={0: b"\x00", 1: b"\x00"}) + for length in (0, 1): + assert list(labelled_perms.good[length]) == [] + assert list(labelled_perms.bad[length]) == [Perm(range(length))] + assert len(labelled_perms.good[length]) == 0 + assert len(labelled_perms.bad[length]) == 1 @pytest.mark.parametrize( @@ -63,13 +74,13 @@ def test_bitmaps_round_trip(): [(0, b"\x03"), (1, b"\x81"), (2, b"\x04"), (3, b"\xff"), (3, b"\x40")], ) def test_set_padding_bits_are_rejected(length, bitmap): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="bits set past"): LabelledPerms(bitmaps={length: bitmap}) @pytest.mark.parametrize("length, bitmap", [(0, 1), (3, 1), (4, 3), (5, 15)]) def test_an_int_is_not_a_bitmap(length, bitmap): - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="must be bytes"): LabelledPerms(bitmaps={length: bitmap}) @@ -91,7 +102,7 @@ def test_auto_bisc_labels_option(): assert auto_bisc(Perm.stack_sortable) == BASIS_231 assert auto_bisc(Perm.stack_sortable, labels="lists") == BASIS_231 assert auto_bisc(Perm.stack_sortable, labels="bitmaps") == BASIS_231 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="labels must be"): auto_bisc(Perm.stack_sortable, labels="sets") @@ -163,9 +174,9 @@ def test_bisc_labels_missing_lengths_with_the_property(): def test_bisc_needs_labels_or_a_property(): bitmaps = {length: encode(Perm.stack_sortable, length) for length in range(4)} - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="none of length 4 and no property"): bisc(LabelledPerms(bitmaps=bitmaps), 3, 5) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="no labels and no property"): bisc(LabelledPerms(), 3) @@ -203,13 +214,17 @@ def test_bisc_labels_up_to_the_pattern_length(): labelled_perms = LabelledPerms(Perm.stack_sortable, bitmaps={0: b"\x01"}) bisc(labelled_perms, 3) assert sorted(labelled_perms.bitmaps) == [0, 1, 2, 3] - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="none of length 1 and no property"): bisc(LabelledPerms(bitmaps={0: b"\x01"}), 3) +def test_bisc_accepts_the_views(): + assert bisc(labelled_up_to(Perm.stack_sortable, 5).good, 3) == BASIS_231 + + def test_bitmaps_need_a_property(): for prop in ([Perm((0,))], ({}, {}), "yt_perm_avoids_22"): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="needs a property"): auto_bisc(prop, labels="bitmaps") labelled_perms = labelled_up_to(Perm.stack_sortable, 8) assert auto_bisc(labelled_perms, labels="bitmaps") == BASIS_231