Skip to content

Add the rotational cut strategy, for classes closed under rotation - #530

Open
ulfarsson wants to merge 8 commits into
developfrom
rotational-cut
Open

ulfarsson wants to merge 8 commits into
developfrom
rotational-cut

Conversation

@ulfarsson

@ulfarsson ulfarsson commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Adds the rotational cut, a strategy for permutation classes closed under rotation, and hints, which tell a TileScope user when a simpler class is worth searching for instead. Permuta#207 proposed this as an enumeration strategy in Permuta; it was closed because it belongs in Tilings as strategies, and this moves it here.

The idea

A rotation here is the cyclic shift that moves a permutation's first entry to its end (Perm.shift_left), not the quarter turn of Perm.rotate. The n rotations of a permutation of length n are distinct, and exactly one of them starts with its minimum. So a class closed under rotation has n times as many permutations of length n as its min cut, the permutations p for which the class has 1 followed by p raised by one, has of length n − 1. Exactly one rotation ends with the maximum too, so the max cut (p followed by a new maximum) counts the class the same way, and it can be a different class. A class closed under the same shift of its values has the same two cuts.

What it adds

  • RotationalCutFactory and RotationalCutStrategy (tilings/strategies/rotation.py): for a 1x1 tiling without assumptions whose class is closed under rotation, of its positions or of its values (the whole class, or its nonempty permutations), the rule to the min cut, and to the max cut when that is a different class. In a search, a cut that is closed under rotation again is cut again. The rule counts, generates and samples objects, so Rule.sanity_check checks all three.
  • RotationalCutRule, a NonBijectiveRule as FusionRule is: its forward map sends the n rotations to one object of the cut, and tells them apart by their index among the rotations, so a bijection between specifications through a cut is one to one.
  • TileScopePack.add_rotational_cuts and the tilescope spec flag -r/--rotational-cuts.
  • tilings.hints: hints_for(basis) returns the hints (Hint) for a class. The first is for a class closed under rotation: it names the class to search for instead, the cut, cut again while it is closed under rotation, and how its counts and generating function give the class's. tilescope spec logs the hints that its pack does not follow.
  • tilescope spec no longer ends with an error when it cannot compute the generating function of the specification it found; it says so. That happens for every specification with a rotational cut, as comb_spec_searcher's get_genf cannot solve an equation with a derivative, and also for develop's specifications with catalytic variables (as with --fusion on Av(123)).
  • The public helpers is_rotationally_stable, rotation_kind, min_cut, max_cut and max_cut_differs, and README and CHANGELOG entries.

Why a rule, not a verification

The cut is searched for like any other class, and the rule has an equation, generates objects and samples. A verification strategy would instead promise that a nested search enumerates the class. As comb_spec_searcher cannot compute a generating function through the rule, the hint points to the cut instead, whose generating function gives the class's.

Who will use it

TileScope users with a class closed under rotation: the hint tells them which simpler class to run, and the strategy cuts such classes wherever a search meets them. Henning's TileScope runs use it.

Tests

In tests/strategies/test_rotation.py, tests/test_hints.py, and the encoding and pack tests:

  • the cuts count the classes, against brute force; every rule passes its sanity check, for the whole class and its nonempty part, including a class closed only under rotation of its values, Av(12, 21), and a class closed under both;
  • the maps are inverse, and a bijection through a cut maps a specification to itself one to one;
  • the equation, also for a cut that is not a geometric series;
  • where the strategy does not apply, a basis that is not minimal, and the strategy's flags and JSON;
  • the chain of cuts, the hints and the class they name, and tilescope spec's hint logging, its flag, and its message when it cannot compute a generating function;
  • every pack in the pack tests with add_rotational_cuts, through JSON;
  • a search with point_placements().add_rotational_cuts() on [1234] = Av(1234, 2341, 3412, 4123), which finds a specification with a rotational cut in about a second, with counts 1, 1, 2, 6, 20, 65, 198, 560, 1480. Its generating function, derived from the cut's (Av(123, 3412)), is (1 - 8x + 27x^2 - 48x^3 + 49x^4 - 27x^5 + 6x^6 - 4x^7)/((1 - 2x)^2 (1 - x)^5).

How it was checked

Before submission, by an automated reviewer, against develop of all three libraries:

  • flake8, mypy, pylint and black pass, and on CPython 3.14 and PyPy 3.11 all 3,863 tests pass;
  • a brute-force check of every class made of one or two rotation orbits of patterns of lengths 3 and 4: the cuts count the class, each rule passes its sanity check, and the indexed maps are a bijection;
  • an independent implementation of the hint's chain of cuts, on 106 classes and in random sweeps of thousands more: the class it names, its counts and its generating function agree with brute force;
  • bijections between specifications through cuts, of positions and of values, min and max cuts, whole and nonempty, and after a JSON round trip, are one to one;
  • mutation testing of the changed code, and timing of standard packs on develop and on this branch, which run equally fast.

AI assistance: written with Claude Code (Opus 5.5) as the TileScope agent, and reviewed before submission by Claude Code (Opus 5.5) as an automated reviewer; Henning Ulfarsson checked and opened it.

ulfarsson and others added 4 commits October 19, 2026 11:37
A 1x1 tiling whose class is closed under rotation (the cyclic shift that moves the
first entry to the end), which is when its basis is rotationally stable, has n times
as many permutations of length n as its min cut, {p : 1 + p in the class}, or its
max cut, {p : p + 1 in the class}, has of length n - 1: exactly one rotation of each
permutation starts with its minimum, and exactly one ends with its maximum. A class
closed under the cyclic shift of the values (its inverse basis is rotationally
stable) has the same two cuts. RotationalCutFactory adds the rule to each cut, the
max cut only when it is a different class, with RotationalCutConstructor:
F(x) = 1 + x (x C(x))' for the whole class, and x (x C(x))' for its nonempty part.
TileScopePack.make_rotational adds the factory to a pack.

It moves the rotationally stable strategy of Permuta's pull request #207, which was
closed in favor of a strategy in Tilings, into a strategy here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uts and hints

TileScopePack.add_rotational_cuts (was make_rotational) adds the rotational cut
strategies to any pack, whose name gets the suffix _rotational_cuts, and
tilescope spec's new flag -r/--rotational-cuts does the same on the command line.

tilings.hints gathers hints: properties of a class that make it likely that a
change to a pack helps the search, where Permuta's enumeration strategies settle
a count outright. The first is a class closed under rotation: the hint names its
cuts and suggests the rotational cuts. tilescope spec logs the hints that the
pack it runs does not follow. The packs' JSON tests cover the new modifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code that builds its own argparse.Namespace for build_pack, as some callers did
before the flag existed, broke on the new attribute (an AttributeError, found on a
test run on a cluster). build_pack now reads it with getattr, and a test calls it
with such a namespace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tests

As a reviewer asked: pattern, patterns, obstruction, value, shift, length,
child_object, objects and jsonable in place of patt, patts, ob, val, k, n, obj,
objs and d, and size, gridded_perm, equation, parent_function and cut_function
in the tests. The parameters the strategy's base classes name (n, idx, obj,
objs, d) keep their names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ulfarsson and others added 3 commits October 25, 2026 14:50
…he CLI

The review's round 1, and Henning's directions for the hint.

- RotationalCutRule: the rule of a cut is a NonBijectiveRule, as
  FusionRule is. Its forward map sends the n rotations of 1 + p (or of
  p + 1) to p, and it tells them apart by their index among the rotations
  its backward map yields, so a bijection through a cut is one to one.
- The hint names the class to search for instead: the cut, cut again
  while it is closed under rotation, with n (n - 1) ... times the count of
  the last cut, and F = 1 + x (x G)' once for each cut. It still mentions
  the rotational cuts for the pack. A Hint now names the strategies that
  follow it, and the CLI asks it whether the pack has them
  (Hint.followed_by), rather than undoing argparse's naming of a flag.
  hints is now hints_for.
- tilescope spec says when it cannot compute the generating function of
  the specification it found, as for a cut, whose equation has a
  derivative, or for catalytic variables, rather than ending with an error.
- is_rotationally_stable minimizes the basis first, so it answers for the
  class. rotation_of is now rotation_kind, through one helper with the
  strategy. Permuta's shift_up, remove and inverse_set are used.
  max_cut_differs decides whether the max cut is another class, for the
  factory and the hint alike.
- A rotation is defined as the cyclic shift, not Perm.rotate's quarter
  turn, in the module, the README and the CHANGELOG. The README's example
  is a class whose cut is used, with the class the hint names. The
  CHANGELOG lists the public names.
- Tests, the review's among them: a bijection through a cut, the
  strategy's and the constructor's flags and predicates, a basis that is
  not minimal, Av(12, 21) and a class closed under both rotations, the
  chain of cuts down to Av(12), an equation for a cut that is not a
  geometric series, the hint's class and chain, and tilescope spec's hint
  logging and its message with no generating function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
comb_spec_searcher's auto_search checks for a specification at times that
depend on the clock, so with the rotational cuts it can find one with or
without a cut. On a loaded machine, PyPy 3.11 found Av(123, 231, 312)'s
without one, and the bijection test failed on its premise.

- test_a_bijection_through_a_cut_is_one_to_one puts its specification
  together from the cut's rule and a specification of the cut, so the cut
  is always in it. It still fails on a plain Rule and on either order
  method.
- test_spec_tells_of_the_hint_unless_the_pack_follows_it, with the flag,
  takes either message about the generating function: there is none to
  show when the specification has a cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant