Repository navigation
Conversation
Tiling.is_empty took the first gridded perm that MinimalGriddedPerms.minimal_gridded_perms(yield_non_minimal=True) yields. That search takes its work packets shortest first, so it builds every shorter gridded perm before it yields one: on a row of n cells, each with a point requirement, every order of the points in the first n - 1 cells. MinimalGriddedPerms.find_gridded_perm takes the same work packets, made by the same rules, depth first, and returns the first gridded perm that avoids the obstructions and contains the requirements, or None. It skips a packet only if the same packet, its minimum indices too, was made before, so it takes every packet that minimal_gridded_perms takes before its first gridded perm, in some order, and finds one whenever minimal_gridded_perms yields one. minimal_gridded_perms is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A depth-first search can spend seconds in branches that hold no gridded permutation, on tilings where taking the shortest packet first finds one within milliseconds. find_gridded_perm now takes the packet of least bound first: the length of its gridded permutation, plus the points it lacks in each cell, and at least one, a lower bound on the length of every gridded permutation made from it. Of equal bounds the longest is taken first, so that where every point inserted is one the packet lacks, as for a row of cells each with a point requirement, the search still goes straight to a gridded permutation. A packet made again is skipped if one made before had minimum indices no larger in each cell that it, and the packets made from it, may still insert into. The initial packets come from _prepare_queue, and minimal_gridded_perms works out its minimum indices with _next_minimum_indices too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed_perm minimal_gridded_perms now works out its minimum indices with _next_minimum_indices, and no test saw a fault on that line: worked out as if each point inserted were one place further right, the minimum indices lose minimal gridded perms, and with them the gridded perms that GriddedPermsOnTiling builds, while every test passes. test_minimal_gridded_perms_against_a_brute_force compares the minimal gridded perms with every gridded perm up to the length a minimal one can have, on three small tilings on which that fault loses some. Two tests insert a point into a cell in which no requirement list needs points, a path of find_gridded_perm that no test took. The test of a row of point requirements has twelve cells, in a row and in a column, so that it fails before this change on a fast machine too. find_gridded_perm's docstring states its argument in plainer words, defines the open cells apart, and says that the gridded perm returned need not be minimal. _packets_made_in_cell has the comments of _process_work_packet, whose steps it repeats, and each points at the other; the heap's entries are made in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unds Where the requirements have several points in a cell, the bound by counts is often loose: many packets one point short of a gridded perm share the least bound, and find_gridded_perm, taking the longest first, took them all, though most can never be finished, as the pattern in a cell is wrong, or the points of cells it can no longer insert into interleave wrongly. On some tilings of two or three cells it took several times as long as minimal_gridded_perms. After its first eight packets, find_gridded_perm compares each packet it takes with the patterns of the requirements in each cell. Nothing made from a packet inserts into a cell that is not open, so a requirement is in no gridded perm made from it if it lacks a point in such a cell, or if its points in those cells, as a gridded perm, are not contained in the packet's; the packet is dropped if that holds of every requirement of a list. Otherwise a requirement lacks, in each cell, its points there less the most of them that form a pattern the packet's points there contain, which gives a bound no lower than that by counts, and still a lower bound on the length of every gridded perm made from the packet that contains the requirements: a packet whose bound that raises goes back on the heap. Most searches take fewer than eight packets, and so pay nothing for this. The tests set PACKETS_TAKEN_BEFORE_PRUNING to 0, to check the dropping on small tilings against every gridded perm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in a list A fault that changes only which packets are dropped, or which go back on the heap, or how far a bound is raised, or the least taken in a list or the most over the lists, changes the order of the search but no answer, so the tests through find_gridded_perm do not see it. test_packets_taken takes five packets from a heap with the threshold at 0: one that can make no gridded perm is dropped, and one whose bound the patterns raise is taken after the longer packets of bound up to its new one. test_points_lacking_by_patterns_least_in_a_list_most_over_lists has a list of two requirements, and two lists that lack a point in one cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_cell_patterns took each cell's pattern with get_gridded_perm_in_cells, which builds a gridded perm, and goes over all the points, for every cell: 5.5 microseconds for eight points in three cells, against 1.5 in one pass over the points. On tilings where most packets are compared with the requirements' patterns, that was about 2% of the time. Three docstrings and a comment are shorter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tiling.is_empty looked for a gridded perm with MinimalGriddedPerms, a search built to make every minimal gridded perm once: a goal fixed at the start, an initial gridded perm, minimum indices, and a rule for a packet made twice whose answer can depend on the order of its heap. is_empty needs one gridded perm, not the minimal ones. GriddedPermSearch places the points of one requirement at a time, each at a point already there or at a new point, where the requirement's order allows. Its state is the gridded perm and the requirement being placed, so a state made twice is skipped, in any order, and every gridded perm on the tiling leads it to one. It goes depth first, its children made as they are taken; a first pass of 64 states; then each pair of lists alone, as a pair with no gridded perm leaves the tiling with none; then the search again, dropping a gridded perm that no gridded perm on the tiling contains: one with a list that cannot be placed in it alone, or that needs more points in a cell than it can take there. MinimalGriddedPerms is develop's again: find_gridded_perm, its bounds and their tests are gone. The tests of is_empty stay, and a test of a long requirement in one cell joins them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GriddedPermSearch.find checked every obstruction for the empty perm, and grouped the obstructions by a set of each one's cells made afresh: two passes over the obstructions a call, which on tilings of about a thousand obstructions made a call about 1.8 times as long. It now groups them by the set of cells a gridded perm keeps, whose hash the set keeps, and notes the empty perm in the same pass; checking a gridded perm against the obstructions is a loop again, not any() over a generator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_find_past_the_first_pass gave the times of an earlier version of the search, and test_find_a_column_of_two_cells named develop's search for a fault that was in a search of minimal gridded perms like it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GriddedPermSearch searched each pair of lists alone after its first pass, and then all the lists in one order, with the look-aheads. Where a list placed early is ruled out only by a list placed late, or where three lists cannot all hold behind lists that have nothing to do with them, that search placed the early lists in every way before it met the late ones, and took seconds where MinimalGriddedPerms took a tenth of a second. After the first pass, the lists are now split into groups whose cells are apart and that no obstruction joins. Each group is searched alone, and the gridded perms found for the groups are put together. The lists of a group are searched again and again with the look-aheads, each search taking twice the states of the one before, until one finishes; each places first the lists that have met the most dead ends so far. The pairs of lists are no longer searched. The class docstring opens with how to use the class, and gives the argument under a heading of its own. The variables are named for what they hold, and the obstructions are grouped when the search is made. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… call A Counter takes longer to make than a defaultdict, and the list of the obstructions' sets of cells copied what the grouping already holds. On the tilings of a search for Av(123465, 564321), the two made a call about one per cent longer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the first pass, the groups of lists were searched one at a time, each to its end. Beside a group whose gridded perm takes long to find, an empty group that a first pass decides at once waited for that whole search, and took thousands of times as long as MinimalGriddedPerms. Each group now has a first pass of its own, and the groups not yet decided are searched in rounds, one search of each in a round, each round with twice the budget of the one before. A search that finishes decides its group, and the rounds stop as soon as a group has no gridded perm, or every group has one. A look-ahead's search of one list had no budget, and one could cost as much as the whole search of a hard group. It now takes its states from those left to the search it looks ahead for, so that a search takes at most its budget's states in all. _combined ends with Perm.to_standard, and _groups uses Permuta's UnionFind and gives both directions of its argument. The obstructions' index has names that do not speak of groups, the dead ends are a Counter, and the class docstring defines what a list's being satisfied and a dead end are, and says why the search ends. The tests add the two families, the rounds, the look-aheads' states, the dead ends' counts and _combined's order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One group with no gridded perm decides the search, so the groups now go smallest first, by the sum over their lists of the length of each list's longest gridded perm. Beside a large group that has a gridded perm, a small empty group is so decided before the large one's first pass. test_find_past_the_first_pass's docstring gives the time its test allows, not a time it does not test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The count started as math.inf, so that a look-ahead called alone has no bound, and became an int when a search set its budget. Under PyPy an attribute that changes from a float to an int made every call slower, and most on tilings that a search decides at once. It now starts at sys.maxsize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list meets dead ends only once a search reaches it, so a list with none may not have been reached. Of the lists with as many dead ends, the one whose longest gridded perm is shortest now goes first: it is the quickest to place, or to find that it cannot be placed. Three lists that cannot all hold, joined to several copies of the hard lists, were decided only after a round for each copy, each round twice as long as the one before. They are now decided in the first round. The test of lists that cannot all hold joined to the hard lists takes up to four copies, and the test of the restarts' budgets a tiling that still takes three rounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GriddedPermSearch and its find have examples, which run as doctests, after a one-line summary each. The groups' docstring says that their order bounds the length of a minimal gridded perm, as Tiling.maximum_length_of_minimum_gridded_perm does, and that a group is close to a factor with interleaving. Names, comments and docstrings now say what they hold and do: the gridded perm found for each group, a budget only for a search that is not a look-ahead's, and why the count of states is an int. Five tests of the groups' first passes and order, and of a group after one with no gridded perm; one helper records the searches and how each ended, in place of two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LEFT, MIDDLE and RIGHT in place of A, B and C, and the lists in them named the same way, as tests/test_tiling.py names its cells. The tests are otherwise unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Of lists with as many dead ends, the one whose longest gridded perm is shortest goes first, and not the one with the shortest gridded perm or the fewest points in all. The groups' first passes keep the search's order, and only the rounds after them put the lists in the order of their dead ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_ordered_by_dead_ends, _search_undecided_groups, PlacingKey and _combined's part_index say what each is. _longest writes once the length of a list's longest gridded perm, which three places worked out. _find puts the groups' first pass in an if of its own, and orders each round's groups itself, so that _search_undecided_groups searches the groups in the order given, and look_ahead switches the look-aheads only. The searches are the same: a group's searches change the dead ends of its own lists alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
searches_recorded returns each search as a Search, whose lists, budget, look_ahead and outcome the tests name where they counted underscores. A list of one point in the left, the middle or the right cell is IN_LEFT, IN_MIDDLE or IN_RIGHT, where tests built it again, and point now means a gridded perm alone. test_groups_are_searched_in_rounds finds a group by the set of its lists, as the rounds can change their order, and _combined's test names its cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Of lists with as many dead ends, the rounds placed the shortest first. That put many short lists ahead of longer lists that cannot all hold together, of which only the last placed meets dead ends: each search placed every short list first, and its work grew as the square of their number. A dead end now also counts, as a dead end of a joined list, for each list the rest satisfies that an obstruction joins to the list that met it, as its points may be what shut that list out. Of lists with as many dead ends, those with the most dead ends of joined lists go first, then those that the most obstructions the search can make use, then the shortest. An obstruction that uses a cell no list uses never occurs in the search, and is not counted. Tests: the review's tiling of 48 point lists beside the three lists, and three tilings built so that each of the middle two keys, and the obstructions counted only if the search can make them, is needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lists alike in every key keep their order; an empty list weighs nothing; more dead ends go before a shorter list, by their number; a list, a gridded perm and an obstruction given twice; and the three lists that cannot all hold made 12...L, joined to three copies of the hard lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The class's docstring says what the search does, then its limits, then its examples, as Perm's do; the examples show a list satisfied by either of its gridded perms, and Tiling.is_empty's call on a tiling, and find's a gridded perm that is not minimal. Each private docstring opens with a summary line, the detail after it. The code is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_ListCells works out, for a list once it is needed, its cells, the cells an obstruction joins to them, and the obstructions the search can make on them, which were four caches of the search's own. The search is the same. Its test counts two obstructions on one set of cells, so that a count of the sets of cells fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each search of the rounds must place the lists in the order of the dead ends the searches before it left; an order made once, before the rounds, passed every test. The padded test's name now says that it calls GriddedPermSearch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GriddedPermSearch's docstring keeps what the search finds, its limits and its examples, so help(GriddedPermSearch) shows what a caller needs. The sections on how the search works, why it finds a gridded perm when there is one, and what makes it fast move, unchanged but dedented, into the module's docstring, for those who work on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Define a joined list before the word is used, and say that a look-ahead's own search counts its dead ends for its list alone. Say which obstructions the order counts, those all of whose cells the lists use, in place of "the obstructions the search can make". The class's limits now say that the order is a heuristic, slower on tilings built against it. The examples' captions are sentences, _combined states its precondition, PlacingKey has a comment, two docstrings are reflowed, and the CHANGELOG says that find's gridded permutation need not be minimal. The code is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eir cells _ObstructionIndex holds the obstructions by the cells they use, and whether one is the empty perm. The search builds it first and hands it to _ListCells, which had shared two of the search's dictionaries and worked only because __init__ made them before it. _ListCells now also keeps the points of each list's gridded perms in each cell, which _closed_out kept in the search with an idiom of its own. The names say what they hold: joined_cells, obstruction_count, _dead_ends_of_joined_lists and _longest_length. The search takes the same states and finds the same gridded perms as before, on the families of the tests and on random tilings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… builders Nine tests in the search's file: the dead ends of joined lists add up, an obstruction joins two lists and a shared cell alone does not, the lists given are read by their indices, and a look-ahead's own search counts no dead end of a joined list; the list cells keep what they work out; each key of the rounds' order comes before the next; and find agrees with MinimalGriddedPerms on the brute force's tilings. In tests/test_tiling.py, the point lists beside three lists that can all hold, with the gridded perm found checked against the tiling. The family tests share builders for the three lists that cannot all hold and for the hard groups joined to them, and build the same tilings as before. The padded family has 28 point lists, where counting every obstruction fails it by far under either Python. The test of longer lists that cannot all hold is gone: no fault of the rounds' order that was tried fails it without failing the test of points too. Records of searches are named recorded, an outcome over budget is OVER_BUDGET, the tie-break test gives its lists in the reverse of their order, and two docstrings say what their tests check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Joins" is defined where the module docstring first uses it, in the groups' bullet. A look-ahead's own search counts its dead ends for its list alone because it is given that list alone. The bullet on the obstructions counted gives its reason and its analogy apart. The class docstring says that the gridded perm found depends on the order of the lists and of a list's gridded perms, not on that of the obstructions, and names its module. _ObstructionIndex gives a sentence to each attribute, obstruction_count's summary says which obstructions it counts, PlacingKey's comment reads as a sentence, and the CHANGELOG says where GriddedPermSearch is imported from. The code is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…full _ObstructionIndex.has_empty_obstruction, where empty read as an empty index; _ListCells.cells_of_lists, and obstruction_index for its local. In the tests: pattern and random_source for patt and rng, entry for a point's value, has_gridded_perm for the brute-force test's flag, and obstruction for ob; the builders take requirement_length and copy_count, and first_column by name. The search takes the same states and finds the same gridded perms, and the builders build the same tilings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cache test asks cell_counts and cells_of_lists twice too, and a new test gives cell_counts' values. Others pin a dead end for an empty list, which is falsy, and not for the list before it; generators taken as the obstructions and lists, as the types allow; and the search's order of the lists, which puts a list across two cells after those whose gridded perms each lie in one cell. The tests of the dead ends of joined lists, of the list cells and of the order's keys now sit with their siblings, and six docstrings say what they mean more plainly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tiling.is_emptyis now much faster on tilings with many requirement lists, such as those of TileScope's insertion encodings. A gridded permutation is a permutation with each point in a cell of a grid. A tiling has obstructions, gridded permutations that no gridded permutation of the tiling may contain, and requirement lists, each asking that every gridded permutation of the tiling contain one of the list's gridded permutations.is_emptyused to askMinimalGriddedPermsfor the first of the tiling's minimal gridded permutations, which it builds shortest first. It now asks a new search,GriddedPermSearch, for any one.The change is the new module
tilings/algorithms/gridded_perm_search.py, its export fromtilings/algorithms/__init__.py, the call inTiling.is_emptyintilings/tiling.py, two entries in the CHANGELOG, and the tests intests/algorithms/test_gridded_perm_search.pyandtests/test_tiling.py.MinimalGriddedPermsis unchanged, as its other users need the minimal gridded permutations.For example, on a row of ten cells, each with a point requirement and no obstructions,
is_emptyused to build every order of the points in the first nine cells before it found one: 2.1 s under PyPy 3.12 and 5.8 s under CPython 3.15. It now takes 0.3 ms and 0.09 ms:In a ten-minute TileScope search for Av(123465, 564321) with
TileScopePack.regular_insertion_encoding(DIR_SOUTH), the 25,168 calls ofis_emptytook 185.5 s before and 7.5 s after, under PyPy.Below, two cells side by side, each with a point requirement, and the decreasing pair across them an obstruction: the search finds the increasing pair. With the increasing pair an obstruction too, the tiling is empty.
Every class timed below is faster, or level where almost no tiling needs a search, and the answers are the same on every tiling checked. Some small tilings take a few milliseconds longer. Two families built against the search's order, one of them also in a form given straight to
GriddedPermSearch, take many times as long as before, the more so the larger they are: at 90 point lists under CPython 3.15, one 58 s where it took 0.044 s, the other over 60 s where it took 8.5 s. No TileScope search checked meets either ("Where it is slower", below).Details
Why it was slow.
Tiling.is_emptyaskedMinimalGriddedPerms.minimal_gridded_perms(yield_non_minimal=True)for a first gridded permutation. That search takes its work packets shortest first, as listing the minimal gridded permutations needs, so before it yields one it builds every shorter gridded permutation that avoids the obstructions. On a row of n cells, each with a point requirement, that is every allowed order of the points in the first n − 1 cells, up to (n − 1)! of them.Such tilings come up in TileScope searches with requirement insertions. In a search for Av(123465, 564321) with
TileScopePack.regular_insertion_encoding(DIR_SOUTH), the emptiness checks grow from about 0.01 s each to several seconds, until they take most of the search. The slowest are all rows of nine cells, each with a point requirement and none a point cell, with up to about 1200 obstructions, and none of them is empty.The search.
is_emptyneeds one gridded permutation, not the minimal ones.MinimalGriddedPermsdoes much that only listing them needs: a goal, one requirement of each list, fixed at the start; an initial gridded permutation merged from it; points inserted into a cell only to the right of the last one inserted there; and a rule for a packet made a second time.GriddedPermSearchhas none of these. It places the points of one requirement at a time, in the code's words:gridded_permthat avoids the obstructions, or a placing: such agridded_perm, arequirementbeing placed in it, andimages, the indices of the points ofgridded_permthat the requirement's leftmost points went to. At a rest, the first list thatgridded_permdoes not satisfy, that is, of which it contains no gridded permutation, is placed: there is a placing for each of its requirements. A rest that satisfies every list is returned.gridded_permin its cell, or to a new point inserted intogridded_permthere: to the right of where the requirement's point before it went, above the points that its points below it went to, and below those that its points above it went to. So every point inserted is a point of a requirement, put where the requirement's own order allows. When the requirement is placed in full, the gridded permutation is a rest again, one list more of it satisfied.gridded_perm, and the requirement with its first points at the points thatimagesgive, then it contains a child's too: the point of it that the requirement's next point is at is a point of its occurrence ofgridded_perm, or it containsgridded_permwith that point inserted, which so avoids the obstructions. So the search finds a gridded permutation whenever there is one.Making it fast.
gridded_permbefore new points. A new point in a cell is checked only against the obstructions that use the cell and no cell the gridded permutation does not use, the obstructions grouped by their cells once a call. This does whatMinimalGriddedPerms.insert_pointand its obstruction checks do, written again for speed.Timings, by CPU time, both versions on the same tilings in one process, in turn.
Every distinct tiling that
is_emptywas asked about in the first two minutes of a search (one minute for Av(12453), Av(123) and Av(1243, 2134), and all of the search, for Av(12354, 45321)), timed all at once, the least of three passes after a first under PyPy and of two under CPython, before → after, in seconds:regular_insertion_encoding(DIR_SOUTH)regular_insertion_encoding(DIR_SOUTH)insertion_row_and_col_placements()insertion_point_placements()insertion_point_placements()point_placements(length=2)point_placements()pattern_placements()point_and_row_and_col_placements()requirement_and_row_and_col_placements()requirement_placements()row_and_col_placements(row_only=True)In Av(1324) and Av(2413, 3142), almost no tiling needs a search (191 of 72,586, and none of 8,501), and the two are about level; their totals, and Av(1243, 2134)'s, move by up to a fifth from run to run.
In ten-minute searches under PyPy 3.12, every call of
is_emptyanswered both ways, alternately first, in seconds:The five slowest tilings of the first search take 7.78 s in all before under PyPy 3.12 (32.8 s under CPython 3.15), and 0.007 s after (0.004 s).
Random tilings. On 5,655 random tilings that need a search, of two or three cells in a row or a column, or 2 × 2, with up to three obstructions and two to four requirement lists of two to four points (33 of them empty), the least of three runs each: 23.3 s before and 0.52 s after under PyPy 3.12 (24.7 s and 0.58 s in a second run), and 52.7 s and 0.37 s under CPython 3.15. The 33 empty, one run each, take 11.9 s before and 0.056 s after under PyPy, and 32.9 s and 0.033 s under CPython.
Families built to be hard, the least of three calls, before → after, in seconds:
Where it is slower.
GriddedPermSearch, with each of the m cells above joined to the first of the three cells by an obstruction that needs two points there, which no gridded permutation of the search has, so that the lists are one group: 4.3 to 5.2 ms for m = 4 to 12 under PyPy, against 1.5 to 3.0 ms before, and 1.2 to 1.3 ms under CPython, against 0.3 to 1.0 ms. ATilingbuilt from these drops the joining obstructions.Tiling.is_empty: at 28 point lists, 0.013 s before and 0.56 s after under CPython 3.15, and 0.005 and 0.12 s under PyPy 3.12; at 60, 0.025 and 10.9 s, and 0.006 and 1.7 s; at 90, 0.044 and 58 s, and 0.008 and 7.7 s.GriddedPermSearch: the three lists 01 in one cell each, and K point cells each two joined by 10, the first joined to the first of the three. At 30 point lists, 0.21 s before and 2.6 s after under CPython 3.15, and at 60, 0.33 and 16 s; under PyPy 3.12, 3.1, 8.8 and 38 times the time before at 10, 30 and 60. ATilingrewrites its joins to the hard groups, and in the form aTilingkeeps it is the family above, which at 90 point lists makes 41 times the gridded permutations the code before makes.Tests (
tests/algorithms/test_gridded_perm_search.py, andtests/test_tiling.py):test_find_against_a_brute_force: 60 random small tilings, with a fixed seed (up to 3 × 1 or 2 × 2 cells, two or three lists of one or two requirements of up to three points, at most five points in a minimal gridded permutation, up to three obstructions), against every gridded permutation up to the length a minimal one can have; with the lists, and the requirements in each, in the search's order and reversed; with the first pass as it is, and with none, so that every tiling is also split into groups and searched in rounds with the look-aheads; andTiling.is_emptyon each.test_find_agrees_with_minimal_gridded_perms_on_the_brute_force_tilings: the same 60 tilings againstMinimalGriddedPerms, the wayis_emptyasked before.test_find_every_order_of_the_lists,test_find_every_order_but_one(four cells in a row, or in a column, each with a point requirement, and every order of the points but one an obstruction), andtest_find_the_only_gridded_perm(the tiling whose only gridded permutation is 516374280).test_search_makes_a_rest_and_a_placing_once,test_can_place_makes_a_placing_onceandtest_find_a_list_whose_gridded_perms_start_alike(the requirement is part of a placing's key: two requirements of a list can place their first points alike),test_search_counts_its_statesandtest_search_counts_the_states_of_its_look_aheads,test_can_place_takes_the_states_left,test_search_remembers_the_lists_a_rest_satisfies,test_unsatisfied_returns_the_lists_it_checked,test_avoids_keeps_the_obstructions_to_check, the order of the lists (among themtest_requirements_puts_a_list_across_cells_after_the_single_cell_lists: a list goes with the lists in one cell only if each of its requirements lies in one cell) and of each list, and_bounding_boxagainstGriddedPerm.get_bounding_box.test_groups,test_groups_smallest_first,test_groups_are_ordered_by_the_longest_gridded_perm_of_each_list,test_groups_an_empty_list_weighs_nothing,test_combinedandtest_combined_puts_an_earlier_gridded_perm_left_and_below,test_find_groups_alone,test_find_two_groups_apart_are_searched_alone,test_each_group_has_a_first_pass,test_two_groups_each_have_a_first_pass_of_their_own,test_a_single_group_has_no_first_pass_of_its_own,test_each_group_has_a_first_pass_in_the_search_order,test_groups_are_searched_in_rounds,test_find_stops_at_the_first_group_with_none,test_find_does_not_search_a_group_after_one_with_none,test_small_tilings_split_into_groupsandtest_find_few_lists_past_the_first_pass.test_dead_ends_put_a_list_first,test_find_places_the_lists_with_the_most_dead_ends_first(more dead ends first, then the shortest, ties as short in the search's order),test_find_places_more_dead_ends_before_a_shorter_list,test_each_round_orders_the_lists_by_the_dead_ends_so_far,test_restarts_double_their_budget(with a first pass of four states, so that the rounds take several budgets),test_restarts_with_no_first_pass_start_at_two_states,test_find_with_the_dead_ends_first(with a timeout of 1 s),test_find_twice_on_one_searchandtest_find_twice_starts_the_dead_ends_of_joined_lists_afresh,test_unmade_children_counts_each_dead_end,test_dropped_counts_a_dead_end,test_dropped_counts_each_dead_endandtest_dropped_counts_the_dead_end_for_an_empty_list_not_for_one_before_it,test_can_place_counts_the_dead_ends_it_meets,test_find_past_the_first_pass(timeout 1 s) andtest_find_two_lists_that_cannot_both_hold.test_count_dead_end_counts_it_for_the_joined_lists_its_rest_satisfies,test_count_dead_end_counts_every_joined_list_its_rest_satisfies,test_count_dead_end_adds_to_the_dead_ends_of_joined_lists,test_count_dead_end_joins_lists_by_an_obstruction_and_not_by_a_shared_cellandtest_count_dead_end_reads_the_lists_it_is_given_by_their_indices;test_unmade_children_counts_a_dead_end_for_the_joined_lists_it_satisfies,test_unmade_children_and_dropped_read_the_lists_they_are_givenandtest_can_place_counts_no_dead_end_of_a_joined_list;test_list_cells(a list's cells, the cells joined to them, and the obstructions on them that the order counts, each counted once)test_list_cells_keep_what_they_work_out(each of its five caches asked twice) andtest_list_cells_count_the_points_of_each_gridded_perm_in_each_cell;test_ordered_by_dead_ends_breaks_ties_by_each_key_in_turn,test_ordered_by_dead_ends_puts_each_key_before_the_next,test_ordered_by_dead_ends_weighs_a_list_by_its_longest_gridded_perm,test_ordered_by_dead_ends_keeps_the_order_of_lists_alike_in_every_key,test_ordered_by_dead_ends_weighs_an_empty_list_as_nothing, andtest_find_point_lists_joined_to_each_other_beside_lists_that_cannot_all_hold(timeout 1 s), which the search exceeds without the dead ends of joined lists.test_find_with_a_list_a_gridded_perm_and_an_obstruction_given_twiceandtest_find_takes_obstructions_and_lists_that_are_iterators: the search is given what it is given, where aTilingkeeps nothing twice, and takes generators, as its types say.test_look_aheads_cut_the_search(at most 200 states, of the 108 the search takes with them),test_can_place,test_closed_outandtest_closed_out_a_cell_with_the_points_the_list_needs.gridded_permbefore new points), a cell no list needs in full, the second requirement of a list, and a column of two cells whose lists put points in both, in turn.GriddedPermSearchandfind, which run as doctests.tests/test_tiling.py: twelve cells in a row, in a column, or on a diagonal, each with a point requirement, with a timeout of 1 s, whichis_emptyexceeds before this change under CPython 3.15 and PyPy 3.12 in the row and the column; 01 in each of 24 cells on a diagonal, past the first pass; rows of three cells with requirements of three and four points; two cells with 01…7 and n…21 in one (n = 22, 24); the families above, a list chosen early (n = 6, 7), three lists that cannot all hold (m = 5, 6), an empty group beside a hard one (m = 5, n = 6 and 7), the three lists joined to one, three or four hard groups (m = 5, n = 6, and for one also n = 7); 48 point lists beside three longer lists that cannot all hold; twelve point lists joined to each of the three, beside six copies of the hard group, and the same with the three able to hold, the gridded permutation found checked against the tiling; and, given straight toGriddedPermSearch, as aTilingtakes most of a second over so many obstructions, 28 point lists each with 80 padding cells above it. Each has a timeout of 1 s (2 s where the three can hold), which the search exceeds without the groups, the restarts, the rounds, the look-aheads' states in their budget, the shortest lists first among those with as many dead ends, the obstructions on a list's cells, or only the obstructions all of whose cells the lists use counted. Also the tiling whose list's two requirements begin alike, andis_emptywith one list, which does not search.Beside these: on 4,836 random tilings (635 of them empty: up to three cells in a row or a column, 2 × 2 cells, and rows of three to six cells with a point requirement each and obstructions across them), each searched with its lists as given and shuffled, with the first pass and with none, the search agrees with a check of every gridded permutation up to the length a minimal one can have. On every tiling of the first table, class by class, the two versions agree, and every gridded permutation the search returns is one of its tiling; so they do on every call of the ten-minute searches.
One line in
_ObstructionIndexreads a gridded permutation's_cells, the set of its cells that it keeps, with pylint's protected-access disabled there: making a set of each obstruction's cells, or grouping them by their positions, made a call about 1.8 times as long on tilings of about a thousand obstructions. A public property onGriddedPermwould be a change of its own.Checked with tox: flake8, mypy, pylint and black pass, and the 3835 tests and doctests pass on CPython 3.15 and on PyPy 3.12.
How it was checked before it was put up here, by an automated reviewer, at this head:
develop: 3,838 tests on each Python.tests/test_tiling.py.AI assistance: written with Claude Code (Opus 5.5) as the Tilings developer, and reviewed before submission by Claude Code (Opus 5.5) as an automated reviewer; Henning Ulfarsson checked and opened it.